Skip to content

Commit 80a909d

Browse files
feat(shell): support hotkeys
1 parent 59c20b5 commit 80a909d

File tree

7 files changed

+163
-23
lines changed

7 files changed

+163
-23
lines changed

CONFIG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ Breeze Shell 配置文件的 JSON Schema 位于
123123
"padding_vertical": 20,
124124
// 水平边距
125125
"padding_horizontal": 0
126-
}
126+
},
127+
// 是否启用热键
128+
"hotkeys" : true,
127129
},
128130

129131
// 开启调试窗口

resources/schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
},
6060
"position": {
6161
"$ref": "#/definitions/Position"
62+
},
63+
"hotkeys": {
64+
"type": "boolean"
6265
}
6366
},
6467
"required": [],

src/shell/config.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,7 @@ std::filesystem::path config::default_fallback_font() {
153153
return std::filesystem::path(env("WINDIR").value()) / "Fonts" / "msyh.ttc";
154154
}
155155
std::string config::dump_config() { return rfl::json::write(*config::current); }
156+
std::filesystem::path config::default_mono_font() {
157+
return std::filesystem::path(env("WINDIR").value()) / "Fonts" / "consola.ttf";
158+
}
156159
} // namespace mb_shell

src/shell/config.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace mb_shell {
1616
struct config {
1717
static std::filesystem::path default_main_font();
1818
static std::filesystem::path default_fallback_font();
19+
static std::filesystem::path default_mono_font();
1920

2021
struct animated_float_conf {
2122
float duration = _default_animation.duration;
@@ -89,6 +90,7 @@ struct config {
8990
bool ignore_owner_draw = true;
9091
bool reverse_if_open_to_up = true;
9192
bool experimental_ownerdraw_support = false;
93+
bool hotkeys = true;
9294

9395
// debug purpose only
9496
bool search_large_dwItemData_range = false;
@@ -102,6 +104,7 @@ struct config {
102104
// Restart to apply font/hook changes
103105
std::filesystem::path font_path_main = default_main_font();
104106
std::filesystem::path font_path_fallback = default_fallback_font();
107+
std::filesystem::path font_path_monospace = default_mono_font();
105108
bool res_string_loader_use_hook = false;
106109
bool avoid_resize_ui = false;
107110
std::vector<std::string> plugin_load_order = {};

src/shell/contextmenu/contextmenu.cc

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
#include <future>
2121
#include <iostream>
2222
#include <print>
23+
#include <ranges>
2324
#include <string>
2425
#include <thread>
2526
#include <type_traits>
2627
#include <variant>
27-
#include <winuser.h>
2828

2929
namespace mb_shell {
3030
owner_draw_menu_info getBitmapFromOwnerDraw(MENUITEMINFOW *menuItemInfo,
@@ -112,6 +112,25 @@ std::wstring strip_extra_infos(std::wstring_view str) {
112112
return result;
113113
}
114114

115+
std::vector<std::string> extract_hotkeys(const std::string &name) {
116+
std::vector<std::string> keys;
117+
118+
size_t pos = 0;
119+
while ((pos = name.find('&', pos)) != std::string::npos) {
120+
if (pos + 1 < name.length()) {
121+
char key = name[pos + 1];
122+
if (key != '&') { // Skip escaped ampersands (&&)
123+
keys.push_back(std::string(1, key));
124+
}
125+
pos += 2;
126+
} else {
127+
pos++;
128+
}
129+
}
130+
131+
return keys;
132+
}
133+
115134
menu menu::construct_with_hmenu(HMENU hMenu, HWND hWnd, bool is_top) {
116135
menu m;
117136

@@ -177,6 +196,13 @@ menu menu::construct_with_hmenu(HMENU hMenu, HWND hWnd, bool is_top) {
177196
} else {
178197
item.name = wstring_to_utf8(strip_extra_infos(buffer));
179198
item.origin_name = wstring_to_utf8(buffer);
199+
if (config::current->context_menu.hotkeys) {
200+
auto hotkeys = extract_hotkeys(item.origin_name.value());
201+
if (!hotkeys.empty()) {
202+
item.hotkey = std::ranges::views::join_with(hotkeys, " + ") |
203+
std::ranges::to<std::string>();
204+
}
205+
}
180206

181207
auto id_stripped = res_string_loader::string_to_id(buffer);
182208
if (std::get_if<size_t>(&id_stripped)) {

src/shell/contextmenu/menu_render.cc

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ menu_render menu_render::create(int x, int y, menu menu) {
4242
config::current->font_path_main.string().c_str());
4343
nvgCreateFont(rt->nvg, "fallback",
4444
config::current->font_path_fallback.string().c_str());
45+
nvgCreateFont(rt->nvg, "monospace",
46+
config::current->font_path_monospace.string().c_str());
4547
nvgAddFallbackFont(rt->nvg, "main", "fallback");
48+
nvgAddFallbackFont(rt->nvg, "monospace", "main");
4649
return rt;
4750
}();
4851
auto render = menu_render(rt, std::nullopt);
@@ -60,8 +63,8 @@ menu_render menu_render::create(int x, int y, menu menu) {
6063
// set the position of the window to fullscreen in this monitor + padding
6164

6265
dbgout("Monitor: {} {} {} {}", monitor_info.rcMonitor.left,
63-
monitor_info.rcMonitor.top, monitor_info.rcMonitor.right,
64-
monitor_info.rcMonitor.bottom);
66+
monitor_info.rcMonitor.top, monitor_info.rcMonitor.right,
67+
monitor_info.rcMonitor.bottom);
6568

6669
rt->set_position(monitor_info.rcMonitor.left, monitor_info.rcMonitor.top);
6770
if (!config::current->avoid_resize_ui)
@@ -88,9 +91,9 @@ menu_render menu_render::create(int x, int y, menu menu) {
8891
listener->operator()(menu_info);
8992
}
9093
dbgout("[perf] JS plugins costed {}ms",
91-
std::chrono::duration_cast<std::chrono::milliseconds>(
92-
rt->clock.now() - before_js)
93-
.count());
94+
std::chrono::duration_cast<std::chrono::milliseconds>(rt->clock.now() -
95+
before_js)
96+
.count());
9497

9598
dbgout("Current menu: {}", menu_render::current.has_value());
9699
return render;

src/shell/contextmenu/menu_widget.cc

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,11 @@ void mb_shell::menu_item_normal_widget::render(ui::nanovg_context ctx) {
113113

114114
// Draw hotkey
115115
if (item.hotkey && !item.hotkey->empty()) {
116-
ctx.fillColor(nvgRGBAf(c, c, c, *opacity / 255.f * 0.7)); // Slightly dimmed
117-
ctx.fontSize(font_size * 0.9); // Slightly smaller font
116+
auto t = ctx.transaction();
117+
ctx.fillColor(nvgRGBAf(c, c, c, *opacity / 255.f * 0.7));
118+
ctx.fontSize(font_size * 0.9);
118119
ctx.textAlign(NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
119-
120+
ctx.fontFace("monospace");
120121
auto hotkey_x = right_x - hotkey_padding;
121122
ctx.text(round(hotkey_x), round(*y + *height / 2), item.hotkey->c_str(),
122123
nullptr);
@@ -146,8 +147,10 @@ float mb_shell::menu_item_normal_widget::measure_width(
146147

147148
// Hotkey
148149
if (item.hotkey && !item.hotkey->empty()) {
150+
auto t = ctx.vg.transaction();
149151
ctx.vg.fontSize(font_size * 0.9);
150152
auto hotkey_padding = config::current->context_menu.theme.hotkey_padding;
153+
ctx.vg.fontFace("monospace");
151154
width +=
152155
ctx.vg.measureText(item.hotkey->c_str()).first + hotkey_padding * 2;
153156
}
@@ -378,18 +381,18 @@ void mb_shell::menu_widget::update(ui::update_context &ctx) {
378381
(!owner_rt->focused_widget.has_value() && rendering_submenus.empty()));
379382

380383
if (should_handle_keyboard) {
381-
auto move_key = [&](this auto &self, bool next) -> void {
384+
auto move_key = [&](this auto &self, bool next, auto &items) -> void {
382385
auto focused_item = std::ranges::find_if(
383-
item_widgets, [](const auto &item) { return item->focused(); });
384-
if (focused_item != item_widgets.end()) {
386+
items, [](const auto &item) { return item->focused(); });
387+
if (focused_item != items.end()) {
385388
if (next) {
386389
focused_item++;
387-
if (focused_item == item_widgets.end()) {
388-
focused_item = item_widgets.begin();
390+
if (focused_item == items.end()) {
391+
focused_item = items.begin();
389392
}
390393
} else {
391-
if (focused_item == item_widgets.begin()) {
392-
focused_item = item_widgets.end() - 1;
394+
if (focused_item == items.begin()) {
395+
focused_item = items.end() - 1;
393396
} else {
394397
focused_item--;
395398
}
@@ -401,22 +404,22 @@ void mb_shell::menu_widget::update(ui::update_context &ctx) {
401404
(*focused_item)->template downcast<menu_item_normal_widget>()) {
402405
if (wid->item.disabled ||
403406
wid->item.type == mb_shell::menu_item::type::spacer) {
404-
self(next);
407+
self(next, items);
405408
return;
406409
}
407410
} else {
408-
self(next);
411+
self(next, items);
409412
}
410413
} else {
411-
if (!item_widgets.empty()) {
412-
item_widgets.front()->set_focus(true);
414+
if (!items.empty()) {
415+
items.front()->set_focus(true);
413416
}
414417
}
415418
};
416419
if (ctx.key_pressed(GLFW_KEY_UP)) {
417-
move_key(false);
420+
move_key(false, item_widgets);
418421
} else if (ctx.key_pressed(GLFW_KEY_DOWN)) {
419-
move_key(true);
422+
move_key(true, item_widgets);
420423
} else if (ctx.key_pressed(GLFW_KEY_LEFT)) {
421424
// close submenu on left key
422425
if (parent_menu) {
@@ -458,6 +461,103 @@ void mb_shell::menu_widget::update(ui::update_context &ctx) {
458461
}
459462
}
460463
}
464+
} else {
465+
auto menus_matching_key =
466+
item_widgets | std::views::filter([&](const auto &item) {
467+
if (auto wid = item->template downcast<menu_item_normal_widget>()) {
468+
if (!wid->item.hotkey)
469+
return false;
470+
auto hotkey = *wid->item.hotkey | std::views::split('+') |
471+
std::views::transform([](const auto &c) {
472+
auto s = std::string(c.begin(), c.end());
473+
// trim whitespace
474+
s.erase(s.find_last_not_of(" \t\n\r") + 1);
475+
s.erase(0, s.find_first_not_of(" \t\n\r"));
476+
return s;
477+
});
478+
479+
static auto translate_map = std::unordered_map<std::string, int>{
480+
{"ctrl", GLFW_KEY_LEFT_CONTROL},
481+
{"shift", GLFW_KEY_LEFT_SHIFT},
482+
{"alt", GLFW_KEY_LEFT_ALT},
483+
{"win", GLFW_KEY_LEFT_SUPER},
484+
{"a", GLFW_KEY_A},
485+
{"b", GLFW_KEY_B},
486+
{"c", GLFW_KEY_C},
487+
{"d", GLFW_KEY_D},
488+
{"e", GLFW_KEY_E},
489+
{"f", GLFW_KEY_F},
490+
{"g", GLFW_KEY_G},
491+
{"h", GLFW_KEY_H},
492+
{"i", GLFW_KEY_I},
493+
{"j", GLFW_KEY_J},
494+
{"k", GLFW_KEY_K},
495+
{"l", GLFW_KEY_L},
496+
{"m", GLFW_KEY_M},
497+
{"n", GLFW_KEY_N},
498+
{"o", GLFW_KEY_O},
499+
{"p", GLFW_KEY_P},
500+
{"q", GLFW_KEY_Q},
501+
{"r", GLFW_KEY_R},
502+
{"s", GLFW_KEY_S},
503+
{"t", GLFW_KEY_T},
504+
{"u", GLFW_KEY_U},
505+
{"v", GLFW_KEY_V},
506+
{"w", GLFW_KEY_W},
507+
{"x", GLFW_KEY_X},
508+
{"y", GLFW_KEY_Y},
509+
{"z", GLFW_KEY_Z},
510+
{"0", GLFW_KEY_0},
511+
{"1", GLFW_KEY_1},
512+
{"2", GLFW_KEY_2},
513+
{"3", GLFW_KEY_3},
514+
{"4", GLFW_KEY_4},
515+
{"5", GLFW_KEY_5},
516+
{"6", GLFW_KEY_6},
517+
{"7", GLFW_KEY_7},
518+
{"8", GLFW_KEY_8},
519+
{"9", GLFW_KEY_9},
520+
};
521+
522+
auto key_combination = std::vector<int>();
523+
for (const auto &key : hotkey) {
524+
if (auto it = translate_map.find(
525+
std::string(key) | std::views::transform(::tolower) |
526+
std::ranges::to<std::string>());
527+
it != translate_map.end()) {
528+
key_combination.push_back(it->second);
529+
} else {
530+
// If the key is not found, we can ignore it
531+
return false;
532+
}
533+
}
534+
535+
return std::ranges::all_of(key_combination, [&](int key) {
536+
return ctx.key_pressed(key);
537+
});
538+
}
539+
return false;
540+
}) |
541+
std::ranges::to<std::vector>();
542+
543+
if (menus_matching_key.size() == 1) {
544+
auto wid = menus_matching_key.front()
545+
->downcast<mb_shell::menu_item_normal_widget>();
546+
if (wid && wid->item.action) {
547+
try {
548+
wid->item.action.value()();
549+
} catch (std::exception &e) {
550+
std::cerr << "Error in action: " << e.what() << std::endl;
551+
}
552+
} else if (wid && wid->item.submenu && !wid->submenu_wid) {
553+
wid->show_submenu(ctx);
554+
current_submenu->set_focus();
555+
}
556+
} else if (menus_matching_key.size() > 1) {
557+
move_key(!ctx.key_pressed(GLFW_KEY_LEFT_SHIFT) &&
558+
!ctx.key_pressed(GLFW_KEY_RIGHT_SHIFT),
559+
menus_matching_key);
560+
}
461561
}
462562
}
463563
}

0 commit comments

Comments
 (0)