mirror of
https://github.com/RGBCube/serenity
synced 2025-05-23 03:05:07 +00:00

Previously Menus set themselves as active input solely to make sure CaptureInput modals would close, but this is a functional half-truth. Menus don't actually use the active input role; they preempt normal Windows during event handling instead. Now the active input window is notified on preemption and Menus can remain outside the active input concept. This lets us make more granular choices about modal behavior. For now, the only thing clients care about is menu preemption on popup. Fixes windows which close on changes to active input closing on their own context menus.
412 lines
13 KiB
C++
412 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/ConnectionFromClient.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;
|
|
}
|
|
|
|
bool MenuManager::is_open(Menu const& 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()
|
|
{
|
|
ConnectionFromClient::for_each_client([&](ConnectionFromClient& 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<KeyEvent const&>(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([&](auto const& 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;
|
|
}
|
|
|
|
if (key_event.key() == Key_Space) {
|
|
auto* hovered_item = m_current_menu->hovered_item();
|
|
if (!hovered_item || !hovered_item->is_enabled())
|
|
return;
|
|
if (!hovered_item->is_checkable())
|
|
return;
|
|
|
|
m_current_menu->open_hovered_item(true);
|
|
}
|
|
|
|
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.in_reverse()) {
|
|
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<ConnectionFromClient>, ConnectionFromClient& 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();
|
|
}
|
|
|
|
Menu* MenuManager::closest_open_ancestor_of(Menu const& other) const
|
|
{
|
|
for (auto& menu : m_open_menu_stack.in_reverse())
|
|
if (menu->is_menu_ancestor_of(other))
|
|
return menu.ptr();
|
|
return nullptr;
|
|
}
|
|
|
|
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()
|
|
{
|
|
if (m_current_menu) {
|
|
auto& wm = WindowManager::the();
|
|
if (auto* window = wm.window_with_active_menu()) {
|
|
window->invalidate_menubar();
|
|
}
|
|
wm.set_window_with_active_menu(nullptr);
|
|
}
|
|
m_current_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;
|
|
}
|
|
|
|
m_current_menu = menu;
|
|
|
|
auto& wm = WindowManager::the();
|
|
if (auto* window = wm.active_input_window()) {
|
|
InputPreemptor preemptor { InputPreemptor::OtherMenu };
|
|
if (window->rect().contains(m_current_menu->unadjusted_position()))
|
|
preemptor = InputPreemptor::ContextMenu;
|
|
else if (!m_current_menu->rect_in_window_menubar().is_null())
|
|
preemptor = InputPreemptor::MenubarMenu;
|
|
wm.notify_input_preempted(*window, preemptor);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
}
|