diff --git a/Applications/FileManager/main.cpp b/Applications/FileManager/main.cpp index ac05d754f9..2cf086ded4 100644 --- a/Applications/FileManager/main.cpp +++ b/Applications/FileManager/main.cpp @@ -37,7 +37,7 @@ int main(int argc, char** argv) auto* directory_table_view = new DirectoryTableView(widget); auto* statusbar = new GStatusBar(widget); - auto open_parent_directory_action = GAction::create("Open parent directory", GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/parentdirectory16.rgb", { 16, 16 }), [directory_table_view] (const GAction&) { + auto open_parent_directory_action = GAction::create("Open parent directory", { Mod_Alt, Key_Up }, GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/parentdirectory16.rgb", { 16, 16 }), [directory_table_view] (const GAction&) { directory_table_view->open_parent_directory(); }); diff --git a/LibGUI/GAction.cpp b/LibGUI/GAction.cpp index aa6e52c5df..27321c2ff4 100644 --- a/LibGUI/GAction.cpp +++ b/LibGUI/GAction.cpp @@ -1,4 +1,5 @@ #include +#include GAction::GAction(const String& text, const String& custom_data, Function on_activation_callback) : on_activation(move(on_activation_callback)) @@ -19,8 +20,19 @@ GAction::GAction(const String& text, RetainPtr&& icon, Function< { } +GAction::GAction(const String& text, const GShortcut& shortcut, RetainPtr&& icon, Function on_activation_callback) + : on_activation(move(on_activation_callback)) + , m_text(text) + , m_icon(move(icon)) + , m_shortcut(shortcut) +{ + GEventLoop::register_action_with_shortcut(Badge(), *this); +} + GAction::~GAction() { + if (m_shortcut.is_valid()) + GEventLoop::unregister_action_with_shortcut(Badge(), *this); } void GAction::activate() diff --git a/LibGUI/GAction.h b/LibGUI/GAction.h index 305abc59e3..959995d38b 100644 --- a/LibGUI/GAction.h +++ b/LibGUI/GAction.h @@ -5,6 +5,7 @@ #include #include #include +#include class GAction : public Retainable { public: @@ -20,9 +21,14 @@ public: { return adopt(*new GAction(text, move(icon), move(callback))); } + static Retained create(const String& text, const GShortcut& shortcut, RetainPtr&& icon, Function callback) + { + return adopt(*new GAction(text, shortcut, move(icon), move(callback))); + } ~GAction(); String text() const { return m_text; } + GShortcut shortcut() const { return m_shortcut; } String custom_data() const { return m_custom_data; } const GraphicsBitmap* icon() const { return m_icon.ptr(); } @@ -32,11 +38,13 @@ public: private: GAction(const String& text, Function = nullptr); + GAction(const String& text, const GShortcut&, RetainPtr&& icon, Function = nullptr); GAction(const String& text, RetainPtr&& icon, Function = nullptr); GAction(const String& text, const String& custom_data = String(), Function = nullptr); String m_text; String m_custom_data; RetainPtr m_icon; + GShortcut m_shortcut; }; diff --git a/LibGUI/GEventLoop.cpp b/LibGUI/GEventLoop.cpp index c8aa0b2bf1..7e671e7ab7 100644 --- a/LibGUI/GEventLoop.cpp +++ b/LibGUI/GEventLoop.cpp @@ -18,18 +18,17 @@ //#define GEVENTLOOP_DEBUG +static HashMap* g_actions; static GEventLoop* s_mainGEventLoop; -void GEventLoop::initialize() -{ - s_mainGEventLoop = nullptr; -} - GEventLoop::GEventLoop() { if (!s_mainGEventLoop) s_mainGEventLoop = this; + if (!g_actions) + g_actions = new HashMap; + m_event_fd = socket(AF_LOCAL, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (m_event_fd < 0) { perror("socket"); @@ -154,6 +153,14 @@ void GEventLoop::handle_key_event(const WSAPI_ServerMessage& event, GWindow& win #ifdef GEVENTLOOP_DEBUG dbgprintf("WID=%x KeyEvent character=0x%b\n", event.window_id, event.key.character); #endif + + unsigned modifiers = (event.key.alt * Mod_Alt) + (event.key.ctrl * Mod_Ctrl) + (event.key.shift * Mod_Shift); + auto it = g_actions->find(GShortcut(modifiers, (KeyCode)event.key.key)); + if (it != g_actions->end()) { + (*it).value->activate(); + return; + } + auto key_event = make(event.type == WSAPI_ServerMessage::Type::KeyDown ? GEvent::KeyDown : GEvent::KeyUp, event.key.key); key_event->m_alt = event.key.alt; key_event->m_ctrl = event.key.ctrl; @@ -448,3 +455,13 @@ WSAPI_ServerMessage GEventLoop::sync_request(const WSAPI_ClientMessage& request, ASSERT(success); return response; } + +void GEventLoop::register_action_with_shortcut(Badge, GAction& action) +{ + g_actions->set(action.shortcut(), &action); +} + +void GEventLoop::unregister_action_with_shortcut(Badge, GAction& action) +{ + g_actions->remove(action.shortcut()); +} diff --git a/LibGUI/GEventLoop.h b/LibGUI/GEventLoop.h index 152c8b52f9..ab79a5ba4b 100644 --- a/LibGUI/GEventLoop.h +++ b/LibGUI/GEventLoop.h @@ -1,13 +1,14 @@ #pragma once -#include "GEvent.h" #include #include #include #include #include #include +#include +class GAction; class GObject; class GNotifier; class GWindow; @@ -23,8 +24,6 @@ public: static GEventLoop& main(); - static void initialize(); - bool running() const { return m_running; } int register_timer(GObject&, int milliseconds, bool should_reload); @@ -42,6 +41,9 @@ public: pid_t server_pid() const { return m_server_pid; } + static void register_action_with_shortcut(Badge, GAction&); + static void unregister_action_with_shortcut(Badge, GAction&); + private: void wait_for_event(); bool drain_messages_from_server(); diff --git a/LibGUI/GMenu.cpp b/LibGUI/GMenu.cpp index cb5ae39c1e..91a22655f9 100644 --- a/LibGUI/GMenu.cpp +++ b/LibGUI/GMenu.cpp @@ -68,6 +68,16 @@ int GMenu::realize_menu() ASSERT(action.text().length() < (ssize_t)sizeof(request.text)); strcpy(request.text, action.text().characters()); request.text_length = action.text().length(); + + if (action.shortcut().is_valid()) { + auto shortcut_text = action.shortcut().to_string(); + ASSERT(shortcut_text.length() < (ssize_t)sizeof(request.menu.shortcut_text)); + strcpy(request.menu.shortcut_text, shortcut_text.characters()); + request.menu.shortcut_text_length = shortcut_text.length(); + } else { + request.menu.shortcut_text_length = 0; + } + GEventLoop::main().sync_request(request, WSAPI_ServerMessage::Type::DidAddMenuItem); } } diff --git a/LibGUI/GShortcut.cpp b/LibGUI/GShortcut.cpp new file mode 100644 index 0000000000..7673133820 --- /dev/null +++ b/LibGUI/GShortcut.cpp @@ -0,0 +1,137 @@ +#include +#include + +static String to_string(KeyCode key) +{ + switch (key) { + case Key_Escape: return "Escape"; + case Key_Tab: return "Tab"; + case Key_Backspace: return "Backspace"; + case Key_Return: return "Return"; + case Key_Insert: return "Insert"; + case Key_Delete: return "Delete"; + case Key_PrintScreen: return "PrintScreen"; + case Key_SysRq: return "SysRq"; + case Key_Home: return "Home"; + case Key_End: return "End"; + case Key_Left: return "Left"; + case Key_Up: return "Up"; + case Key_Right: return "Right"; + case Key_Down: return "Down"; + case Key_PageUp: return "PageUp"; + case Key_PageDown: return "PageDown"; + case Key_Shift: return "Shift"; + case Key_Control: return "Control"; + case Key_Alt: return "Alt"; + case Key_CapsLock: return "CapsLock"; + case Key_NumLock: return "NumLock"; + case Key_ScrollLock: return "ScrollLock"; + case Key_F1: return "F1"; + case Key_F2: return "F2"; + case Key_F3: return "F3"; + case Key_F4: return "F4"; + case Key_F5: return "F5"; + case Key_F6: return "F6"; + case Key_F7: return "F7"; + case Key_F8: return "F8"; + case Key_F9: return "F9"; + case Key_F10: return "F10"; + case Key_F11: return "F11"; + case Key_F12: return "F12"; + case Key_Space: return "Space"; + case Key_ExclamationPoint: return "!"; + case Key_DoubleQuote: return "\""; + case Key_Hashtag: return "#"; + case Key_Dollar: return "$"; + case Key_Percent: return "%"; + case Key_Ampersand: return "&"; + case Key_Apostrophe: return "'"; + case Key_LeftParen: return "("; + case Key_RightParen: return ")"; + case Key_Asterisk: return "*"; + case Key_Plus: return "+"; + case Key_Comma: return ","; + case Key_Minus: return "-"; + case Key_Period: return ","; + case Key_Slash: return "/"; + case Key_0: return "0"; + case Key_1: return "1"; + case Key_2: return "2"; + case Key_3: return "3"; + case Key_4: return "4"; + case Key_5: return "5"; + case Key_6: return "6"; + case Key_7: return "7"; + case Key_8: return "8"; + case Key_9: return "9"; + case Key_Colon: return ":"; + case Key_Semicolon: return ";"; + case Key_LessThan: return "<"; + case Key_Equal: return "="; + case Key_GreaterThan: return ">"; + case Key_QuestionMark: return "?"; + case Key_AtSign: return "@"; + case Key_A: return "A"; + case Key_B: return "B"; + case Key_C: return "C"; + case Key_D: return "D"; + case Key_E: return "E"; + case Key_F: return "F"; + case Key_G: return "G"; + case Key_H: return "H"; + case Key_I: return "I"; + case Key_J: return "J"; + case Key_K: return "K"; + case Key_L: return "L"; + case Key_M: return "M"; + case Key_N: return "N"; + case Key_O: return "O"; + case Key_P: return "P"; + case Key_Q: return "Q"; + case Key_R: return "R"; + case Key_S: return "S"; + case Key_T: return "T"; + case Key_U: return "U"; + case Key_V: return "V"; + case Key_W: return "W"; + case Key_X: return "X"; + case Key_Y: return "Y"; + case Key_Z: return "Z"; + case Key_LeftBracket: return "["; + case Key_RightBracket: return "]"; + case Key_Backslash: return "\\"; + case Key_Circumflex: return "^"; + case Key_Underscore: return "_"; + case Key_LeftBrace: return "{"; + case Key_RightBrace: return "}"; + case Key_Pipe: return "|"; + case Key_Tilde: return "~"; + case Key_Backtick: return "`"; + + case Key_Invalid: return "Invalid"; + default: + ASSERT_NOT_REACHED(); + } +} + +String GShortcut::to_string() const +{ + Vector parts; + + if (m_modifiers & Mod_Ctrl) + parts.append("Ctrl"); + if (m_modifiers & Mod_Shift) + parts.append("Shift"); + if (m_modifiers & Mod_Alt) + parts.append("Alt"); + + parts.append(::to_string(m_key)); + + StringBuilder builder; + for (int i = 0; i < parts.size(); ++i) { + builder.append(parts[i]); + if (i != parts.size() - 1) + builder.append('+'); + } + return builder.to_string(); +} diff --git a/LibGUI/GShortcut.h b/LibGUI/GShortcut.h new file mode 100644 index 0000000000..7675a6ef30 --- /dev/null +++ b/LibGUI/GShortcut.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +class GShortcut { +public: + GShortcut() { } + GShortcut(unsigned modifiers, KeyCode key) + : m_modifiers(modifiers) + , m_key(key) + { + } + + bool is_valid() const { return m_key != KeyCode::Key_Invalid; } + unsigned modifiers() const { return m_modifiers; } + KeyCode key() const { return m_key; } + String to_string() const; + + bool operator==(const GShortcut& other) const + { + return m_modifiers == other.m_modifiers + && m_key == other.m_key; + } + +private: + unsigned m_modifiers { 0 }; + KeyCode m_key { KeyCode::Key_Invalid }; +}; + +namespace AK { + +template<> +struct Traits { + static unsigned hash(const GShortcut& shortcut) + { + return pair_int_hash(shortcut.modifiers(), shortcut.key()); + } +}; + +} diff --git a/LibGUI/Makefile b/LibGUI/Makefile index 11a0bd563e..29e8b33006 100644 --- a/LibGUI/Makefile +++ b/LibGUI/Makefile @@ -31,6 +31,7 @@ LIBGUI_OBJS = \ GTableView.o \ GTableModel.o \ GVariant.o \ + GShortcut.o \ GWindow.o OBJS = $(SHAREDGRAPHICS_OBJS) $(LIBGUI_OBJS) diff --git a/WindowServer/WSAPITypes.h b/WindowServer/WSAPITypes.h index e7fd570809..1818e91155 100644 --- a/WindowServer/WSAPITypes.h +++ b/WindowServer/WSAPITypes.h @@ -166,6 +166,8 @@ struct WSAPI_ClientMessage { int menubar_id; int menu_id; unsigned identifier; + char shortcut_text[32]; + int shortcut_text_length; } menu; struct { WSAPI_Rect rect; diff --git a/WindowServer/WSClientConnection.cpp b/WindowServer/WSClientConnection.cpp index 0cf421b0f4..71c4c8a076 100644 --- a/WindowServer/WSClientConnection.cpp +++ b/WindowServer/WSClientConnection.cpp @@ -210,14 +210,13 @@ void WSClientConnection::handle_request(WSAPIAddMenuItemRequest& request) { int menu_id = request.menu_id(); unsigned identifier = request.identifier(); - String text = request.text(); auto it = m_menus.find(menu_id); if (it == m_menus.end()) { post_error("Bad menu ID"); return; } auto& menu = *(*it).value; - menu.add_item(make(identifier, move(text))); + menu.add_item(make(identifier, request.text(), request.shortcut_text())); WSAPI_ServerMessage response; response.type = WSAPI_ServerMessage::Type::DidAddMenuItem; response.menu.menu_id = menu_id; diff --git a/WindowServer/WSMenu.cpp b/WindowServer/WSMenu.cpp index 79e3d2de9c..383cde2472 100644 --- a/WindowServer/WSMenu.cpp +++ b/WindowServer/WSMenu.cpp @@ -29,8 +29,13 @@ int WSMenu::width() const { int longest = 0; for (auto& item : m_items) { - if (item->type() == WSMenuItem::Text) - longest = max(longest, font().width(item->text())); + if (item->type() == WSMenuItem::Text) { + int item_width = font().width(item->text()); + if (!item->shortcut_text().is_empty()) + item_width += padding_between_text_and_shortcut() + font().width(item->shortcut_text()); + + longest = max(longest, item_width); + } } return max(longest, rect_in_menubar().width()) + horizontal_padding(); @@ -91,6 +96,9 @@ void WSMenu::draw() text_color = Color::White; } painter.draw_text(item->rect().translated(left_padding(), 0), item->text(), TextAlignment::CenterLeft, text_color); + if (!item->shortcut_text().is_empty()) { + painter.draw_text(item->rect().translated(-right_padding(), 0), item->shortcut_text(), TextAlignment::CenterRight, text_color); + } } else if (item->type() == WSMenuItem::Separator) { Point p1(1, item->rect().center().y()); Point p2(width() - 2, item->rect().center().y()); diff --git a/WindowServer/WSMenu.h b/WindowServer/WSMenu.h index f99f747d8c..107d6fc78f 100644 --- a/WindowServer/WSMenu.h +++ b/WindowServer/WSMenu.h @@ -73,6 +73,7 @@ public: void close(); private: + int padding_between_text_and_shortcut() const { return 50; } void did_activate(WSMenuItem&); WSClientConnection* m_client { nullptr }; int m_menu_id { 0 }; diff --git a/WindowServer/WSMenuItem.cpp b/WindowServer/WSMenuItem.cpp index b0f2e3e03a..49041e0034 100644 --- a/WindowServer/WSMenuItem.cpp +++ b/WindowServer/WSMenuItem.cpp @@ -1,9 +1,10 @@ #include "WSMenuItem.h" -WSMenuItem::WSMenuItem(unsigned identifier, const String& text) +WSMenuItem::WSMenuItem(unsigned identifier, const String& text, const String& shortcut_text) : m_type(Text) , m_identifier(identifier) , m_text(text) + , m_shortcut_text(shortcut_text) { } diff --git a/WindowServer/WSMenuItem.h b/WindowServer/WSMenuItem.h index 0a21adb92d..e3dce736f8 100644 --- a/WindowServer/WSMenuItem.h +++ b/WindowServer/WSMenuItem.h @@ -12,7 +12,7 @@ public: Separator, }; - explicit WSMenuItem(unsigned identifier, const String& text); + explicit WSMenuItem(unsigned identifier, const String& text, const String& shortcut_text = { }); explicit WSMenuItem(Type); ~WSMenuItem(); @@ -20,6 +20,7 @@ public: bool enabled() const { return m_enabled; } String text() const { return m_text; } + String shortcut_text() const { return m_shortcut_text; } void set_rect(const Rect& rect) { m_rect = rect; } Rect rect() const { return m_rect; } @@ -31,6 +32,7 @@ private: bool m_enabled { true }; unsigned m_identifier { 0 }; String m_text; + String m_shortcut_text; Rect m_rect; }; diff --git a/WindowServer/WSMessage.h b/WindowServer/WSMessage.h index 8cd7a6a627..125bd2ef90 100644 --- a/WindowServer/WSMessage.h +++ b/WindowServer/WSMessage.h @@ -189,22 +189,25 @@ private: class WSAPIAddMenuItemRequest : public WSAPIClientRequest { public: - WSAPIAddMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text) + WSAPIAddMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text) : WSAPIClientRequest(WSMessage::APIAddMenuItemRequest, client_id) , m_menu_id(menu_id) , m_identifier(identifier) , m_text(text) + , m_shortcut_text(shortcut_text) { } int menu_id() const { return m_menu_id; } unsigned identifier() const { return m_identifier; } String text() const { return m_text; } + String shortcut_text() const { return m_shortcut_text; } private: int m_menu_id { 0 }; unsigned m_identifier { 0 }; String m_text; + String m_shortcut_text; }; class WSAPIAddMenuSeparatorRequest : public WSAPIClientRequest { diff --git a/WindowServer/WSMessageLoop.cpp b/WindowServer/WSMessageLoop.cpp index 8e68215381..11fc7b1df4 100644 --- a/WindowServer/WSMessageLoop.cpp +++ b/WindowServer/WSMessageLoop.cpp @@ -294,7 +294,8 @@ void WSMessageLoop::on_receive_from_client(int client_id, const WSAPI_ClientMess break; case WSAPI_ClientMessage::Type::AddMenuItem: ASSERT(message.text_length < (ssize_t)sizeof(message.text)); - post_message(client, make(client_id, message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length))); + ASSERT(message.menu.shortcut_text_length < (ssize_t)sizeof(message.menu.shortcut_text)); + post_message(client, 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))); break; case WSAPI_ClientMessage::Type::CreateWindow: ASSERT(message.text_length < (ssize_t)sizeof(message.text));