mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 09:48:11 +00:00
LibGUI+WindowServer: Add app-global keyboard shortcuts.
This patch adds a GShortcut class. Each GAction can have a GShortcut which will cause the event loop to listen for that key combination app-globally and activate the event in case it's pressed. The shortcut will also be displayed when the action is added to a menu. Use this to hook up Alt+Up with the "open parent directory" action in the FileManager app. :^)
This commit is contained in:
parent
5c0fca0a95
commit
596a5ce5a4
17 changed files with 263 additions and 17 deletions
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
|
||||
GAction::GAction(const String& text, const String& custom_data, Function<void(const GAction&)> on_activation_callback)
|
||||
: on_activation(move(on_activation_callback))
|
||||
|
@ -19,8 +20,19 @@ GAction::GAction(const String& text, RetainPtr<GraphicsBitmap>&& icon, Function<
|
|||
{
|
||||
}
|
||||
|
||||
GAction::GAction(const String& text, const GShortcut& shortcut, RetainPtr<GraphicsBitmap>&& icon, Function<void(const GAction&)> 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<GAction>(), *this);
|
||||
}
|
||||
|
||||
GAction::~GAction()
|
||||
{
|
||||
if (m_shortcut.is_valid())
|
||||
GEventLoop::unregister_action_with_shortcut(Badge<GAction>(), *this);
|
||||
}
|
||||
|
||||
void GAction::activate()
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <AK/Retainable.h>
|
||||
#include <AK/Retained.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
#include <LibGUI/GShortcut.h>
|
||||
|
||||
class GAction : public Retainable<GAction> {
|
||||
public:
|
||||
|
@ -20,9 +21,14 @@ public:
|
|||
{
|
||||
return adopt(*new GAction(text, move(icon), move(callback)));
|
||||
}
|
||||
static Retained<GAction> create(const String& text, const GShortcut& shortcut, RetainPtr<GraphicsBitmap>&& icon, Function<void(const GAction&)> 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<void(const GAction&)> = nullptr);
|
||||
GAction(const String& text, const GShortcut&, RetainPtr<GraphicsBitmap>&& icon, Function<void(const GAction&)> = nullptr);
|
||||
GAction(const String& text, RetainPtr<GraphicsBitmap>&& icon, Function<void(const GAction&)> = nullptr);
|
||||
GAction(const String& text, const String& custom_data = String(), Function<void(const GAction&)> = nullptr);
|
||||
|
||||
String m_text;
|
||||
String m_custom_data;
|
||||
RetainPtr<GraphicsBitmap> m_icon;
|
||||
GShortcut m_shortcut;
|
||||
};
|
||||
|
||||
|
|
|
@ -18,18 +18,17 @@
|
|||
|
||||
//#define GEVENTLOOP_DEBUG
|
||||
|
||||
static HashMap<GShortcut, GAction*>* 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<GShortcut, GAction*>;
|
||||
|
||||
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<GKeyEvent>(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>, GAction& action)
|
||||
{
|
||||
g_actions->set(action.shortcut(), &action);
|
||||
}
|
||||
|
||||
void GEventLoop::unregister_action_with_shortcut(Badge<GAction>, GAction& action)
|
||||
{
|
||||
g_actions->remove(action.shortcut());
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "GEvent.h"
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <WindowServer/WSAPITypes.h>
|
||||
#include <LibGUI/GEvent.h>
|
||||
|
||||
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>, GAction&);
|
||||
static void unregister_action_with_shortcut(Badge<GAction>, GAction&);
|
||||
|
||||
private:
|
||||
void wait_for_event();
|
||||
bool drain_messages_from_server();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
137
LibGUI/GShortcut.cpp
Normal file
137
LibGUI/GShortcut.cpp
Normal file
|
@ -0,0 +1,137 @@
|
|||
#include <LibGUI/GShortcut.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
|
||||
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<String> 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();
|
||||
}
|
42
LibGUI/GShortcut.h
Normal file
42
LibGUI/GShortcut.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <Kernel/KeyCode.h>
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Traits.h>
|
||||
|
||||
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<GShortcut> {
|
||||
static unsigned hash(const GShortcut& shortcut)
|
||||
{
|
||||
return pair_int_hash(shortcut.modifiers(), shortcut.key());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -31,6 +31,7 @@ LIBGUI_OBJS = \
|
|||
GTableView.o \
|
||||
GTableModel.o \
|
||||
GVariant.o \
|
||||
GShortcut.o \
|
||||
GWindow.o
|
||||
|
||||
OBJS = $(SHAREDGRAPHICS_OBJS) $(LIBGUI_OBJS)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<WSMenuItem>(identifier, move(text)));
|
||||
menu.add_item(make<WSMenuItem>(identifier, request.text(), request.shortcut_text()));
|
||||
WSAPI_ServerMessage response;
|
||||
response.type = WSAPI_ServerMessage::Type::DidAddMenuItem;
|
||||
response.menu.menu_id = menu_id;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<WSAPIAddMenuItemRequest>(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<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)));
|
||||
break;
|
||||
case WSAPI_ClientMessage::Type::CreateWindow:
|
||||
ASSERT(message.text_length < (ssize_t)sizeof(message.text));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue