mirror of
https://github.com/RGBCube/serenity
synced 2025-05-14 10:54:57 +00:00

The point of a reference type is to behave just like the referred-to type. So, a Foo& should behave just like a Foo. In these cases, we had a const Vector. If it was a const Vector of Foo, iterating over the Vector would only permit taking const references to the individual Foos. However, we had a const Vector of Foo&. The behavior should not change. We should still only be permitted to take const references to the individual Foos. Otherwise, we would be allowed to mutate the individual Foos, which would mutate the elements of the const Vector. This wouldn't modify the stored pointers, but it would modify the objects that the references refer to. Since references should be transparent, this should not be legal. So it should be impossible to get mutable references into a const Vector. Since we need mutable references in these cases to call the mutating member functions, we need to mark the Vector as mutable as well.
402 lines
13 KiB
C++
402 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
|
* Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Badge.h>
|
|
#include <WindowServer/ClientConnection.h>
|
|
#include <WindowServer/MenuManager.h>
|
|
#include <WindowServer/Screen.h>
|
|
#include <WindowServer/WindowManager.h>
|
|
|
|
namespace WindowServer {
|
|
|
|
static MenuManager* s_the;
|
|
|
|
MenuManager& MenuManager::the()
|
|
{
|
|
VERIFY(s_the);
|
|
return *s_the;
|
|
}
|
|
|
|
MenuManager::MenuManager()
|
|
{
|
|
s_the = this;
|
|
}
|
|
|
|
MenuManager::~MenuManager()
|
|
{
|
|
}
|
|
|
|
bool MenuManager::is_open(const Menu& menu) const
|
|
{
|
|
for (size_t i = 0; i < m_open_menu_stack.size(); ++i) {
|
|
if (&menu == m_open_menu_stack[i].ptr())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MenuManager::refresh()
|
|
{
|
|
ClientConnection::for_each_client([&](ClientConnection& client) {
|
|
client.for_each_menu([&](Menu& menu) {
|
|
menu.redraw();
|
|
return IterationDecision::Continue;
|
|
});
|
|
});
|
|
}
|
|
|
|
void MenuManager::event(Core::Event& event)
|
|
{
|
|
auto& wm = WindowManager::the();
|
|
|
|
if (static_cast<Event&>(event).is_mouse_event()) {
|
|
handle_mouse_event(static_cast<MouseEvent&>(event));
|
|
return;
|
|
}
|
|
|
|
if (static_cast<Event&>(event).is_key_event()) {
|
|
auto& key_event = static_cast<const KeyEvent&>(event);
|
|
|
|
if (key_event.type() == Event::KeyUp && key_event.key() == Key_Escape) {
|
|
close_everyone();
|
|
return;
|
|
}
|
|
|
|
if (m_current_menu && event.type() == Event::KeyDown
|
|
&& ((key_event.key() >= Key_A && key_event.key() <= Key_Z)
|
|
|| (key_event.key() >= Key_0 && key_event.key() <= Key_9))) {
|
|
|
|
if (auto* shortcut_item_indices = m_current_menu->items_with_alt_shortcut(key_event.code_point())) {
|
|
VERIFY(!shortcut_item_indices->is_empty());
|
|
// FIXME: If there are multiple items with the same Alt shortcut, we should cycle through them
|
|
// with each keypress instead of activating immediately.
|
|
auto index = shortcut_item_indices->at(0);
|
|
auto& item = m_current_menu->item(index);
|
|
m_current_menu->set_hovered_index(index);
|
|
if (item.is_submenu())
|
|
m_current_menu->descend_into_submenu_at_hovered_item();
|
|
else
|
|
m_current_menu->open_hovered_item(false);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (event.type() == Event::KeyDown) {
|
|
|
|
if (key_event.key() == Key_Left) {
|
|
auto it = m_open_menu_stack.find_if([&](const auto& other) { return m_current_menu == other.ptr(); });
|
|
VERIFY(!it.is_end());
|
|
|
|
// Going "back" a menu should be the previous menu in the stack
|
|
if (it.index() > 0)
|
|
set_current_menu(m_open_menu_stack.at(it.index() - 1));
|
|
else {
|
|
if (m_current_menu->hovered_item())
|
|
m_current_menu->set_hovered_index(-1);
|
|
else {
|
|
auto* target_menu = previous_menu(m_current_menu);
|
|
if (target_menu) {
|
|
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()));
|
|
open_menu(*target_menu);
|
|
wm.window_with_active_menu()->invalidate_menubar();
|
|
}
|
|
}
|
|
}
|
|
close_everyone_not_in_lineage(*m_current_menu);
|
|
return;
|
|
}
|
|
|
|
if (key_event.key() == Key_Right) {
|
|
auto hovered_item = m_current_menu->hovered_item();
|
|
if (hovered_item && hovered_item->is_submenu())
|
|
m_current_menu->descend_into_submenu_at_hovered_item();
|
|
else if (m_open_menu_stack.size() <= 1 && wm.window_with_active_menu()) {
|
|
auto* target_menu = next_menu(m_current_menu);
|
|
if (target_menu) {
|
|
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()));
|
|
open_menu(*target_menu);
|
|
wm.window_with_active_menu()->invalidate_menubar();
|
|
close_everyone_not_in_lineage(*target_menu);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (key_event.key() == Key_Return) {
|
|
auto hovered_item = m_current_menu->hovered_item();
|
|
if (!hovered_item || !hovered_item->is_enabled())
|
|
return;
|
|
if (hovered_item->is_submenu())
|
|
m_current_menu->descend_into_submenu_at_hovered_item();
|
|
else
|
|
m_current_menu->open_hovered_item(key_event.modifiers() & KeyModifier::Mod_Ctrl);
|
|
return;
|
|
}
|
|
m_current_menu->dispatch_event(event);
|
|
}
|
|
}
|
|
|
|
return Core::Object::event(event);
|
|
}
|
|
|
|
void MenuManager::handle_mouse_event(MouseEvent& mouse_event)
|
|
{
|
|
if (!has_open_menu())
|
|
return;
|
|
auto* topmost_menu = m_open_menu_stack.last().ptr();
|
|
VERIFY(topmost_menu);
|
|
auto* window = topmost_menu->menu_window();
|
|
if (!window) {
|
|
dbgln("MenuManager::handle_mouse_event: No menu window");
|
|
return;
|
|
}
|
|
VERIFY(window->is_visible());
|
|
|
|
bool event_is_inside_current_menu = window->rect().contains(mouse_event.position());
|
|
if (event_is_inside_current_menu) {
|
|
WindowManager::the().set_hovered_window(window);
|
|
WindowManager::the().deliver_mouse_event(*window, mouse_event, true);
|
|
return;
|
|
}
|
|
|
|
if (topmost_menu->hovered_item())
|
|
topmost_menu->clear_hovered_item();
|
|
if (mouse_event.type() == Event::MouseDown || mouse_event.type() == Event::MouseUp) {
|
|
auto* window_menu_of = topmost_menu->window_menu_of();
|
|
if (window_menu_of) {
|
|
bool event_is_inside_taskbar_button = window_menu_of->taskbar_rect().contains(mouse_event.position());
|
|
if (event_is_inside_taskbar_button && !topmost_menu->is_window_menu_open()) {
|
|
topmost_menu->set_window_menu_open(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (mouse_event.type() == Event::MouseDown) {
|
|
for (auto& menu : m_open_menu_stack) {
|
|
if (!menu)
|
|
continue;
|
|
if (!menu->menu_window()->rect().contains(mouse_event.position()))
|
|
continue;
|
|
return;
|
|
}
|
|
MenuManager::the().close_everyone();
|
|
topmost_menu->set_window_menu_open(false);
|
|
}
|
|
}
|
|
|
|
if (mouse_event.type() == Event::MouseMove) {
|
|
for (auto& menu : m_open_menu_stack) {
|
|
if (!menu)
|
|
continue;
|
|
if (!menu->menu_window()->rect().contains(mouse_event.position()))
|
|
continue;
|
|
WindowManager::the().set_hovered_window(menu->menu_window());
|
|
WindowManager::the().deliver_mouse_event(*menu->menu_window(), mouse_event, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MenuManager::close_all_menus_from_client(Badge<ClientConnection>, ClientConnection& client)
|
|
{
|
|
if (!has_open_menu())
|
|
return;
|
|
if (m_open_menu_stack.first()->client() != &client)
|
|
return;
|
|
close_everyone();
|
|
}
|
|
|
|
void MenuManager::close_everyone()
|
|
{
|
|
for (auto& menu : m_open_menu_stack) {
|
|
VERIFY(menu);
|
|
menu->set_visible(false);
|
|
menu->clear_hovered_item();
|
|
}
|
|
m_open_menu_stack.clear();
|
|
clear_current_menu();
|
|
}
|
|
|
|
void MenuManager::close_everyone_not_in_lineage(Menu& menu)
|
|
{
|
|
Vector<Menu&> menus_to_close;
|
|
for (auto& open_menu : m_open_menu_stack) {
|
|
if (!open_menu)
|
|
continue;
|
|
if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu))
|
|
continue;
|
|
menus_to_close.append(*open_menu);
|
|
}
|
|
close_menus(menus_to_close);
|
|
}
|
|
|
|
void MenuManager::close_menus(Vector<Menu&>& menus)
|
|
{
|
|
for (auto& menu : menus) {
|
|
if (&menu == m_current_menu)
|
|
clear_current_menu();
|
|
menu.set_visible(false);
|
|
menu.clear_hovered_item();
|
|
m_open_menu_stack.remove_first_matching([&](auto& entry) {
|
|
return entry == &menu;
|
|
});
|
|
}
|
|
}
|
|
|
|
static void collect_menu_subtree(Menu& menu, Vector<Menu&>& menus)
|
|
{
|
|
menus.append(menu);
|
|
for (size_t i = 0; i < menu.item_count(); ++i) {
|
|
auto& item = menu.item(i);
|
|
if (!item.is_submenu())
|
|
continue;
|
|
collect_menu_subtree(*item.submenu(), menus);
|
|
}
|
|
}
|
|
|
|
void MenuManager::close_menu_and_descendants(Menu& menu)
|
|
{
|
|
Vector<Menu&> menus_to_close;
|
|
collect_menu_subtree(menu, menus_to_close);
|
|
close_menus(menus_to_close);
|
|
}
|
|
|
|
void MenuManager::set_hovered_menu(Menu* menu)
|
|
{
|
|
if (m_hovered_menu == menu)
|
|
return;
|
|
if (menu) {
|
|
m_hovered_menu = menu->make_weak_ptr<Menu>();
|
|
} else {
|
|
// FIXME: This is quite aggressive. If we knew which window the previously hovered menu was in,
|
|
// we could just invalidate that one instead of iterating all windows in the client.
|
|
if (auto* client = m_hovered_menu->client()) {
|
|
client->for_each_window([&](Window& window) {
|
|
window.invalidate_menubar();
|
|
return IterationDecision::Continue;
|
|
});
|
|
}
|
|
m_hovered_menu = nullptr;
|
|
}
|
|
}
|
|
|
|
void MenuManager::open_menu(Menu& menu, bool as_current_menu)
|
|
{
|
|
if (menu.is_open()) {
|
|
if (as_current_menu || current_menu() != &menu) {
|
|
// This menu is already open. If requested, or if the current
|
|
// window doesn't match this one, then set it to this
|
|
set_current_menu(&menu);
|
|
}
|
|
return;
|
|
}
|
|
|
|
m_open_menu_stack.append(menu);
|
|
|
|
menu.set_visible(true);
|
|
|
|
if (!menu.is_empty()) {
|
|
menu.redraw_if_theme_changed();
|
|
auto* window = menu.menu_window();
|
|
VERIFY(window);
|
|
window->set_visible(true);
|
|
}
|
|
|
|
if (as_current_menu || !current_menu()) {
|
|
// Only make this menu the current menu if requested, or if no
|
|
// other menu is current
|
|
set_current_menu(&menu);
|
|
}
|
|
}
|
|
|
|
void MenuManager::clear_current_menu()
|
|
{
|
|
Menu* previous_current_menu = m_current_menu;
|
|
m_current_menu = nullptr;
|
|
if (previous_current_menu) {
|
|
// When closing the last menu, restore the previous active input window
|
|
auto& wm = WindowManager::the();
|
|
wm.restore_active_input_window(m_previous_input_window);
|
|
if (auto* window = wm.window_with_active_menu()) {
|
|
window->invalidate_menubar();
|
|
}
|
|
wm.set_window_with_active_menu(nullptr);
|
|
}
|
|
}
|
|
|
|
void MenuManager::set_current_menu(Menu* menu)
|
|
{
|
|
if (!menu) {
|
|
clear_current_menu();
|
|
return;
|
|
}
|
|
|
|
VERIFY(is_open(*menu));
|
|
if (menu == m_current_menu) {
|
|
return;
|
|
}
|
|
|
|
Menu* previous_current_menu = m_current_menu;
|
|
m_current_menu = menu;
|
|
|
|
auto& wm = WindowManager::the();
|
|
if (!previous_current_menu) {
|
|
// When opening the first menu, store the current active input window
|
|
if (auto* active_input = wm.active_input_window())
|
|
m_previous_input_window = *active_input;
|
|
else
|
|
m_previous_input_window = nullptr;
|
|
}
|
|
|
|
wm.set_active_input_window(m_current_menu->menu_window());
|
|
}
|
|
|
|
Menu* MenuManager::previous_menu(Menu* current)
|
|
{
|
|
auto& wm = WindowManager::the();
|
|
if (!wm.window_with_active_menu())
|
|
return nullptr;
|
|
Menu* found = nullptr;
|
|
Menu* previous = nullptr;
|
|
wm.window_with_active_menu()->menubar().for_each_menu([&](Menu& menu) {
|
|
if (current == &menu) {
|
|
found = previous;
|
|
return IterationDecision::Break;
|
|
}
|
|
previous = &menu;
|
|
return IterationDecision::Continue;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
Menu* MenuManager::next_menu(Menu* current)
|
|
{
|
|
Menu* found = nullptr;
|
|
bool is_next = false;
|
|
auto& wm = WindowManager::the();
|
|
if (!wm.window_with_active_menu())
|
|
return nullptr;
|
|
wm.window_with_active_menu()->menubar().for_each_menu([&](Menu& menu) {
|
|
if (is_next) {
|
|
found = &menu;
|
|
return IterationDecision::Break;
|
|
}
|
|
if (current == &menu)
|
|
is_next = true;
|
|
return IterationDecision::Continue;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
void MenuManager::did_change_theme()
|
|
{
|
|
++m_theme_index;
|
|
refresh();
|
|
}
|
|
|
|
}
|