1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 05:27:43 +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

@ -39,15 +39,25 @@ void GMenu::add_action(NonnullRefPtr<GAction> action)
#endif
}
void GMenu::add_submenu(NonnullOwnPtr<GMenu> submenu)
{
m_items.append(make<GMenuItem>(m_menu_id, move(submenu)));
}
void GMenu::add_separator()
{
m_items.append(make<GMenuItem>(m_menu_id, GMenuItem::Separator));
}
void GMenu::popup(const Point& screen_position)
void GMenu::realize_if_needed()
{
if (m_menu_id == -1)
realize_menu();
}
void GMenu::popup(const Point& screen_position)
{
realize_if_needed();
WSAPI_ClientMessage request;
request.type = WSAPI_ClientMessage::Type::PopupMenu;
request.menu.menu_id = m_menu_id;
@ -87,14 +97,34 @@ int GMenu::realize_menu()
WSAPI_ClientMessage request;
request.type = WSAPI_ClientMessage::Type::AddMenuSeparator;
request.menu.menu_id = m_menu_id;
request.menu.submenu_id = -1;
GWindowServerConnection::the().sync_request(request, WSAPI_ServerMessage::Type::DidAddMenuSeparator);
continue;
}
if (item.type() == GMenuItem::Submenu) {
auto& submenu = *item.submenu();
submenu.realize_if_needed();
WSAPI_ClientMessage request;
request.type = WSAPI_ClientMessage::Type::AddMenuItem;
request.menu.menu_id = m_menu_id;
request.menu.submenu_id = submenu.menu_id();
request.menu.identifier = i;
// FIXME: It should be possible to disable a submenu.
request.menu.enabled = true;
request.menu.checkable = false;
request.menu.checked = false;
ASSERT(submenu.name().length() < (ssize_t)sizeof(request.text));
strcpy(request.text, submenu.name().characters());
request.text_length = submenu.name().length();
GWindowServerConnection::the().sync_request(request, WSAPI_ServerMessage::Type::DidAddMenuItem);
continue;
}
if (item.type() == GMenuItem::Action) {
auto& action = *item.action();
WSAPI_ClientMessage request;
request.type = WSAPI_ClientMessage::Type::AddMenuItem;
request.menu.menu_id = m_menu_id;
request.menu.submenu_id = -1;
request.menu.identifier = i;
request.menu.enabled = action.is_enabled();
request.menu.checkable = action.is_checkable();

View file

@ -15,10 +15,13 @@ public:
static GMenu* from_menu_id(int);
const String& name() const { return m_name; }
GAction* action_at(int);
void add_action(NonnullRefPtr<GAction>);
void add_separator();
void add_submenu(NonnullOwnPtr<GMenu>);
void popup(const Point& screen_position);
void dismiss();
@ -31,6 +34,7 @@ private:
int menu_id() const { return m_menu_id; }
int realize_menu();
void unrealize_menu();
void realize_if_needed();
int m_menu_id { -1 };
String m_name;

View file

@ -1,5 +1,6 @@
#include <LibGUI/GAction.h>
#include <LibGUI/GEventLoop.h>
#include <LibGUI/GMenu.h>
#include <LibGUI/GMenuItem.h>
#include <WindowServer/WSAPITypes.h>
@ -21,6 +22,13 @@ GMenuItem::GMenuItem(unsigned menu_id, NonnullRefPtr<GAction>&& action)
m_checked = m_action->is_checked();
}
GMenuItem::GMenuItem(unsigned menu_id, NonnullOwnPtr<GMenu>&& submenu)
: m_type(Submenu)
, m_menu_id(menu_id)
, m_submenu(move(submenu))
{
}
GMenuItem::~GMenuItem()
{
if (m_action)

View file

@ -2,6 +2,8 @@
#include <AK/AKString.h>
#include <AK/Badge.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/OwnPtr.h>
class GAction;
class GMenu;
@ -11,11 +13,13 @@ public:
enum Type {
Invalid,
Action,
Separator
Separator,
Submenu,
};
GMenuItem(unsigned menu_id, Type);
GMenuItem(unsigned menu_id, NonnullRefPtr<GAction>&&);
GMenuItem(unsigned menu_id, NonnullOwnPtr<GMenu>&&);
~GMenuItem();
Type type() const { return m_type; }
@ -24,6 +28,9 @@ public:
GAction* action() { return m_action.ptr(); }
unsigned identifier() const { return m_identifier; }
GMenu* submenu() { return m_submenu.ptr(); }
const GMenu* submenu() const { return m_submenu.ptr(); }
bool is_checkable() const { return m_checkable; }
void set_checkable(bool checkable) { m_checkable = checkable; }
@ -46,4 +53,5 @@ private:
bool m_checkable { false };
bool m_checked { false };
RefPtr<GAction> m_action;
OwnPtr<GMenu> m_submenu;
};

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;