mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 19:27:44 +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:
parent
d3ebd8897f
commit
63e6b09816
16 changed files with 177 additions and 12 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue