Serenity Operating System
at master 245 lines 7.0 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/Debug.h> 8#include <AK/HashMap.h> 9#include <AK/IDAllocator.h> 10#include <LibGUI/Action.h> 11#include <LibGUI/ActionGroup.h> 12#include <LibGUI/Application.h> 13#include <LibGUI/ConnectionToWindowServer.h> 14#include <LibGUI/Menu.h> 15#include <LibGUI/MenuItem.h> 16#include <LibGfx/Bitmap.h> 17 18namespace GUI { 19 20static IDAllocator s_menu_id_allocator; 21 22static HashMap<int, Menu*>& all_menus() 23{ 24 static HashMap<int, Menu*> s_map; 25 return s_map; 26} 27 28Menu* Menu::from_menu_id(int menu_id) 29{ 30 auto it = all_menus().find(menu_id); 31 if (it == all_menus().end()) 32 return nullptr; 33 return (*it).value; 34} 35 36Menu::Menu(DeprecatedString name) 37 : m_name(move(name)) 38{ 39} 40 41Menu::~Menu() 42{ 43 unrealize_menu(); 44} 45 46void Menu::set_icon(Gfx::Bitmap const* icon) 47{ 48 m_icon = icon; 49} 50 51ErrorOr<void> Menu::try_add_action(NonnullRefPtr<Action> action) 52{ 53 // NOTE: We grow the vector first, to get allocation failure handled immediately. 54 TRY(m_items.try_ensure_capacity(m_items.size() + 1)); 55 56 auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) MenuItem(m_menu_id, move(action)))); 57 if (m_menu_id != -1) 58 realize_menu_item(*item, m_items.size()); 59 m_items.unchecked_append(move(item)); 60 return {}; 61} 62 63void Menu::add_action(NonnullRefPtr<Action> action) 64{ 65 MUST(try_add_action(move(action))); 66} 67 68void Menu::remove_all_actions() 69{ 70 for (auto& item : m_items) { 71 ConnectionToWindowServer::the().async_remove_menu_item(m_menu_id, item->identifier()); 72 } 73 m_items.clear(); 74} 75 76ErrorOr<NonnullRefPtr<Menu>> Menu::try_add_submenu(DeprecatedString name) 77{ 78 // NOTE: We grow the vector first, to get allocation failure handled immediately. 79 TRY(m_items.try_ensure_capacity(m_items.size() + 1)); 80 81 auto submenu = TRY(Menu::try_create(name)); 82 83 auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) MenuItem(m_menu_id, submenu))); 84 if (m_menu_id != -1) 85 realize_menu_item(*item, m_items.size()); 86 87 m_items.unchecked_append(move(item)); 88 return submenu; 89} 90 91Menu& Menu::add_submenu(DeprecatedString name) 92{ 93 auto menu = MUST(try_add_submenu(move(name))); 94 return menu; 95} 96 97ErrorOr<void> Menu::try_add_separator() 98{ 99 // NOTE: We grow the vector first, to get allocation failure handled immediately. 100 TRY(m_items.try_ensure_capacity(m_items.size() + 1)); 101 102 auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) MenuItem(m_menu_id, MenuItem::Type::Separator))); 103 if (m_menu_id != -1) 104 realize_menu_item(*item, m_items.size()); 105 m_items.unchecked_append(move(item)); 106 return {}; 107} 108 109void Menu::add_separator() 110{ 111 MUST(try_add_separator()); 112} 113 114void Menu::realize_if_needed(RefPtr<Action> const& default_action) 115{ 116 if (m_menu_id == -1 || m_current_default_action.ptr() != default_action) 117 realize_menu(default_action); 118} 119 120void Menu::popup(Gfx::IntPoint screen_position, RefPtr<Action> const& default_action, Gfx::IntRect const& button_rect) 121{ 122 realize_if_needed(default_action); 123 ConnectionToWindowServer::the().async_popup_menu(m_menu_id, screen_position, button_rect); 124} 125 126void Menu::dismiss() 127{ 128 if (m_menu_id == -1) 129 return; 130 ConnectionToWindowServer::the().async_dismiss_menu(m_menu_id); 131} 132 133int Menu::realize_menu(RefPtr<Action> default_action) 134{ 135 unrealize_menu(); 136 m_menu_id = s_menu_id_allocator.allocate(); 137 138 ConnectionToWindowServer::the().async_create_menu(m_menu_id, m_name); 139 140 dbgln_if(MENU_DEBUG, "GUI::Menu::realize_menu(): New menu ID: {}", m_menu_id); 141 VERIFY(m_menu_id > 0); 142 m_current_default_action = default_action; 143 144 for (size_t i = 0; i < m_items.size(); ++i) { 145 realize_menu_item(*m_items[i], i); 146 } 147 148 all_menus().set(m_menu_id, this); 149 return m_menu_id; 150} 151 152void Menu::unrealize_menu() 153{ 154 if (m_menu_id == -1) 155 return; 156 all_menus().remove(m_menu_id); 157 ConnectionToWindowServer::the().async_destroy_menu(m_menu_id); 158 m_menu_id = -1; 159} 160 161void Menu::realize_menu_if_needed() 162{ 163 if (menu_id() == -1) 164 realize_menu(); 165} 166 167Action* Menu::action_at(size_t index) 168{ 169 if (index >= m_items.size()) 170 return nullptr; 171 return m_items[index]->action(); 172} 173 174void Menu::set_children_actions_enabled(bool enabled) 175{ 176 for (auto& item : m_items) { 177 if (item->action()) 178 item->action()->set_enabled(enabled); 179 } 180} 181 182void Menu::visibility_did_change(Badge<ConnectionToWindowServer>, bool visible) 183{ 184 if (m_visible == visible) 185 return; 186 m_visible = visible; 187 if (on_visibility_change) 188 on_visibility_change(visible); 189} 190 191void Menu::realize_menu_item(MenuItem& item, int item_id) 192{ 193 item.set_menu_id({}, m_menu_id); 194 item.set_identifier({}, item_id); 195 switch (item.type()) { 196 case MenuItem::Type::Separator: 197 ConnectionToWindowServer::the().async_add_menu_separator(m_menu_id); 198 break; 199 case MenuItem::Type::Action: { 200 auto& action = *item.action(); 201 auto shortcut_text = action.shortcut().is_valid() ? action.shortcut().to_deprecated_string() : DeprecatedString(); 202 bool exclusive = action.group() && action.group()->is_exclusive() && action.is_checkable(); 203 bool is_default = (m_current_default_action.ptr() == &action); 204 auto icon = action.icon() ? action.icon()->to_shareable_bitmap() : Gfx::ShareableBitmap(); 205 ConnectionToWindowServer::the().async_add_menu_item(m_menu_id, item_id, -1, action.text(), action.is_enabled(), action.is_visible(), action.is_checkable(), action.is_checkable() ? action.is_checked() : false, is_default, shortcut_text, icon, exclusive); 206 break; 207 } 208 case MenuItem::Type::Submenu: { 209 auto& submenu = *item.submenu(); 210 submenu.realize_if_needed(m_current_default_action.strong_ref()); 211 auto icon = submenu.icon() ? submenu.icon()->to_shareable_bitmap() : Gfx::ShareableBitmap(); 212 ConnectionToWindowServer::the().async_add_menu_item(m_menu_id, item_id, submenu.menu_id(), submenu.name(), true, true, false, false, false, "", icon, false); 213 break; 214 } 215 case MenuItem::Type::Invalid: 216 default: 217 VERIFY_NOT_REACHED(); 218 } 219} 220 221ErrorOr<void> Menu::add_recent_files_list(Function<void(Action&)> callback) 222{ 223 m_recent_files_callback = move(callback); 224 225 Vector<NonnullRefPtr<GUI::Action>> recent_file_actions; 226 227 for (size_t i = 0; i < GUI::Application::max_recently_open_files(); ++i) { 228 recent_file_actions.append(GUI::Action::create("", [&](auto& action) { m_recent_files_callback(action); })); 229 } 230 231 recent_file_actions.append(GUI::Action::create("(No recently open files)", nullptr)); 232 recent_file_actions.last()->set_enabled(false); 233 234 auto* app = GUI::Application::the(); 235 app->register_recent_file_actions({}, recent_file_actions); 236 237 for (auto& action : recent_file_actions) { 238 TRY(try_add_action(action)); 239 } 240 241 TRY(try_add_separator()); 242 return {}; 243} 244 245}