1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 01:07:36 +00:00

WindowServer+LibGUI: Add support for nested menus

It's now possible to add a GMenu as a submenu of another GMenu.
Simply use the GMenu::add_submenu(NonnullOwnPtr<GMenu>) API :^)

The WindowServer now keeps track of a stack of open menus rather than
just one "current menu". This code needs a bit more work, but the basic
functionality is now here!
This commit is contained in:
Andreas Kling 2019-08-28 21:11:53 +02:00
parent d3ebd8897f
commit 63e6b09816
16 changed files with 177 additions and 12 deletions

View file

@ -263,6 +263,7 @@ struct WSAPI_ClientMessage {
struct {
int menubar_id;
int menu_id;
int submenu_id;
int icon_buffer_id;
unsigned identifier;
char shortcut_text[32];

View file

@ -159,7 +159,7 @@ bool WSClientConnection::handle_message(const WSAPI_ClientMessage& message, cons
did_misbehave();
return false;
}
CEventLoop::current().post_event(*this, make<WSAPIAddMenuItemRequest>(client_id(), message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length), String(message.menu.shortcut_text, message.menu.shortcut_text_length), message.menu.enabled, message.menu.checkable, message.menu.checked, message.menu.icon_buffer_id));
CEventLoop::current().post_event(*this, make<WSAPIAddMenuItemRequest>(client_id(), message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length), String(message.menu.shortcut_text, message.menu.shortcut_text_length), message.menu.enabled, message.menu.checkable, message.menu.checked, message.menu.icon_buffer_id, message.menu.submenu_id));
break;
case WSAPI_ClientMessage::Type::UpdateMenuItem:
if (message.text_length > (int)sizeof(message.text)) {
@ -434,6 +434,7 @@ void WSClientConnection::handle_request(const WSAPIAddMenuItemRequest& request)
auto shared_icon = GraphicsBitmap::create_with_shared_buffer(GraphicsBitmap::Format::RGBA32, icon_buffer.release_nonnull(), { 16, 16 });
menu_item->set_icon(shared_icon);
}
menu_item->set_submenu_id(request.submenu_id());
menu.add_item(move(menu_item));
WSAPI_ServerMessage response;
response.type = WSAPI_ServerMessage::Type::DidAddMenuItem;

View file

@ -37,6 +37,12 @@ public:
void notify_about_new_screen_rect(const Rect&);
void post_paint_message(WSWindow&);
WSMenu* find_menu_by_id(int menu_id)
{
// FIXME: Remove this const_cast when Optional knows how to vend a non-const fallback value somehow.
return const_cast<WSMenu*>(m_menus.get(menu_id).value_or(nullptr));
}
private:
virtual void event(CEvent&) override;

View file

