From 919dd9141f342ff110e0070647500887c0463d67 Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Mon, 6 Apr 2026 17:02:10 +0000 Subject: [PATCH] Windows/macOS: Re-register global shortcuts on keyboard layout change When the keyboard layout changes, the native keycodes for registered shortcuts may change. Re-register all shortcuts with updated native codes to keep them working. Windows: Handle WM_INPUTLANGCHANGE message in the native event filter. macOS: Observe kTISNotifySelectedKeyboardInputSourceChanged via the CF distributed notification center, which is the correct mechanism for detecting input source changes (polling TISCopyCurrentKeyboardInputSource does not work reliably as it requires the event loop to run). Fixes #1323 Assisted-by: Opus 4.6 (Anthropic) --- qxt/qxtglobalshortcut.cpp | 12 ++++++++++++ qxt/qxtglobalshortcut.h | 4 ++++ qxt/qxtglobalshortcut_mac.cpp | 16 ++++++++++++++++ qxt/qxtglobalshortcut_p.h | 1 + qxt/qxtglobalshortcut_win.cpp | 5 +++-- src/app/clipboardserver.cpp | 36 ++++++++++++++++++++++++++++++----- src/app/clipboardserver.h | 2 ++ 7 files changed, 69 insertions(+), 7 deletions(-) diff --git a/qxt/qxtglobalshortcut.cpp b/qxt/qxtglobalshortcut.cpp index 417aacf568..0272c59449 100644 --- a/qxt/qxtglobalshortcut.cpp +++ b/qxt/qxtglobalshortcut.cpp @@ -38,6 +38,7 @@ namespace { int referenceCounter = 0; +std::function layoutChangedCallback; QHash, QxtGlobalShortcut*> shortcuts; @@ -128,6 +129,12 @@ void QxtGlobalShortcutPrivate::activateShortcut(quint32 nativeKey, quint32 nativ emit shortcut->activated(shortcut); } +void QxtGlobalShortcutPrivate::onKeyboardLayoutChanged() +{ + if (layoutChangedCallback) + layoutChangedCallback(); +} + /*! \class QxtGlobalShortcut \brief The QxtGlobalShortcut class provides a global shortcut aka "hotkey". @@ -306,3 +313,8 @@ void QxtGlobalShortcut::setDisabled(bool disabled) { d_ptr->enabled = !disabled; } + +void QxtGlobalShortcut::setLayoutChangedCallback(LayoutChangedCallback callback) +{ + layoutChangedCallback = std::move(callback); +} diff --git a/qxt/qxtglobalshortcut.h b/qxt/qxtglobalshortcut.h index 628e3cd193..3bc206ce09 100644 --- a/qxt/qxtglobalshortcut.h +++ b/qxt/qxtglobalshortcut.h @@ -31,6 +31,7 @@ #include #include +#include class QxtGlobalShortcutPrivate; @@ -61,6 +62,9 @@ class QxtGlobalShortcut final : public QObject static void notifyRestartNeeded(); + using LayoutChangedCallback = std::function; + static void setLayoutChangedCallback(LayoutChangedCallback callback); + public Q_SLOTS: void setEnabled(bool enabled = true); void setDisabled(bool disabled = true); diff --git a/qxt/qxtglobalshortcut_mac.cpp b/qxt/qxtglobalshortcut_mac.cpp index 8629252274..22ff8f537b 100644 --- a/qxt/qxtglobalshortcut_mac.cpp +++ b/qxt/qxtglobalshortcut_mac.cpp @@ -40,6 +40,12 @@ static QMap keyRefs; static QHash keyIDs; static quint32 hotKeySerial = 0; static bool qxt_mac_handler_installed = false; +static bool qxt_mac_layout_observer_installed = false; + +void qxt_mac_keyboard_layout_changed(CFNotificationCenterRef, void *, CFStringRef, const void *, CFDictionaryRef) +{ + QxtGlobalShortcutPrivate::onKeyboardLayoutChanged(); +} OSStatus qxt_mac_handle_hot_key(EventHandlerCallRef nextHandler, EventRef event, void* data) { @@ -57,6 +63,16 @@ OSStatus qxt_mac_handle_hot_key(EventHandlerCallRef nextHandler, EventRef event, void QxtGlobalShortcutPrivate::init() { + if (!qxt_mac_layout_observer_installed) { + qxt_mac_layout_observer_installed = true; + CFNotificationCenterAddObserver( + CFNotificationCenterGetDistributedCenter(), + nullptr, + qxt_mac_keyboard_layout_changed, + kTISNotifySelectedKeyboardInputSourceChanged, + nullptr, + CFNotificationSuspensionBehaviorDeliverImmediately); + } } void QxtGlobalShortcutPrivate::destroy() diff --git a/qxt/qxtglobalshortcut_p.h b/qxt/qxtglobalshortcut_p.h index b736861c4b..fba923f4a1 100644 --- a/qxt/qxtglobalshortcut_p.h +++ b/qxt/qxtglobalshortcut_p.h @@ -67,6 +67,7 @@ class QxtGlobalShortcutPrivate const QByteArray &eventType, void *message, NativeEventResult *result) override; static void activateShortcut(quint32 nativeKey, quint32 nativeMods); + static void onKeyboardLayoutChanged(); private: void initFallback(); diff --git a/qxt/qxtglobalshortcut_win.cpp b/qxt/qxtglobalshortcut_win.cpp index 931d89473e..f8eb9a649e 100644 --- a/qxt/qxtglobalshortcut_win.cpp +++ b/qxt/qxtglobalshortcut_win.cpp @@ -57,11 +57,12 @@ bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray & eventType, Q_UNUSED(eventType) Q_UNUSED(result) MSG* msg = static_cast(message); - if (msg->message == WM_HOTKEY) - { + if (msg->message == WM_HOTKEY) { const quint32 keycode = HIWORD(msg->lParam); const quint32 modifiers = LOWORD(msg->lParam); activateShortcut(keycode, modifiers); + } else if (msg->message == WM_INPUTLANGCHANGE) { + onKeyboardLayoutChanged(); } return false; } diff --git a/src/app/clipboardserver.cpp b/src/app/clipboardserver.cpp index bf8e0648eb..0f8d1e2a19 100644 --- a/src/app/clipboardserver.cpp +++ b/src/app/clipboardserver.cpp @@ -7,6 +7,7 @@ #include "common/clientsocket.h" #include "common/client_server.h" #include "common/commandstatus.h" +#include "common/commandstore.h" #include "common/config.h" #include "common/log.h" #include "common/mimetypes.h" @@ -233,6 +234,12 @@ ClipboardServer::ClipboardServer(QApplication *app, const QString &sessionName) setClipboardMonitorRunning(false); startMonitoring(); +#ifdef COPYQ_GLOBAL_SHORTCUTS + QxtGlobalShortcut::setLayoutChangedCallback([this]() { + onKeyboardLayoutChanged(); + }); +#endif + callback(QStringLiteral("onStart")); } @@ -240,6 +247,9 @@ ClipboardServer::~ClipboardServer() { qApp->setProperty("CopyQ_server", QVariant()); +#ifdef COPYQ_GLOBAL_SHORTCUTS + QxtGlobalShortcut::setLayoutChangedCallback(nullptr); +#endif removeGlobalShortcuts(); delete m_wnd; @@ -303,13 +313,12 @@ void ClipboardServer::removeGlobalShortcuts() m_shortcutActions.clear(); } -void ClipboardServer::onCommandsSaved(const QVector &commands) +void ClipboardServer::addGlobalShortcuts(const QVector &commands) { -#ifdef COPYQ_GLOBAL_SHORTCUTS - removeGlobalShortcuts(); - +#ifndef COPYQ_GLOBAL_SHORTCUTS + Q_UNUSED(commands) +#else QList usedShortcuts; - for (const auto &command : commands) { if (command.type() & CommandType::GlobalShortcut) { for (const auto &shortcutText : command.globalShortcuts) { @@ -322,6 +331,23 @@ void ClipboardServer::onCommandsSaved(const QVector &commands) } } #endif +} + +void ClipboardServer::onKeyboardLayoutChanged() +{ +#ifdef COPYQ_GLOBAL_SHORTCUTS + COPYQ_LOG("Keyboard layout changed, re-registering shortcuts"); + removeGlobalShortcuts(); + addGlobalShortcuts(loadAllCommands()); +#endif +} + +void ClipboardServer::onCommandsSaved(const QVector &commands) +{ +#ifdef COPYQ_GLOBAL_SHORTCUTS + removeGlobalShortcuts(); + addGlobalShortcuts(commands); +#endif const auto hash = monitorCommandStateHash(commands); if ( m_monitor && hash != m_monitorCommandsStateHash ) { diff --git a/src/app/clipboardserver.h b/src/app/clipboardserver.h index b48371e56a..e2ac51d88f 100644 --- a/src/app/clipboardserver.h +++ b/src/app/clipboardserver.h @@ -84,6 +84,8 @@ class ClipboardServer final : public QObject, public App void shortcutActivated(QxtGlobalShortcut *shortcut); void removeGlobalShortcuts(); + void addGlobalShortcuts(const QVector &commands); + void onKeyboardLayoutChanged(); /** Called when new commands are available. */ void onCommandsSaved(const QVector &commands);