diff --git a/Libraries/LibGUI/GMenu.cpp b/Libraries/LibGUI/GMenu.cpp index b2940768f5..9be1fc53d6 100644 --- a/Libraries/LibGUI/GMenu.cpp +++ b/Libraries/LibGUI/GMenu.cpp @@ -39,15 +39,25 @@ void GMenu::add_action(NonnullRefPtr action) #endif } +void GMenu::add_submenu(NonnullOwnPtr submenu) +{ + m_items.append(make(m_menu_id, move(submenu))); +} + void GMenu::add_separator() { m_items.append(make(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(); diff --git a/Libraries/LibGUI/GMenu.h b/Libraries/LibGUI/GMenu.h index 2ddd6f41f2..f873c92def 100644 --- a/Libraries/LibGUI/GMenu.h +++ b/Libraries/LibGUI/GMenu.h @@ -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); void add_separator(); + void add_submenu(NonnullOwnPtr); 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; diff --git a/Libraries/LibGUI/GMenuItem.cpp b/Libraries/LibGUI/GMenuItem.cpp index 3007d560f0..2da0200ca3 100644 --- a/Libraries/LibGUI/GMenuItem.cpp +++ b/Libraries/LibGUI/GMenuItem.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -21,6 +22,13 @@ GMenuItem::GMenuItem(unsigned menu_id, NonnullRefPtr&& action) m_checked = m_action->is_checked(); } +GMenuItem::GMenuItem(unsigned menu_id, NonnullOwnPtr&& submenu) + : m_type(Submenu) + , m_menu_id(menu_id) + , m_submenu(move(submenu)) +{ +} + GMenuItem::~GMenuItem() { if (m_action) diff --git a/Libraries/LibGUI/GMenuItem.h b/Libraries/LibGUI/GMenuItem.h index 092b2c9fe0..73fa8bcdad 100644 --- a/Libraries/LibGUI/GMenuItem.h +++ b/Libraries/LibGUI/GMenuItem.h @@ -2,6 +2,8 @@ #include #include +#include +#include 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&&); + GMenuItem(unsigned menu_id, NonnullOwnPtr&&); ~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 m_action; + OwnPtr m_submenu; }; diff --git a/Servers/WindowServer/WSAPITypes.h b/Servers/WindowServer/WSAPITypes.h index be7eb0ef43..4d5e7574db 100644 --- a/Servers/WindowServer/WSAPITypes.h +++ b/Servers/WindowServer/WSAPITypes.h @@ -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]; diff --git a/Servers/WindowServer/WSClientConnection.cpp b/Servers/WindowServer/WSClientConnection.cpp index 154fdc015b..c71215473a 100644 --- a/Servers/WindowServer/WSClientConnection.cpp +++ b/Servers/WindowServer/WSClientConnection.cpp @@ -159,7 +159,7 @@ bool WSClientConnection::handle_message(const WSAPI_ClientMessage& message, cons did_misbehave(); return false; } - CEventLoop::current().post_event(*this, make(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(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; diff --git a/Servers/WindowServer/WSClientConnection.h b/Servers/WindowServer/WSClientConnection.h index 115951ecfa..16770464f7 100644 --- a/Servers/WindowServer/WSClientConnection.h +++ b/Servers/WindowServer/WSClientConnection.h @@ -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(m_menus.get(menu_id).value_or(nullptr)); + } + private: virtual void event(CEvent&) override; diff --git a/Servers/WindowServer/WSEvent.h b/Servers/WindowServer/WSEvent.h index 215bc00fec..ab1083b5c6 100644 --- a/Servers/WindowServer/WSEvent.h +++ b/Servers/WindowServer/WSEvent.h @@ -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 { diff --git a/Servers/WindowServer/WSMenu.cpp b/Servers/WindowServer/WSMenu.cpp index 1484728e58..a57a7ace17 100644 --- a/Servers/WindowServer/WSMenu.cpp +++ b/Servers/WindowServer/WSMenu.cpp @@ -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); } diff --git a/Servers/WindowServer/WSMenu.h b/Servers/WindowServer/WSMenu.h index 8b0d437775..f57841f446 100644 --- a/Servers/WindowServer/WSMenu.h +++ b/Servers/WindowServer/WSMenu.h @@ -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; diff --git a/Servers/WindowServer/WSMenuItem.cpp b/Servers/WindowServer/WSMenuItem.cpp index ba65e391e6..477497b9dc 100644 --- a/Servers/WindowServer/WSMenuItem.cpp +++ b/Servers/WindowServer/WSMenuItem.cpp @@ -1,4 +1,5 @@ #include "WSMenuItem.h" +#include "WSClientConnection.h" #include "WSMenu.h" #include @@ -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); +} diff --git a/Servers/WindowServer/WSMenuItem.h b/Servers/WindowServer/WSMenuItem.h index 27b9595222..fe334b3c0d 100644 --- a/Servers/WindowServer/WSMenuItem.h +++ b/Servers/WindowServer/WSMenuItem.h @@ -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 m_icon; + int m_submenu_id { -1 }; }; diff --git a/Servers/WindowServer/WSMenuManager.cpp b/Servers/WindowServer/WSMenuManager.cpp index 3f31e6197d..8fb805cba1 100644 --- a/Servers/WindowServer/WSMenuManager.cpp +++ b/Servers/WindowServer/WSMenuManager.cpp @@ -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); diff --git a/Servers/WindowServer/WSMenuManager.h b/Servers/WindowServer/WSMenuManager.h index d039ed4164..dab4717720 100644 --- a/Servers/WindowServer/WSMenuManager.h +++ b/Servers/WindowServer/WSMenuManager.h @@ -1,5 +1,6 @@ #pragma once +#include "WSMenu.h" #include #include #include @@ -15,6 +16,10 @@ public: virtual void event(CEvent&) override; + bool is_open(const WSMenu&) const; + + Vector>& 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 m_window; WSCPUMonitor m_cpu_monitor; String m_username; + + Vector> m_open_menu_stack; }; diff --git a/Servers/WindowServer/WSWindowManager.cpp b/Servers/WindowServer/WSWindowManager.cpp index 04dcb9022a..56074469de 100644 --- a/Servers/WindowServer/WSWindowManager.cpp +++ b/Servers/WindowServer/WSWindowManager.cpp @@ -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()); diff --git a/Servers/WindowServer/WSWindowManager.h b/Servers/WindowServer/WSWindowManager.h index 3c0dabb072..cece8d2de2 100644 --- a/Servers/WindowServer/WSWindowManager.h +++ b/Servers/WindowServer/WSWindowManager.h @@ -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;