@ -303,7 +303,7 @@ private:
class WSAPIAddMenuItemRequest : public WSAPIClientRequest {
public:
WSAPIAddMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked, int icon_buffer_id)
WSAPIAddMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked, int icon_buffer_id, int submenu_id)
: WSAPIClientRequest(WSEvent::APIAddMenuItemRequest, client_id)
, m_menu_id(menu_id)
, m_identifier(identifier)
@ -313,6 +313,7 @@ public:
, m_checkable(checkable)
, m_checked(checked)
, m_icon_buffer_id(icon_buffer_id)
, m_submenu_id(submenu_id)
{
}
@ -324,6 +325,7 @@ public:
bool is_checkable() const { return m_checkable; }
bool is_checked() const { return m_checked; }
int icon_buffer_id() const { return m_icon_buffer_id; }
int submenu_id() const { return m_submenu_id; }
private:
int m_menu_id { 0 };
@ -334,6 +336,7 @@ private:
bool m_checkable;
bool m_checked;
int m_icon_buffer_id { 0 };
int m_submenu_id { 0 };
};
class WSAPIUpdateMenuItemRequest : public WSAPIClientRequest {

View file

@ -2,6 +2,7 @@
#include "WSEvent.h"
#include "WSEventLoop.h"
#include "WSMenuItem.h"
#include "WSMenuManager.h"
#include "WSScreen.h"
#include "WSWindow.h"
#include "WSWindowManager.h"
@ -42,9 +43,23 @@ static const char* s_checked_bitmap_data = {
" "
};
static const char* s_submenu_arrow_bitmap_data = {
" "
" # "
" ## "
" ### "
" #### "
" ### "
" ## "
" # "
" "
};
static CharacterBitmap* s_checked_bitmap;
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_checkbox_or_icon_padding = 6;
static const int s_stripe_width = 23;
@ -163,6 +178,17 @@ void WSMenu::draw()
if (!item.shortcut_text().is_empty()) {
painter.draw_text(item.rect().translated(-right_padding(), 0), item.shortcut_text(), TextAlignment::CenterRight, text_color);
}
if (item.is_submenu()) {
static auto& submenu_arrow_bitmap = CharacterBitmap::create_from_ascii(s_submenu_arrow_bitmap_data, s_submenu_arrow_bitmap_width, s_submenu_arrow_bitmap_height).leak_ref();
Rect 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, Color::Black);
}
} else if (item.type() == WSMenuItem::Separator) {
Point p1(item.rect().translated(stripe_rect.width() + 4, 0).x(), item.rect().center().y() - 1);
Point p2(width - 7, item.rect().center().y() - 1);
@ -180,6 +206,23 @@ void WSMenu::event(CEvent& event)
if (!item || m_hovered_item == item)
return;
m_hovered_item = item;
if (m_hovered_item->is_submenu()) {
m_hovered_item->submenu()->popup(m_hovered_item->rect().top_right().translated(menu_window()->rect().location()), true);
} else {
bool close_remaining_menus = false;
for (auto& open_menu : WSWindowManager::the().menu_manager().open_menu_stack()) {
if (!open_menu)
continue;
if (close_remaining_menus) {
open_menu->menu_window()->set_visible(false);
continue;
}
if (open_menu == this) {
close_remaining_menus = true;
continue;
}
}
}
redraw();
return;
}
@ -246,7 +289,7 @@ void WSMenu::close()
menu_window()->set_visible(false);
}
void WSMenu::popup(const Point& position)
void WSMenu::popup(const Point& position, bool is_submenu)
{
ASSERT(!is_empty());
@ -262,5 +305,5 @@ void WSMenu::popup(const Point& position)
window.move_to(adjusted_pos);
window.set_visible(true);
WSWindowManager::the().set_current_menu(this);
WSWindowManager::the().set_current_menu(this, is_submenu);
}

View file

@ -73,7 +73,7 @@ public:
void close();
void popup(const Point&);
void popup(const Point&, bool is_submenu = false);
private:
virtual void event(CEvent&) override;

View file

@ -1,4 +1,5 @@
#include "WSMenuItem.h"
#include "WSClientConnection.h"
#include "WSMenu.h"
#include <LibDraw/GraphicsBitmap.h>
@ -40,3 +41,10 @@ void WSMenuItem::set_checked(bool checked)
m_checked = checked;
m_menu.redraw();
}
WSMenu* WSMenuItem::submenu()
{
ASSERT(is_submenu());
ASSERT(m_menu.client());
return m_menu.client()->find_menu_by_id(m_submenu_id);
}

View file

@ -44,6 +44,12 @@ public:
const GraphicsBitmap* icon() const { return m_icon; }
void set_icon(const GraphicsBitmap* icon) { m_icon = icon; }
bool is_submenu() const { return m_submenu_id != -1; }
int submenu_id() const { return m_submenu_id; }
void set_submenu_id(int submenu_id) { m_submenu_id = submenu_id; }
WSMenu* submenu();
private:
WSMenu& m_menu;
Type m_type { None };
@ -55,4 +61,5 @@ private:
String m_shortcut_text;
Rect m_rect;
RefPtr<GraphicsBitmap> m_icon;
int m_submenu_id { -1 };
};

View file

