2020-01-18 05:38:21 -03:00
|
|
|
/*
|
2021-03-26 05:11:44 -03:00
|
|
|
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
2020-01-18 20:13:26 -03:00
|
|
|
* Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com>
|
2020-01-18 05:38:21 -03:00
|
|
|
*
|
2021-04-22 05:24:48 -03:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2020-01-18 05:38:21 -03:00
|
|
|
*/
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
#include "Menu.h"
|
|
|
|
|
#include "Event.h"
|
|
|
|
|
#include "MenuItem.h"
|
|
|
|
|
#include "MenuManager.h"
|
|
|
|
|
#include "Screen.h"
|
|
|
|
|
#include "Window.h"
|
|
|
|
|
#include "WindowManager.h"
|
2021-06-01 16:18:08 -03:00
|
|
|
#include <AK/CharacterTypes.h>
|
2020-02-06 16:03:37 -03:00
|
|
|
#include <LibGfx/Bitmap.h>
|
2020-02-06 08:04:00 -03:00
|
|
|
#include <LibGfx/CharacterBitmap.h>
|
|
|
|
|
#include <LibGfx/Font.h>
|
|
|
|
|
#include <LibGfx/Painter.h>
|
|
|
|
|
#include <LibGfx/StylePainter.h>
|
|
|
|
|
#include <LibGfx/Triangle.h>
|
2020-02-06 16:03:37 -03:00
|
|
|
#include <WindowServer/ClientConnection.h>
|
2019-12-02 05:33:37 -03:00
|
|
|
#include <WindowServer/WindowClientEndpoint.h>
|
2019-02-11 06:47:10 -02:00
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
namespace WindowServer {
|
|
|
|
|
|
2021-04-09 08:48:26 -03:00
|
|
|
u32 find_ampersand_shortcut_character(const StringView& string)
|
2021-04-05 18:03:55 -03:00
|
|
|
{
|
|
|
|
|
Utf8View utf8_view { string };
|
|
|
|
|
for (auto it = utf8_view.begin(); it != utf8_view.end(); ++it) {
|
|
|
|
|
if (*it == '&') {
|
|
|
|
|
++it;
|
|
|
|
|
if (it != utf8_view.end() && *it != '&')
|
|
|
|
|
return *it;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Menu::Menu(ClientConnection* client, int menu_id, String name)
|
2020-02-02 08:34:39 -03:00
|
|
|
: Core::Object(client)
|
2019-08-18 07:05:33 -03:00
|
|
|
, m_client(client)
|
2019-02-14 07:44:47 -02:00
|
|
|
, m_menu_id(menu_id)
|
2019-02-11 21:52:19 -02:00
|
|
|
, m_name(move(name))
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
2021-04-05 18:03:55 -03:00
|
|
|
m_alt_shortcut_character = find_ampersand_shortcut_character(m_name);
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
Menu::~Menu()
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
const Gfx::Font& Menu::font() const
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
2020-12-29 14:25:13 -03:00
|
|
|
return Gfx::FontDatabase::default_font();
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
|
|
|
|
|
2021-05-21 06:30:21 -03:00
|
|
|
static const char* s_checked_bitmap_data = {
|
2019-04-26 16:09:56 -03:00
|
|
|
" "
|
2019-05-10 17:50:42 -03:00
|
|
|
" # "
|
2019-04-26 16:09:56 -03:00
|
|
|
" ## "
|
2019-05-10 17:50:42 -03:00
|
|
|
" ### "
|
|
|
|
|
" ## ### "
|
|
|
|
|
" ##### "
|
|
|
|
|
" ### "
|
|
|
|
|
" # "
|
2019-04-26 16:09:56 -03:00
|
|
|
" "
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-21 06:30:21 -03:00
|
|
|
static const char* s_submenu_arrow_bitmap_data = {
|
2019-08-28 16:11:53 -03:00
|
|
|
" "
|
|
|
|
|
" # "
|
|
|
|
|
" ## "
|
|
|
|
|
" ### "
|
|
|
|
|
" #### "
|
|
|
|
|
" ### "
|
|
|
|
|
" ## "
|
|
|
|
|
" # "
|
|
|
|
|
" "
|
|
|
|
|
};
|
|
|
|
|
|
2020-02-06 07:56:38 -03:00
|
|
|
static Gfx::CharacterBitmap* s_checked_bitmap;
|
2021-05-21 06:30:21 -03:00
|
|
|
static const int s_checked_bitmap_width = 9;
|
|
|
|
|
static const int s_checked_bitmap_height = 9;
|
|
|
|
|
static const int s_submenu_arrow_bitmap_width = 9;
|
|
|
|
|
static const int s_submenu_arrow_bitmap_height = 9;
|
|
|
|
|
static const int s_item_icon_width = 16;
|
|
|
|
|
static const int s_stripe_width = 24;
|
2019-04-26 16:09:56 -03:00
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
int Menu::content_width() const
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
2019-04-29 18:41:48 -03:00
|
|
|
int widest_text = 0;
|
|
|
|
|
int widest_shortcut = 0;
|
2019-02-11 06:47:10 -02:00
|
|
|
for (auto& item : m_items) {
|
2020-02-06 16:03:37 -03:00
|
|
|
if (item.type() != MenuItem::Text)
|
2019-04-29 18:41:48 -03:00
|
|
|
continue;
|
2021-05-20 13:40:48 -03:00
|
|
|
auto& use_font = item.is_default() ? font().bold_variant() : font();
|
2021-04-12 10:24:17 -03:00
|
|
|
int text_width = use_font.width(Gfx::parse_ampersand_string(item.text()));
|
2019-07-24 04:04:57 -03:00
|
|
|
if (!item.shortcut_text().is_empty()) {
|
2020-07-07 15:09:18 -03:00
|
|
|
int shortcut_width = use_font.width(item.shortcut_text());
|
2019-04-29 18:41:48 -03:00
|
|
|
widest_shortcut = max(shortcut_width, widest_shortcut);
|
2019-03-02 06:04:49 -03:00
|
|
|
}
|
2019-04-29 18:41:48 -03:00
|
|
|
widest_text = max(widest_text, text_width);
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
|
|
|
|
|
2019-08-26 15:43:30 -03:00
|
|
|
int widest_item = widest_text + s_stripe_width;
|
2019-04-29 18:41:48 -03:00
|
|
|
if (widest_shortcut)
|
|
|
|
|
widest_item += padding_between_text_and_shortcut() + widest_shortcut;
|
|
|
|
|
|
2021-03-25 19:08:34 -03:00
|
|
|
return max(widest_item, rect_in_window_menubar().width()) + horizontal_padding() + frame_thickness() * 2;
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::redraw()
|
2019-02-11 07:08:37 -02:00
|
|
|
{
|
2019-04-11 21:53:27 -03:00
|
|
|
if (!menu_window())
|
|
|
|
|
return;
|
2019-02-11 07:08:37 -02:00
|
|
|
draw();
|
|
|
|
|
menu_window()->invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-18 17:38:34 -03:00
|
|
|
void Menu::redraw(MenuItem const& menu_item)
|
|
|
|
|
{
|
|
|
|
|
draw(menu_item);
|
|
|
|
|
menu_window()->invalidate(menu_item.rect());
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-27 17:40:51 -03:00
|
|
|
Window& Menu::ensure_menu_window(Gfx::IntPoint const& position)
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
2021-06-27 17:40:51 -03:00
|
|
|
auto& screen = Screen::closest_to_location(position);
|
2020-01-19 17:28:38 -03:00
|
|
|
int width = this->content_width();
|
2019-02-11 06:47:10 -02:00
|
|
|
|
2021-06-27 17:40:51 -03:00
|
|
|
auto calculate_window_rect = [&]() -> Gfx::IntRect {
|
|
|
|
|
int window_height_available = screen.height() - frame_thickness() * 2;
|
|
|
|
|
int max_window_height = (window_height_available / item_height()) * item_height() + frame_thickness() * 2;
|
|
|
|
|
int content_height = m_items.is_empty() ? 0 : (m_items.last().rect().bottom() + 1) + frame_thickness();
|
|
|
|
|
int window_height = min(max_window_height, content_height);
|
|
|
|
|
if (window_height < content_height) {
|
|
|
|
|
m_scrollable = true;
|
|
|
|
|
m_max_scroll_offset = item_count() - window_height / item_height() + 2;
|
|
|
|
|
}
|
|
|
|
|
return { position, { width, window_height } };
|
|
|
|
|
};
|
|
|
|
|
|
2020-06-10 05:57:59 -03:00
|
|
|
Gfx::IntPoint next_item_location(frame_thickness(), frame_thickness());
|
2020-02-11 04:33:38 -03:00
|
|
|
for (auto& item : m_items) {
|
|
|
|
|
int height = 0;
|
|
|
|
|
if (item.type() == MenuItem::Text)
|
|
|
|
|
height = item_height();
|
|
|
|
|
else if (item.type() == MenuItem::Separator)
|
|
|
|
|
height = 8;
|
|
|
|
|
item.set_rect({ next_item_location, { width - frame_thickness() * 2, height } });
|
2021-04-12 15:47:09 -03:00
|
|
|
next_item_location.translate_by(0, height);
|
2020-02-11 04:33:38 -03:00
|
|
|
}
|
2020-01-19 17:28:38 -03:00
|
|
|
|
2021-07-01 20:17:31 -03:00
|
|
|
if (m_menu_window) {
|
|
|
|
|
// We might be on a different screen than previously, so recalculate the
|
|
|
|
|
// menu's rectangle as we have more or less screen available now
|
|
|
|
|
auto new_rect = calculate_window_rect();
|
|
|
|
|
if (new_rect != m_menu_window->rect()) {
|
2021-07-18 17:38:34 -03:00
|
|
|
auto size_changed = new_rect.size() != m_menu_window->rect().size();
|
2021-07-01 20:17:31 -03:00
|
|
|
m_menu_window->set_rect(new_rect);
|
2021-07-18 17:38:34 -03:00
|
|
|
if (size_changed)
|
|
|
|
|
draw();
|
2021-07-01 20:17:31 -03:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
auto window = Window::construct(*this, WindowType::Menu);
|
|
|
|
|
window->set_visible(false);
|
|
|
|
|
window->set_rect(calculate_window_rect());
|
|
|
|
|
m_menu_window = move(window);
|
|
|
|
|
draw();
|
|
|
|
|
}
|
2019-02-11 06:47:10 -02:00
|
|
|
return *m_menu_window;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-09 06:09:58 -03:00
|
|
|
size_t Menu::visible_item_count() const
|
2020-01-19 17:28:38 -03:00
|
|
|
{
|
|
|
|
|
if (!is_scrollable())
|
|
|
|
|
return m_items.size();
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(m_menu_window);
|
2020-01-19 17:28:38 -03:00
|
|
|
// Make space for up/down arrow indicators
|
|
|
|
|
return m_menu_window->height() / item_height() - 2;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-18 17:38:34 -03:00
|
|
|
Gfx::IntRect Menu::stripe_rect()
|
|
|
|
|
{
|
|
|
|
|
return { frame_thickness(), frame_thickness(), s_stripe_width, menu_window()->height() - frame_thickness() * 2 };
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::draw()
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
2020-02-06 16:03:37 -03:00
|
|
|
auto palette = WindowManager::the().palette();
|
|
|
|
|
m_theme_index_at_last_paint = MenuManager::the().theme_index();
|
2019-12-23 16:24:26 -03:00
|
|
|
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(menu_window());
|
|
|
|
|
VERIFY(menu_window()->backing_store());
|
2020-02-06 07:56:38 -03:00
|
|
|
Gfx::Painter painter(*menu_window()->backing_store());
|
2019-02-11 06:47:10 -02:00
|
|
|
|
2020-06-10 05:57:59 -03:00
|
|
|
Gfx::IntRect rect { {}, menu_window()->size() };
|
2021-03-25 17:10:49 -03:00
|
|
|
painter.draw_rect(rect, Color::Black);
|
|
|
|
|
painter.fill_rect(rect.shrunken(2, 2), palette.menu_base());
|
2019-02-11 06:47:10 -02:00
|
|
|
|
2019-04-26 16:09:56 -03:00
|
|
|
if (!s_checked_bitmap)
|
2020-02-06 07:56:38 -03:00
|
|
|
s_checked_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref();
|
2019-04-26 16:09:56 -03:00
|
|
|
|
2019-08-26 13:10:07 -03:00
|
|
|
bool has_checkable_items = false;
|
2019-08-26 13:54:44 -03:00
|
|
|
bool has_items_with_icon = false;
|
|
|
|
|
for (auto& item : m_items) {
|
2019-08-26 13:10:07 -03:00
|
|
|
has_checkable_items = has_checkable_items | item.is_checkable();
|
2019-08-26 13:54:44 -03:00
|
|
|
has_items_with_icon = has_items_with_icon | !!item.icon();
|
|
|
|
|
}
|
2019-08-26 13:10:07 -03:00
|
|
|
|
2021-07-18 17:38:34 -03:00
|
|
|
// Draw the stripe first, which may extend outside of individual items. We can
|
|
|
|
|
// skip this step when painting an individual item since we're drawing all of them
|
|
|
|
|
painter.fill_rect(stripe_rect(), palette.menu_stripe());
|
2020-01-19 17:28:38 -03:00
|
|
|
|
|
|
|
|
if (is_scrollable()) {
|
|
|
|
|
bool can_go_up = m_scroll_offset > 0;
|
|
|
|
|
bool can_go_down = m_scroll_offset < m_max_scroll_offset;
|
2020-06-10 05:57:59 -03:00
|
|
|
Gfx::IntRect up_indicator_rect { frame_thickness(), frame_thickness(), content_width(), item_height() };
|
2020-05-16 16:21:29 -03:00
|
|
|
painter.draw_text(up_indicator_rect, "\xE2\xAC\x86", Gfx::TextAlignment::Center, can_go_up ? palette.menu_base_text() : palette.color(ColorRole::DisabledText));
|
2020-06-10 05:57:59 -03:00
|
|
|
Gfx::IntRect down_indicator_rect { frame_thickness(), menu_window()->height() - item_height() - frame_thickness(), content_width(), item_height() };
|
2020-05-16 16:21:29 -03:00
|
|
|
painter.draw_text(down_indicator_rect, "\xE2\xAC\x87", Gfx::TextAlignment::Center, can_go_down ? palette.menu_base_text() : palette.color(ColorRole::DisabledText));
|
2020-01-19 17:28:38 -03:00
|
|
|
}
|
|
|
|
|
|
2021-07-18 17:38:34 -03:00
|
|
|
int visible_item_count = this->visible_item_count();
|
|
|
|
|
for (int i = 0; i < visible_item_count; ++i)
|
|
|
|
|
draw(m_items.at(m_scroll_offset + i), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Menu::draw(MenuItem const& item, bool is_drawing_all)
|
|
|
|
|
{
|
|
|
|
|
auto palette = WindowManager::the().palette();
|
|
|
|
|
int width = this->content_width();
|
|
|
|
|
Gfx::Painter painter(*menu_window()->backing_store());
|
|
|
|
|
painter.add_clip_rect(item.rect());
|
|
|
|
|
|
|
|
|
|
auto stripe_rect = this->stripe_rect();
|
|
|
|
|
if (!is_drawing_all) {
|
|
|
|
|
// If we're redrawing all of them then we already did this in draw()
|
|
|
|
|
painter.fill_rect(stripe_rect, palette.menu_stripe());
|
|
|
|
|
for (auto& rect : item.rect().shatter(stripe_rect))
|
|
|
|
|
painter.fill_rect(rect, palette.menu_base());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (item.type() == MenuItem::Text) {
|
|
|
|
|
Color text_color = palette.menu_base_text();
|
|
|
|
|
if (&item == hovered_item() && item.is_enabled()) {
|
|
|
|
|
painter.fill_rect(item.rect(), palette.menu_selection());
|
|
|
|
|
painter.draw_rect(item.rect(), palette.menu_selection().darkened());
|
|
|
|
|
text_color = palette.menu_selection_text();
|
|
|
|
|
} else if (!item.is_enabled()) {
|
|
|
|
|
text_color = Color::MidGray;
|
|
|
|
|
}
|
|
|
|
|
Gfx::IntRect text_rect = item.rect().translated(stripe_rect.width() + 6, 0);
|
|
|
|
|
if (item.is_checkable()) {
|
|
|
|
|
if (item.is_exclusive()) {
|
|
|
|
|
Gfx::IntRect radio_rect { item.rect().x() + 5, 0, 12, 12 };
|
|
|
|
|
radio_rect.center_vertically_within(text_rect);
|
|
|
|
|
Gfx::StylePainter::paint_radio_button(painter, radio_rect, palette, item.is_checked(), false);
|
|
|
|
|
} else {
|
|
|
|
|
Gfx::IntRect checkmark_rect { item.rect().x() + 7, 0, s_checked_bitmap_width, s_checked_bitmap_height };
|
|
|
|
|
checkmark_rect.center_vertically_within(text_rect);
|
|
|
|
|
Gfx::IntRect checkbox_rect = checkmark_rect.inflated(4, 4);
|
|
|
|
|
painter.fill_rect(checkbox_rect, palette.base());
|
|
|
|
|
Gfx::StylePainter::paint_frame(painter, checkbox_rect, palette, Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2);
|
|
|
|
|
if (item.is_checked()) {
|
|
|
|
|
painter.draw_bitmap(checkmark_rect.location(), *s_checked_bitmap, palette.button_text());
|
2020-10-25 11:30:37 -03:00
|
|
|
}
|
2019-03-02 06:04:49 -03:00
|
|
|
}
|
2021-07-18 17:38:34 -03:00
|
|
|
} else if (item.icon()) {
|
|
|
|
|
Gfx::IntRect icon_rect { item.rect().x() + 3, 0, s_item_icon_width, s_item_icon_width };
|
|
|
|
|
icon_rect.center_vertically_within(text_rect);
|
|
|
|
|
|
|
|
|
|
if (&item == hovered_item() && item.is_enabled()) {
|
|
|
|
|
auto shadow_color = palette.menu_selection().darkened(0.7f);
|
|
|
|
|
painter.blit_filtered(icon_rect.location().translated(1, 1), *item.icon(), item.icon()->rect(), [&shadow_color](auto) {
|
|
|
|
|
return shadow_color;
|
|
|
|
|
});
|
|
|
|
|
icon_rect.translate_by(-1, -1);
|
2019-08-28 16:11:53 -03:00
|
|
|
}
|
2021-07-18 17:38:34 -03:00
|
|
|
if (item.is_enabled())
|
|
|
|
|
painter.blit(icon_rect.location(), *item.icon(), item.icon()->rect());
|
|
|
|
|
else
|
|
|
|
|
painter.blit_disabled(icon_rect.location(), *item.icon(), item.icon()->rect(), palette);
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
2021-07-18 17:38:34 -03:00
|
|
|
auto& previous_font = painter.font();
|
|
|
|
|
if (item.is_default())
|
|
|
|
|
painter.set_font(previous_font.bold_variant());
|
|
|
|
|
painter.draw_ui_text(text_rect, item.text(), painter.font(), Gfx::TextAlignment::CenterLeft, text_color);
|
|
|
|
|
if (!item.shortcut_text().is_empty()) {
|
|
|
|
|
painter.draw_text(item.rect().translated(-right_padding(), 0), item.shortcut_text(), Gfx::TextAlignment::CenterRight, text_color);
|
|
|
|
|
}
|
|
|
|
|
painter.set_font(previous_font);
|
|
|
|
|
if (item.is_submenu()) {
|
|
|
|
|
static auto& submenu_arrow_bitmap = Gfx::CharacterBitmap::create_from_ascii(s_submenu_arrow_bitmap_data, s_submenu_arrow_bitmap_width, s_submenu_arrow_bitmap_height).leak_ref();
|
|
|
|
|
Gfx::IntRect submenu_arrow_rect {
|
|
|
|
|
item.rect().right() - s_submenu_arrow_bitmap_width - 2,
|
|
|
|
|
0,
|
|
|
|
|
s_submenu_arrow_bitmap_width,
|
|
|
|
|
s_submenu_arrow_bitmap_height
|
|
|
|
|
};
|
|
|
|
|
submenu_arrow_rect.center_vertically_within(item.rect());
|
|
|
|
|
painter.draw_bitmap(submenu_arrow_rect.location(), submenu_arrow_bitmap, text_color);
|
|
|
|
|
}
|
|
|
|
|
} else if (item.type() == MenuItem::Separator) {
|
|
|
|
|
Gfx::IntPoint p1(item.rect().translated(stripe_rect.width() + 4, 0).x(), item.rect().center().y() - 1);
|
|
|
|
|
Gfx::IntPoint p2(width - 7, item.rect().center().y() - 1);
|
|
|
|
|
painter.draw_line(p1, p2, palette.threed_shadow1());
|
|
|
|
|
painter.draw_line(p1.translated(0, 1), p2.translated(0, 1), palette.threed_highlight());
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
MenuItem* Menu::hovered_item() const
|
2020-01-18 04:56:51 -03:00
|
|
|
{
|
|
|
|
|
if (m_hovered_item_index == -1)
|
|
|
|
|
return nullptr;
|
2020-02-06 16:03:37 -03:00
|
|
|
return const_cast<MenuItem*>(&item(m_hovered_item_index));
|
2020-01-18 04:56:51 -03:00
|
|
|
}
|
|
|
|
|
|
2020-07-15 13:13:13 -03:00
|
|
|
void Menu::update_for_new_hovered_item(bool make_input)
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
{
|
2021-04-17 13:16:54 -03:00
|
|
|
if (auto* hovered_item = this->hovered_item()) {
|
|
|
|
|
if (hovered_item->is_submenu()) {
|
|
|
|
|
VERIFY(menu_window());
|
|
|
|
|
MenuManager::the().close_everyone_not_in_lineage(*hovered_item->submenu());
|
|
|
|
|
hovered_item->submenu()->do_popup(hovered_item->rect().top_right().translated(menu_window()->rect().location()), make_input, true);
|
|
|
|
|
} else {
|
|
|
|
|
MenuManager::the().close_everyone_not_in_lineage(*this);
|
2021-06-27 17:40:51 -03:00
|
|
|
VERIFY(menu_window());
|
2021-04-17 13:16:54 -03:00
|
|
|
set_visible(true);
|
|
|
|
|
}
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-26 10:45:15 -03:00
|
|
|
void Menu::open_hovered_item(bool leave_menu_open)
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
{
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(menu_window());
|
|
|
|
|
VERIFY(menu_window()->is_visible());
|
2020-01-18 04:56:51 -03:00
|
|
|
if (!hovered_item())
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
return;
|
2021-08-18 15:29:08 -03:00
|
|
|
if (hovered_item()->is_enabled()) {
|
2021-03-26 10:45:15 -03:00
|
|
|
did_activate(*hovered_item(), leave_menu_open);
|
2021-08-18 15:29:08 -03:00
|
|
|
if (!leave_menu_open)
|
|
|
|
|
clear_hovered_item();
|
|
|
|
|
}
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
}
|
|
|
|
|
|
2020-04-24 12:26:16 -03:00
|
|
|
void Menu::descend_into_submenu_at_hovered_item()
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
{
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(hovered_item());
|
2020-01-18 04:56:51 -03:00
|
|
|
auto submenu = hovered_item()->submenu();
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(submenu);
|
2021-07-07 20:24:06 -03:00
|
|
|
MenuManager::the().open_menu(*submenu, true);
|
2021-04-15 12:42:23 -03:00
|
|
|
submenu->set_hovered_index(0);
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(submenu->hovered_item()->type() != MenuItem::Separator);
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::handle_mouse_move_event(const MouseEvent& mouse_event)
|
2019-02-11 06:47:10 -02:00
|
|
|
{
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(menu_window());
|
2020-05-09 00:57:22 -03:00
|
|
|
MenuManager::the().set_current_menu(this);
|
2020-01-19 17:28:38 -03:00
|
|
|
if (hovered_item() && hovered_item()->is_submenu()) {
|
2020-01-18 20:13:26 -03:00
|
|
|
|
2020-01-19 17:28:38 -03:00
|
|
|
auto item = *hovered_item();
|
2020-06-10 05:57:59 -03:00
|
|
|
auto submenu_top_left = item.rect().location() + Gfx::IntPoint { item.rect().width(), 0 };
|
|
|
|
|
auto submenu_bottom_left = submenu_top_left + Gfx::IntPoint { 0, item.submenu()->menu_window()->height() };
|
2020-01-18 20:13:26 -03:00
|
|
|
|
2020-02-06 07:56:38 -03:00
|
|
|
auto safe_hover_triangle = Gfx::Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left };
|
2020-01-19 17:28:38 -03:00
|
|
|
m_last_position_in_hover = mouse_event.position();
|
2020-01-18 20:13:26 -03:00
|
|
|
|
2020-01-19 17:28:38 -03:00
|
|
|
// Don't update the hovered item if mouse is moving towards a submenu
|
|
|
|
|
if (safe_hover_triangle.contains(mouse_event.position()))
|
2019-02-11 07:08:37 -02:00
|
|
|
return;
|
2020-01-19 17:28:38 -03:00
|
|
|
}
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
2020-01-19 17:28:38 -03:00
|
|
|
int index = item_index_at(mouse_event.position());
|
2021-04-15 12:42:23 -03:00
|
|
|
set_hovered_index(index);
|
2020-01-19 17:28:38 -03:00
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::event(Core::Event& event)
|
2020-01-19 17:28:38 -03:00
|
|
|
{
|
2020-02-06 16:03:37 -03:00
|
|
|
if (event.type() == Event::MouseMove) {
|
|
|
|
|
handle_mouse_move_event(static_cast<const MouseEvent&>(event));
|
2019-02-11 07:55:02 -02:00
|
|
|
return;
|
2019-02-11 07:08:37 -02:00
|
|
|
}
|
2019-02-11 07:55:02 -02:00
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
if (event.type() == Event::MouseUp) {
|
2021-03-26 10:45:15 -03:00
|
|
|
open_hovered_item(static_cast<MouseEvent&>(event).modifiers() & KeyModifier::Mod_Ctrl);
|
2019-02-11 07:55:02 -02:00
|
|
|
return;
|
|
|
|
|
}
|
2019-04-21 20:15:47 -03:00
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
if (event.type() == Event::MouseWheel && is_scrollable()) {
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(menu_window());
|
2020-02-06 16:03:37 -03:00
|
|
|
auto& mouse_event = static_cast<const MouseEvent&>(event);
|
2021-07-18 17:38:34 -03:00
|
|
|
auto previous_scroll_offset = m_scroll_offset;
|
2020-01-19 17:28:38 -03:00
|
|
|
m_scroll_offset += mouse_event.wheel_delta();
|
2020-01-20 06:03:57 -03:00
|
|
|
m_scroll_offset = clamp(m_scroll_offset, 0, m_max_scroll_offset);
|
2021-07-18 17:38:34 -03:00
|
|
|
if (m_scroll_offset != previous_scroll_offset)
|
|
|
|
|
redraw();
|
2020-01-20 06:03:57 -03:00
|
|
|
|
|
|
|
|
int index = item_index_at(mouse_event.position());
|
2021-04-15 12:42:23 -03:00
|
|
|
set_hovered_index(index);
|
2020-01-19 17:28:38 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
if (event.type() == Event::KeyDown) {
|
|
|
|
|
auto key = static_cast<KeyEvent&>(event).key();
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
|
|
|
|
if (!(key == Key_Up || key == Key_Down || key == Key_Left || key == Key_Right || key == Key_Return))
|
|
|
|
|
return;
|
|
|
|
|
|
2021-02-23 16:42:32 -03:00
|
|
|
VERIFY(menu_window());
|
|
|
|
|
VERIFY(menu_window()->is_visible());
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
2020-01-18 04:56:51 -03:00
|
|
|
if (!hovered_item()) {
|
2021-05-08 20:54:13 -03:00
|
|
|
if (key == Key_Up) {
|
|
|
|
|
// Default to the last enabled, non-separator item on key press if one has not been selected yet
|
|
|
|
|
for (auto i = static_cast<int>(m_items.size()) - 1; i >= 0; i--) {
|
|
|
|
|
auto& item = m_items.at(i);
|
|
|
|
|
if (item.type() != MenuItem::Separator && item.is_enabled()) {
|
|
|
|
|
set_hovered_index(i, key == Key_Right);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Default to the first enabled, non-separator item on key press if one has not been selected yet
|
|
|
|
|
int counter = 0;
|
|
|
|
|
for (const auto& item : m_items) {
|
|
|
|
|
if (item.type() != MenuItem::Separator && item.is_enabled()) {
|
|
|
|
|
set_hovered_index(counter, key == Key_Right);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
counter++;
|
2021-02-13 10:40:20 -03:00
|
|
|
}
|
|
|
|
|
}
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key == Key_Up) {
|
2021-04-15 12:42:23 -03:00
|
|
|
VERIFY(item(0).type() != MenuItem::Separator);
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
2020-01-19 17:28:38 -03:00
|
|
|
if (is_scrollable() && m_hovered_item_index == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
2020-04-24 12:24:09 -03:00
|
|
|
auto original_index = m_hovered_item_index;
|
2021-04-15 12:42:23 -03:00
|
|
|
auto new_index = original_index;
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
do {
|
2021-04-15 12:42:23 -03:00
|
|
|
if (new_index == 0)
|
|
|
|
|
new_index = m_items.size() - 1;
|
2020-01-30 19:06:01 -03:00
|
|
|
else
|
2021-04-15 12:42:23 -03:00
|
|
|
--new_index;
|
|
|
|
|
if (new_index == original_index)
|
2020-04-24 12:24:09 -03:00
|
|
|
return;
|
2021-04-15 12:42:23 -03:00
|
|
|
} while (item(new_index).type() == MenuItem::Separator || !item(new_index).is_enabled());
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
2021-04-15 12:42:23 -03:00
|
|
|
VERIFY(new_index >= 0);
|
|
|
|
|
VERIFY(new_index <= static_cast<int>(m_items.size()) - 1);
|
2020-02-11 06:14:35 -03:00
|
|
|
|
2021-04-15 12:42:23 -03:00
|
|
|
if (is_scrollable() && new_index < m_scroll_offset)
|
2020-01-19 17:28:38 -03:00
|
|
|
--m_scroll_offset;
|
|
|
|
|
|
2021-04-15 12:42:23 -03:00
|
|
|
set_hovered_index(new_index);
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key == Key_Down) {
|
2021-04-15 12:42:23 -03:00
|
|
|
VERIFY(item(0).type() != MenuItem::Separator);
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
2020-02-25 10:49:47 -03:00
|
|
|
if (is_scrollable() && m_hovered_item_index == static_cast<int>(m_items.size()) - 1)
|
2020-01-19 17:28:38 -03:00
|
|
|
return;
|
|
|
|
|
|
2020-04-24 12:24:09 -03:00
|
|
|
auto original_index = m_hovered_item_index;
|
2021-04-15 12:42:23 -03:00
|
|
|
auto new_index = original_index;
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
do {
|
2021-04-15 12:42:23 -03:00
|
|
|
if (new_index == static_cast<int>(m_items.size()) - 1)
|
|
|
|
|
new_index = 0;
|
2020-01-30 19:06:01 -03:00
|
|
|
else
|
2021-04-15 12:42:23 -03:00
|
|
|
++new_index;
|
|
|
|
|
if (new_index == original_index)
|
2020-04-24 12:24:09 -03:00
|
|
|
return;
|
2021-04-15 12:42:23 -03:00
|
|
|
} while (item(new_index).type() == MenuItem::Separator || !item(new_index).is_enabled());
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
|
2021-04-15 12:42:23 -03:00
|
|
|
VERIFY(new_index >= 0);
|
|
|
|
|
VERIFY(new_index <= static_cast<int>(m_items.size()) - 1);
|
2020-02-11 06:14:35 -03:00
|
|
|
|
2021-05-09 06:09:58 -03:00
|
|
|
if (is_scrollable() && new_index >= (m_scroll_offset + static_cast<int>(visible_item_count())))
|
2020-01-19 17:28:38 -03:00
|
|
|
++m_scroll_offset;
|
|
|
|
|
|
2021-04-15 12:42:23 -03:00
|
|
|
set_hovered_index(new_index);
|
WSMenu: Support menu navigation through key presses
Add event handling for key presses for navigating a menu. The currently
hovered menu item is tracked through an index which is either
incremented or decremented on up or down arrow key presses, changing the
hovered item.
Whenever there is a mouse move event, we ensure that the current index
matches the currently hovered item so that the mouse and keyboard do not
get out of sync.
If the right key is pressed, and we are on a submenu menu item, we
'enter' that submenu. While we are currently in a submenu, we forward
all keypress events to that submenu for handling. This allows us to
traverse the heirachy of a menu. While in a submenu, if the left key is
pressed, we leave that submenu and start handling the keypresses
ourselves again.
There is currently a small issue where the mouse hover and key hover can
get out of sync. The mouse can be traversing a submenu, but the parent
menu has no idea that the mouse has 'entered' a submenu, so will handle
the key presses itself, instead of forwarding them to the submenu. One
potential fix for this is for a menu to tell its menu parent that the
submenu is being traversed.
2020-01-02 22:11:49 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-02 08:34:39 -03:00
|
|
|
Core::Object::event(event);
|
2019-02-11 07:55:02 -02:00
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::clear_hovered_item()
|
2019-02-11 08:06:41 -02:00
|
|
|
{
|
2021-04-17 13:16:54 -03:00
|
|
|
set_hovered_index(-1);
|
2019-02-11 08:06:41 -02:00
|
|
|
}
|
|
|
|
|
|
2021-03-27 17:34:38 -03:00
|
|
|
void Menu::start_activation_animation(MenuItem& item)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(menu_window());
|
|
|
|
|
VERIFY(menu_window()->backing_store());
|
|
|
|
|
auto window = Window::construct(*this, WindowType::Menu);
|
|
|
|
|
window->set_frameless(true);
|
|
|
|
|
window->set_hit_testing_enabled(false);
|
|
|
|
|
window->set_opacity(0.8f); // start out transparent so we don't have to recompute occlusions
|
|
|
|
|
window->set_rect(item.rect().translated(m_menu_window->rect().location()));
|
|
|
|
|
window->set_event_filter([](Core::Event&) {
|
|
|
|
|
// ignore all events
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
VERIFY(window->backing_store());
|
|
|
|
|
Gfx::Painter painter(*window->backing_store());
|
|
|
|
|
painter.blit({}, *menu_window()->backing_store(), item.rect(), 1.0f, false);
|
|
|
|
|
window->invalidate();
|
|
|
|
|
|
|
|
|
|
struct AnimationInfo {
|
|
|
|
|
RefPtr<Core::Timer> timer;
|
|
|
|
|
RefPtr<Window> window;
|
|
|
|
|
u8 step { 8 }; // Must be even number!
|
|
|
|
|
|
|
|
|
|
AnimationInfo(NonnullRefPtr<Window>&& window)
|
|
|
|
|
: window(move(window))
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
auto animation = adopt_own(*new AnimationInfo(move(window)));
|
|
|
|
|
auto& timer = animation->timer;
|
2021-07-05 13:13:42 -03:00
|
|
|
timer = Core::Timer::create_repeating(50, [animation = animation.ptr(), animation_ref = move(animation)] {
|
2021-03-27 17:34:38 -03:00
|
|
|
VERIFY(animation->step % 2 == 0);
|
|
|
|
|
animation->step -= 2;
|
|
|
|
|
if (animation->step == 0) {
|
|
|
|
|
animation->window->set_visible(false);
|
|
|
|
|
animation->timer->stop();
|
|
|
|
|
animation->timer = nullptr; // break circular reference
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float opacity = (float)animation->step / 10.0f;
|
|
|
|
|
animation->window->set_opacity(opacity);
|
|
|
|
|
});
|
|
|
|
|
timer->start();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-26 10:45:15 -03:00
|
|
|
void Menu::did_activate(MenuItem& item, bool leave_menu_open)
|
2019-02-11 07:55:02 -02:00
|
|
|
{
|
2020-02-06 16:03:37 -03:00
|
|
|
if (item.type() == MenuItem::Type::Separator)
|
2019-11-11 08:56:13 -03:00
|
|
|
return;
|
|
|
|
|
|
2021-03-27 17:34:38 -03:00
|
|
|
if (!leave_menu_open)
|
|
|
|
|
start_activation_animation(item);
|
|
|
|
|
|
2019-02-11 07:55:02 -02:00
|
|
|
if (on_item_activation)
|
|
|
|
|
on_item_activation(item);
|
2019-02-12 07:08:35 -02:00
|
|
|
|
2021-03-26 10:45:15 -03:00
|
|
|
if (!leave_menu_open)
|
|
|
|
|
MenuManager::the().close_everyone();
|
2019-02-12 07:08:35 -02:00
|
|
|
|
2019-02-17 05:06:47 -03:00
|
|
|
if (m_client)
|
2021-05-03 08:33:59 -03:00
|
|
|
m_client->async_menu_item_activated(m_menu_id, item.identifier());
|
2019-02-11 07:08:37 -02:00
|
|
|
}
|
|
|
|
|
|
2020-07-07 15:09:18 -03:00
|
|
|
bool Menu::activate_default()
|
|
|
|
|
{
|
|
|
|
|
for (auto& item : m_items) {
|
|
|
|
|
if (item.type() == MenuItem::Type::Separator)
|
|
|
|
|
continue;
|
|
|
|
|
if (item.is_enabled() && item.is_default()) {
|
2021-03-26 10:45:15 -03:00
|
|
|
did_activate(item, false);
|
2020-07-07 15:09:18 -03:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 18:14:37 -03:00
|
|
|
MenuItem* Menu::item_with_identifier(unsigned identifier)
|
2019-04-11 21:53:27 -03:00
|
|
|
{
|
|
|
|
|
for (auto& item : m_items) {
|
2020-10-02 18:14:37 -03:00
|
|
|
if (item.identifier() == identifier)
|
2019-07-24 04:04:57 -03:00
|
|
|
return &item;
|
2019-04-11 21:53:27 -03:00
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-10 05:57:59 -03:00
|
|
|
int Menu::item_index_at(const Gfx::IntPoint& position)
|
2019-02-11 07:08:37 -02:00
|
|
|
{
|
2020-01-18 04:56:51 -03:00
|
|
|
int i = 0;
|
2019-02-11 07:08:37 -02:00
|
|
|
for (auto& item : m_items) {
|
2021-07-07 15:27:46 -03:00
|
|
|
if (item.rect().contains(position))
|
2020-01-18 04:56:51 -03:00
|
|
|
return i;
|
|
|
|
|
++i;
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
2020-01-18 04:56:51 -03:00
|
|
|
return -1;
|
2019-02-11 06:47:10 -02:00
|
|
|
}
|
2019-02-11 10:59:26 -02:00
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::close()
|
2019-02-11 10:59:26 -02:00
|
|
|
{
|
2020-02-06 16:03:37 -03:00
|
|
|
MenuManager::the().close_menu_and_descendants(*this);
|
2019-04-12 12:10:30 -03:00
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
void Menu::redraw_if_theme_changed()
|
2019-12-23 16:24:26 -03:00
|
|
|
{
|
2020-02-06 16:03:37 -03:00
|
|
|
if (m_theme_index_at_last_paint != MenuManager::the().theme_index())
|
2019-12-23 16:24:26 -03:00
|
|
|
redraw();
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-10 05:57:59 -03:00
|
|
|
void Menu::popup(const Gfx::IntPoint& position)
|
2020-07-15 13:13:13 -03:00
|
|
|
{
|
|
|
|
|
do_popup(position, true);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-26 05:22:31 -03:00
|
|
|
void Menu::do_popup(const Gfx::IntPoint& position, bool make_input, bool as_submenu)
|
2019-04-12 12:10:30 -03:00
|
|
|
{
|
2020-04-07 14:59:48 -03:00
|
|
|
if (is_empty()) {
|
2021-01-09 14:51:44 -03:00
|
|
|
dbgln("Menu: Empty menu popup");
|
2020-04-07 14:59:48 -03:00
|
|
|
return;
|
|
|
|
|
}
|
2019-05-15 20:06:21 -03:00
|
|
|
|
2021-06-27 17:40:51 -03:00
|
|
|
auto& screen = Screen::closest_to_location(position);
|
|
|
|
|
auto& window = ensure_menu_window(position);
|
2019-12-23 16:24:26 -03:00
|
|
|
redraw_if_theme_changed();
|
|
|
|
|
|
2019-05-15 20:06:21 -03:00
|
|
|
const int margin = 30;
|
2020-06-10 05:57:59 -03:00
|
|
|
Gfx::IntPoint adjusted_pos = position;
|
2020-01-19 17:28:38 -03:00
|
|
|
|
2021-06-27 17:40:51 -03:00
|
|
|
if (adjusted_pos.x() + window.width() > screen.rect().right() - margin) {
|
2020-01-19 17:28:38 -03:00
|
|
|
adjusted_pos = adjusted_pos.translated(-window.width(), 0);
|
2021-06-02 19:50:40 -03:00
|
|
|
} else {
|
|
|
|
|
adjusted_pos.set_x(adjusted_pos.x() + 1);
|
2019-05-15 20:06:21 -03:00
|
|
|
}
|
2021-06-27 17:40:51 -03:00
|
|
|
if (adjusted_pos.y() + window.height() > screen.rect().bottom() - margin) {
|
2021-05-09 07:27:41 -03:00
|
|
|
adjusted_pos = adjusted_pos.translated(0, -min(window.height(), adjusted_pos.y()));
|
2021-03-26 05:22:31 -03:00
|
|
|
if (as_submenu)
|
|
|
|
|
adjusted_pos = adjusted_pos.translated(0, item_height());
|
2020-01-19 17:28:38 -03:00
|
|
|
}
|
|
|
|
|
|
2019-05-15 20:06:21 -03:00
|
|
|
window.move_to(adjusted_pos);
|
2021-04-05 09:32:34 -03:00
|
|
|
set_visible(true);
|
2021-03-26 04:54:33 -03:00
|
|
|
MenuManager::the().open_menu(*this, make_input);
|
2020-05-09 11:40:13 -03:00
|
|
|
WindowManager::the().did_popup_a_menu({});
|
2019-04-12 12:10:30 -03:00
|
|
|
}
|
2019-11-11 06:43:03 -03:00
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
bool Menu::is_menu_ancestor_of(const Menu& other) const
|
2019-11-11 06:43:03 -03:00
|
|
|
{
|
|
|
|
|
for (auto& item : m_items) {
|
|
|
|
|
if (!item.is_submenu())
|
|
|
|
|
continue;
|
2020-05-18 07:09:05 -03:00
|
|
|
auto& submenu = *item.submenu();
|
2019-11-11 06:43:03 -03:00
|
|
|
if (&submenu == &other)
|
|
|
|
|
return true;
|
|
|
|
|
if (submenu.is_menu_ancestor_of(other))
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2020-02-06 16:03:37 -03:00
|
|
|
|
2021-04-05 09:32:34 -03:00
|
|
|
void Menu::set_visible(bool visible)
|
|
|
|
|
{
|
|
|
|
|
if (!menu_window())
|
|
|
|
|
return;
|
|
|
|
|
if (visible == menu_window()->is_visible())
|
|
|
|
|
return;
|
|
|
|
|
menu_window()->set_visible(visible);
|
|
|
|
|
if (m_client)
|
2021-05-03 08:33:59 -03:00
|
|
|
m_client->async_menu_visibility_did_change(m_menu_id, visible);
|
2021-04-05 09:32:34 -03:00
|
|
|
}
|
|
|
|
|
|
2021-04-09 11:33:37 -03:00
|
|
|
void Menu::add_item(NonnullOwnPtr<MenuItem> item)
|
|
|
|
|
{
|
|
|
|
|
if (auto alt_shortcut = find_ampersand_shortcut_character(item->text())) {
|
2021-06-01 16:18:08 -03:00
|
|
|
m_alt_shortcut_character_to_item_indices.ensure(to_ascii_lowercase(alt_shortcut)).append(m_items.size());
|
2021-04-09 11:33:37 -03:00
|
|
|
}
|
|
|
|
|
m_items.append(move(item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Vector<size_t>* Menu::items_with_alt_shortcut(u32 alt_shortcut) const
|
|
|
|
|
{
|
2021-06-01 16:18:08 -03:00
|
|
|
auto it = m_alt_shortcut_character_to_item_indices.find(to_ascii_lowercase(alt_shortcut));
|
2021-04-29 17:23:52 -03:00
|
|
|
if (it == m_alt_shortcut_character_to_item_indices.end())
|
2021-04-09 11:33:37 -03:00
|
|
|
return nullptr;
|
|
|
|
|
return &it->value;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-17 13:16:54 -03:00
|
|
|
void Menu::set_hovered_index(int index, bool make_input)
|
|
|
|
|
{
|
|
|
|
|
if (m_hovered_item_index == index)
|
|
|
|
|
return;
|
2021-07-18 17:38:34 -03:00
|
|
|
auto* old_hovered_item = hovered_item();
|
|
|
|
|
if (old_hovered_item) {
|
2021-07-07 15:27:46 -03:00
|
|
|
if (client() && old_hovered_item->type() != MenuItem::Type::Separator)
|
2021-05-03 08:33:59 -03:00
|
|
|
client()->async_menu_item_left(m_menu_id, old_hovered_item->identifier());
|
2021-04-17 13:16:54 -03:00
|
|
|
}
|
|
|
|
|
m_hovered_item_index = index;
|
|
|
|
|
update_for_new_hovered_item(make_input);
|
|
|
|
|
if (auto* new_hovered_item = hovered_item()) {
|
2021-07-07 15:27:46 -03:00
|
|
|
if (client() && new_hovered_item->type() != MenuItem::Type::Separator)
|
2021-05-03 08:33:59 -03:00
|
|
|
client()->async_menu_item_entered(m_menu_id, new_hovered_item->identifier());
|
2021-07-18 17:38:34 -03:00
|
|
|
redraw(*new_hovered_item);
|
2021-04-17 13:16:54 -03:00
|
|
|
}
|
2021-07-18 17:38:34 -03:00
|
|
|
if (old_hovered_item)
|
|
|
|
|
redraw(*old_hovered_item);
|
2021-04-17 13:16:54 -03:00
|
|
|
}
|
|
|
|
|
|
2021-05-17 17:01:05 -03:00
|
|
|
bool Menu::is_open() const
|
|
|
|
|
{
|
|
|
|
|
return MenuManager::the().is_open(*this);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-06 16:03:37 -03:00
|
|
|
}
|