Skip to content

Commit 7d7d185

Browse files
Copilotlijy91
andcommitted
Fix macOS global shortcuts: F-key lookup table and background event thread
Co-authored-by: lijy91 <3889523+lijy91@users.noreply.github.com>
1 parent 111f83f commit 7d7d185

File tree

1 file changed

+88
-8
lines changed

1 file changed

+88
-8
lines changed

src/platform/macos/shortcut_manager_macos.mm

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#include <algorithm>
2+
#include <atomic>
23
#include <cctype>
4+
#include <mutex>
35
#include <string>
6+
#include <thread>
47
#include <unordered_map>
58
#include <vector>
69

@@ -138,12 +141,26 @@ bool ParseAcceleratorMac(const std::string& accelerator, UInt32& modifiers, UInt
138141
}
139142
}
140143

141-
if (key_token.rfind("f", 0) == 0) {
142-
int fnum = std::stoi(key_token.substr(1));
143-
if (fnum >= 1 && fnum <= 24) {
144-
keycode = kVK_F1 + (fnum - 1);
145-
return true;
144+
if (key_token.rfind("f", 0) == 0 && key_token.size() > 1) {
145+
// Carbon virtual key codes for function keys are non-contiguous; use explicit table.
146+
static const UInt32 kFKeyCodes[] = {
147+
kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5,
148+
kVK_F6, kVK_F7, kVK_F8, kVK_F9, kVK_F10,
149+
kVK_F11, kVK_F12, kVK_F13, kVK_F14, kVK_F15,
150+
kVK_F16, kVK_F17, kVK_F18, kVK_F19, kVK_F20,
151+
};
152+
try {
153+
int fnum = std::stoi(key_token.substr(1));
154+
if (fnum >= 1 && fnum <= static_cast<int>(std::size(kFKeyCodes))) {
155+
keycode = kFKeyCodes[fnum - 1];
156+
return true;
157+
}
158+
} catch (const std::invalid_argument&) {
159+
return false;
160+
} catch (const std::out_of_range&) {
161+
return false;
146162
}
163+
return false;
147164
}
148165

149166
if (key_token == "space") {
@@ -189,11 +206,17 @@ bool ParseAcceleratorMac(const std::string& accelerator, UInt32& modifiers, UInt
189206

190207
} // namespace
191208

209+
// Polling interval for the background event thread (seconds).
210+
static constexpr EventTimeout kEventPollTimeout = kEventDurationSecond * 0.1;
211+
192212
class ShortcutManagerImpl final : public ShortcutManager::Impl {
193213
public:
194214
explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) {}
195215

196216
~ShortcutManagerImpl() override {
217+
StopThread();
218+
JoinThread();
219+
197220
for (const auto& [id, hotkey] : hotkeys_) {
198221
UnregisterEventHotKey(hotkey);
199222
}
@@ -208,7 +231,7 @@ explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) {}
208231
bool IsSupported() override { return true; }
209232

210233
bool RegisterShortcut(const std::shared_ptr<Shortcut>& shortcut) override {
211-
EnsureHandler();
234+
EnsureThread();
212235

213236
UInt32 modifiers = 0;
214237
UInt32 keycode = 0;
@@ -242,10 +265,10 @@ bool UnregisterShortcut(const std::shared_ptr<Shortcut>& shortcut) override {
242265
return true;
243266
}
244267

245-
void SetupEventMonitoring() override { EnsureHandler(); }
268+
void SetupEventMonitoring() override { EnsureThread(); }
246269

247270
void CleanupEventMonitoring() override {
248-
// Keep handler installed as long as shortcuts exist.
271+
// Keep thread running while shortcuts may still be registered.
249272
}
250273

251274
private:
@@ -279,6 +302,59 @@ void EnsureHandler() {
279302
&handler_);
280303
}
281304

305+
void EnsureThread() {
306+
std::lock_guard<std::mutex> lock(thread_mutex_);
307+
if (running_.load()) {
308+
return;
309+
}
310+
311+
EnsureHandler();
312+
313+
running_.store(true);
314+
thread_ = std::thread([this]() { ThreadMain(); });
315+
}
316+
317+
void StopThread() {
318+
std::lock_guard<std::mutex> lock(thread_mutex_);
319+
if (!running_.load()) {
320+
return;
321+
}
322+
323+
running_.store(false);
324+
// Unlock before joining so the thread can complete any in-progress work.
325+
// Note: the thread only touches running_ and Carbon APIs, so this is safe.
326+
}
327+
328+
// Must be called without thread_mutex_ held (to allow the thread to finish).
329+
void JoinThread() {
330+
if (thread_.joinable()) {
331+
thread_.join();
332+
}
333+
}
334+
335+
void ThreadMain() {
336+
const EventTypeSpec hotkey_spec = {kEventClassKeyboard, kEventHotKeyPressed};
337+
338+
while (running_.load()) {
339+
EventRef event = nullptr;
340+
// Use a short timeout so the loop can check the running_ flag periodically.
341+
OSStatus status =
342+
ReceiveNextEvent(1, &hotkey_spec, kEventPollTimeout, true, &event);
343+
344+
if (!running_.load()) {
345+
if (status == noErr && event) {
346+
ReleaseEvent(event);
347+
}
348+
break;
349+
}
350+
351+
if (status == noErr && event) {
352+
SendEventToEventTarget(event, GetApplicationEventTarget());
353+
ReleaseEvent(event);
354+
}
355+
}
356+
}
357+
282358
void HandleHotKey(ShortcutId shortcut_id) {
283359
auto shortcut = manager_->Get(shortcut_id);
284360
if (!shortcut) {
@@ -296,6 +372,10 @@ void HandleHotKey(ShortcutId shortcut_id) {
296372
ShortcutManager* manager_;
297373
std::unordered_map<ShortcutId, EventHotKeyRef> hotkeys_;
298374
EventHandlerRef handler_ = nullptr;
375+
376+
std::atomic<bool> running_{false};
377+
std::thread thread_;
378+
std::mutex thread_mutex_;
299379
};
300380

301381
ShortcutManager::ShortcutManager()

0 commit comments

Comments
 (0)