@ -31,6 +31,15 @@ void WSMenuManager::setup()
m_window->set_rect(WSWindowManager::the().menubar_rect());
}
bool WSMenuManager::is_open(const WSMenu& menu) const
{
for (int i = 0; i < m_open_menu_stack.size(); ++i) {
if (&menu == m_open_menu_stack[i].ptr())
return true;
}
return false;
}
void WSMenuManager::draw()
{
auto& wm = WSWindowManager::the();
@ -43,7 +52,7 @@ void WSMenuManager::draw()
int index = 0;
wm.for_each_active_menubar_menu([&](WSMenu& menu) {
Color text_color = Color::Black;
if (&menu == wm.current_menu()) {
if (is_open(menu)) {
painter.fill_rect(menu.rect_in_menubar(), Color::from_rgb(0xad714f));
painter.draw_rect(menu.rect_in_menubar(), Color::from_rgb(0x793016));
text_color = Color::White;
@ -124,7 +133,9 @@ void WSMenuManager::event(CEvent& event)
void WSMenuManager::handle_menu_mouse_event(WSMenu& menu, const WSMouseEvent& event)
{
auto& wm = WSWindowManager::the();
bool is_hover_with_any_menu_open = event.type() == WSMouseEvent::MouseMove && wm.current_menu() && (wm.current_menu()->menubar() || wm.current_menu() == wm.system_menu());
bool is_hover_with_any_menu_open = event.type() == WSMouseEvent::MouseMove
&& !m_open_menu_stack.is_empty()
&& (m_open_menu_stack.first()->menubar() || m_open_menu_stack.first() == wm.system_menu());
bool is_mousedown_with_left_button = event.type() == WSMouseEvent::MouseDown && event.button() == MouseButton::Left;
bool should_open_menu = &menu != wm.current_menu() && (is_hover_with_any_menu_open || is_mousedown_with_left_button);

View file

@ -1,5 +1,6 @@
#pragma once
#include "WSMenu.h"
#include <LibCore/CObject.h>
#include <WindowServer/WSCPUMonitor.h>
#include <WindowServer/WSWindow.h>
@ -15,6 +16,10 @@ public:
virtual void event(CEvent&) override;
bool is_open(const WSMenu&) const;
Vector<WeakPtr<WSMenu>>& open_menu_stack() { return m_open_menu_stack; }
private:
WSWindow& window() { return *m_window; }
const WSWindow& window() const { return *m_window; }
@ -27,4 +32,6 @@ private:
OwnPtr<WSWindow> m_window;
WSCPUMonitor m_cpu_monitor;
String m_username;
Vector<WeakPtr<WSMenu>> m_open_menu_stack;
};

View file

@ -211,14 +211,22 @@ int WSWindowManager::menubar_menu_margin() const
return 16;
}
void WSWindowManager::set_current_menu(WSMenu* menu)
void WSWindowManager::set_current_menu(WSMenu* menu, bool is_submenu)
{
if (m_current_menu == menu)
return;
if (m_current_menu)
if (!is_submenu && m_current_menu)
m_current_menu->close();
if (menu)
m_current_menu = menu->make_weak_ptr();
if (!is_submenu) {
m_menu_manager.open_menu_stack().clear();
if (menu)
m_menu_manager.open_menu_stack().append(menu->make_weak_ptr());
} else {
m_menu_manager.open_menu_stack().append(menu->make_weak_ptr());
}
}
void WSWindowManager::set_current_menubar(WSMenuBar* menubar)
@ -393,6 +401,11 @@ void WSWindowManager::close_current_menu()
if (m_current_menu && m_current_menu->menu_window())
m_current_menu->menu_window()->set_visible(false);
m_current_menu = nullptr;
for (auto& menu : m_menu_manager.open_menu_stack()) {
if (menu)
menu->menu_window()->set_visible(false);
}
m_menu_manager.open_menu_stack().clear();
m_menu_manager.refresh();
}
@ -696,6 +709,18 @@ void WSWindowManager::process_mouse_event(WSMouseEvent& event, WSWindow*& hovere
m_current_menu->clear_hovered_item();
if (event.type() == WSEvent::MouseDown || event.type() == WSEvent::MouseUp)
close_current_menu();
if (event.type() == WSEvent::MouseMove) {
for (auto& menu : m_menu_manager.open_menu_stack()) {
if (!menu)
continue;
if (!menu->menu_window()->rect().contains(event.position()))
continue;
hovered_window = menu->menu_window();
auto translated_event = event.translated(-menu->menu_window()->position());
deliver_mouse_event(*menu->menu_window(), translated_event);
break;
}
}
} else {
hovered_window = &window;
auto translated_event = event.translated(-window.position());

View file

@ -78,11 +78,14 @@ public:
void draw_window_switcher();
WSMenuManager& menu_manager() { return m_menu_manager; }
const WSMenuManager& menu_manager() const { return m_menu_manager; }
Rect menubar_rect() const;
WSMenuBar* current_menubar() { return m_current_menubar.ptr(); }
void set_current_menubar(WSMenuBar*);
WSMenu* current_menu() { return m_current_menu.ptr(); }
void set_current_menu(WSMenu*);
void set_current_menu(WSMenu*, bool is_submenu = false);
WSMenu* system_menu() { return m_system_menu.ptr(); }
const WSCursor& active_cursor() const;