Workaround for triggering shortcuts for non-English keyboard layouts

For some unknown reason, QCocoaKeyMapper::possibleKeys does not work correctly for non-English keyboard layouts.
For example, if you press Ctrl+Z on the Russian layout, then the result of the possibleKeys method will be 'Ctrl+Я' and 'Z'. Because of this, qt cannot find shortcuts for these combinations.

The workaround is to register a shortcut depending on the current keyboard layout for possible keys - 'Ctrl+Я' and 'Ctrl+Z'
This commit is contained in:
Eism 2022-06-06 16:32:34 +03:00
parent 4c1081420a
commit d91763e392
6 changed files with 371 additions and 6 deletions

View file

@ -52,5 +52,26 @@ set(MODULE_QRC shortcuts.qrc)
set(MODULE_QML_IMPORT ${CMAKE_CURRENT_LIST_DIR}/qml )
if (OS_IS_MAC)
find_library(CARBON_LIBRARY Carbon)
set(MODULE_SRC ${MODULE_SRC}
${CMAKE_CURRENT_LIST_DIR}/internal/platform/macos/macosshortcutsinstancemodel.mm
${CMAKE_CURRENT_LIST_DIR}/internal/platform/macos/macosshortcutsinstancemodel.h
)
set_source_files_properties(
${CMAKE_CURRENT_LIST_DIR}/internal/platform/macos/macosshortcutsinstancemodel.mm
PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
)
set(MODULE_INCLUDE
${Qt5Gui_PRIVATE_INCLUDE_DIRS}
${CARBON_LIBRARY}
)
endif(OS_IS_MAC)
include(${PROJECT_SOURCE_DIR}/build/module.cmake)

View file

@ -0,0 +1,45 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MU_SHORTCUTS_MACOSSHORTCUTSINSTANCEMODEL_H
#define MU_SHORTCUTS_MACOSSHORTCUTSINSTANCEMODEL_H
#include <QObject>
#include "../../shortcutsinstancemodel.h"
namespace mu::shortcuts {
class MacOSShortcutsInstanceModel : public ShortcutsInstanceModel
{
Q_OBJECT
public:
explicit MacOSShortcutsInstanceModel(QObject* parent = nullptr);
private:
void doLoadShortcuts() override;
void doActivate(const QString& key) override;
QHash<QString, QString> m_shortcutMap;
};
}
#endif // MU_SHORTCUTS_MACOSSHORTCUTSINSTANCEMODEL_H

View file

@ -0,0 +1,281 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "macosshortcutsinstancemodel.h"
#import <Carbon/Carbon.h>
#include <QApplication>
#include <QKeyEvent>
#include <private/qguiapplication_p.h>
#include <qpa/qplatformintegration.h>
#include "log.h"
using namespace mu::shortcuts;
quint32 nativeKeycode(Qt::Key keyCode)
{
switch (keyCode) {
case Qt::Key_Return:
return kVK_Return;
case Qt::Key_Enter:
return kVK_ANSI_KeypadEnter;
case Qt::Key_Tab:
return kVK_Tab;
case Qt::Key_Space:
return kVK_Space;
case Qt::Key_Backspace:
return kVK_Delete;
case Qt::Key_Escape:
return kVK_Escape;
case Qt::Key_CapsLock:
return kVK_CapsLock;
case Qt::Key_Option:
return kVK_Option;
case Qt::Key_F17:
return kVK_F17;
case Qt::Key_VolumeUp:
return kVK_VolumeUp;
case Qt::Key_VolumeDown:
return kVK_VolumeDown;
case Qt::Key_F18:
return kVK_F18;
case Qt::Key_F19:
return kVK_F19;
case Qt::Key_F20:
return kVK_F20;
case Qt::Key_F5:
return kVK_F5;
case Qt::Key_F6:
return kVK_F6;
case Qt::Key_F7:
return kVK_F7;
case Qt::Key_F3:
return kVK_F3;
case Qt::Key_F8:
return kVK_F8;
case Qt::Key_F9:
return kVK_F9;
case Qt::Key_F11:
return kVK_F11;
case Qt::Key_F13:
return kVK_F13;
case Qt::Key_F16:
return kVK_F16;
case Qt::Key_F14:
return kVK_F14;
case Qt::Key_F10:
return kVK_F10;
case Qt::Key_F12:
return kVK_F12;
case Qt::Key_F15:
return kVK_F15;
case Qt::Key_Help:
return kVK_Help;
case Qt::Key_Home:
return kVK_Home;
case Qt::Key_PageUp:
return kVK_PageUp;
case Qt::Key_Delete:
return kVK_ForwardDelete;
case Qt::Key_F4:
return kVK_F4;
case Qt::Key_End:
return kVK_End;
case Qt::Key_F2:
return kVK_F2;
case Qt::Key_PageDown:
return kVK_PageDown;
case Qt::Key_F1:
return kVK_F1;
case Qt::Key_Left:
return kVK_LeftArrow;
case Qt::Key_Right:
return kVK_RightArrow;
case Qt::Key_Down:
return kVK_DownArrow;
case Qt::Key_Up:
return kVK_UpArrow;
default:
break;
}
UTF16Char keyCodeChar = keyCode;
static TISInputSourceRef (* TISCopyCurrentKeyboardLayoutInputSource)(void);
static void*(* TISGetInputSourceProperty)(TISInputSourceRef inputSource, CFStringRef propertyKey);
CFBundleRef bundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Carbon"));
if (bundle) {
*(void**)& TISGetInputSourceProperty = CFBundleGetFunctionPointerForName(bundle, CFSTR("TISGetInputSourceProperty"));
*(void**)& TISCopyCurrentKeyboardLayoutInputSource
= CFBundleGetFunctionPointerForName(bundle, CFSTR("TISCopyCurrentKeyboardLayoutInputSource"));
}
if (!TISCopyCurrentKeyboardLayoutInputSource || !TISGetInputSourceProperty) {
LOGE() << "Error getting functions from Carbon framework";
return 0;
}
TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardLayoutInputSource();
CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, CFSTR("TISPropertyUnicodeKeyLayoutData"));
UCKeyboardLayout* keyboardLayout = (UCKeyboardLayout*)CFDataGetBytePtr(uchr);
if (!keyboardLayout) {
LOGE() << "The keyboard layout is not valid";
return 0;
}
UCKeyboardTypeHeader* table = keyboardLayout->keyboardTypeList;
uint8_t* data = (uint8_t*)keyboardLayout;
for (quint32 i = 0; i < keyboardLayout->keyboardTypeCount; i++) {
UCKeyStateRecordsIndex* stateRec = 0;
if (table[i].keyStateRecordsIndexOffset != 0) {
stateRec = reinterpret_cast<UCKeyStateRecordsIndex*>(data + table[i].keyStateRecordsIndexOffset);
if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) {
stateRec = 0;
}
}
UCKeyToCharTableIndex* charTable = reinterpret_cast<UCKeyToCharTableIndex*>(data + table[i].keyToCharTableIndexOffset);
if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat) {
continue;
}
for (quint32 j = 0; j < charTable->keyToCharTableCount; j++) {
UCKeyOutput* keyToChar = reinterpret_cast<UCKeyOutput*>(data + charTable->keyToCharTableOffsets[j]);
for (quint32 k = 0; k < charTable->keyToCharTableSize; k++) {
if (keyToChar[k] & kUCKeyOutputTestForIndexMask) {
long idx = keyToChar[k] & kUCKeyOutputGetIndexMask;
if (stateRec && idx < stateRec->keyStateRecordCount) {
UCKeyStateRecord* rec = reinterpret_cast<UCKeyStateRecord*>(data + stateRec->keyStateRecordOffsets[idx]);
if (rec->stateZeroCharData == keyCodeChar) {
return k;
}
}
} else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) && keyToChar[k] < 0xFFFE) {
if (keyToChar[k] == keyCodeChar) {
return k;
}
}
}
}
}
return 0;
}
quint32 nativeModifiers(Qt::KeyboardModifiers modifiers)
{
quint32 result = 0;
if (modifiers & Qt::ShiftModifier) {
result |= shiftKey;
}
if (modifiers & Qt::ControlModifier) {
result |= cmdKey;
}
if (modifiers & Qt::AltModifier) {
result |= optionKey;
}
if (modifiers & Qt::MetaModifier) {
result |= controlKey;
}
if (modifiers & Qt::KeypadModifier) {
result |= kEventKeyModifierNumLockMask;
}
return result;
}
QSet<int> possibleKeys(const QKeySequence& sequence)
{
const int key = sequence[0];
const Qt::KeyboardModifiers modifiers = Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask);
quint32 keyNativeCode = nativeKeycode(Qt::Key(key & ~Qt::KeyboardModifierMask));
quint32 keyNativeModifiers = nativeModifiers(modifiers);
QKeyEvent fakeKey(QKeyEvent::None, key, modifiers, keyNativeCode, keyNativeCode, keyNativeModifiers);
QList<int> keys = QGuiApplicationPrivate::platformIntegration()->possibleKeys(&fakeKey);
QList<int> result;
for (int key : keys) {
if (modifiers != Qt::NoModifier) {
if (Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask) == Qt::NoModifier) {
key += modifiers;
}
if (Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask) != modifiers) {
continue;
}
}
result << key;
}
return QSet<int>(result.cbegin(), result.cend());
}
MacOSShortcutsInstanceModel::MacOSShortcutsInstanceModel(QObject* parent)
: ShortcutsInstanceModel(parent)
{
connect(qApp->inputMethod(), &QInputMethod::localeChanged, this, [this]() {
doLoadShortcuts();
});
}
void MacOSShortcutsInstanceModel::doLoadShortcuts()
{
m_shortcuts.clear();
m_shortcutMap.clear();
ShortcutList shortcuts = shortcutsRegister()->shortcuts();
for (const Shortcut& sc : shortcuts) {
for (const std::string& seq : sc.sequences) {
QString sequence = QString::fromStdString(seq);
QSet<int> keys = possibleKeys(QKeySequence::fromString(sequence, QKeySequence::PortableText));
for (int key : keys) {
QKeySequence keySeq(key);
QString seqStr = keySeq.toString(QKeySequence::PortableText);
//! NOTE There may be several identical shortcuts for different contexts.
//! We only need a list of unique ones.
if (!m_shortcuts.contains(seqStr)) {
m_shortcuts << seqStr;
m_shortcutMap.insert(seqStr, sequence);
}
}
}
}
emit shortcutsChanged();
}
void MacOSShortcutsInstanceModel::doActivate(const QString& key)
{
ShortcutsInstanceModel::doActivate(m_shortcutMap[key]);
}

View file

@ -33,14 +33,14 @@ ShortcutsInstanceModel::ShortcutsInstanceModel(QObject* parent)
void ShortcutsInstanceModel::init()
{
shortcutsRegister()->shortcutsChanged().onNotify(this, [this](){
loadShortcuts();
doLoadShortcuts();
});
shortcutsRegister()->activeChanged().onNotify(this, [this](){
emit activeChanged();
});
loadShortcuts();
doLoadShortcuts();
}
QStringList ShortcutsInstanceModel::shortcuts() const
@ -55,10 +55,10 @@ bool ShortcutsInstanceModel::active() const
void ShortcutsInstanceModel::activate(const QString& key)
{
controller()->activate(key.toStdString());
doActivate(key);
}
void ShortcutsInstanceModel::loadShortcuts()
void ShortcutsInstanceModel::doLoadShortcuts()
{
m_shortcuts.clear();
@ -77,3 +77,8 @@ void ShortcutsInstanceModel::loadShortcuts()
emit shortcutsChanged();
}
void ShortcutsInstanceModel::doActivate(const QString& key)
{
controller()->activate(key.toStdString());
}

View file

@ -27,6 +27,7 @@
#include <QList>
#include "async/asyncable.h"
#include "modularity/ioc.h"
#include "ishortcutsregister.h"
#include "ishortcutscontroller.h"
@ -55,8 +56,9 @@ signals:
void shortcutsChanged();
void activeChanged();
private:
void loadShortcuts();
protected:
virtual void doLoadShortcuts();
virtual void doActivate(const QString& key);
QStringList m_shortcuts;
};

View file

@ -26,7 +26,13 @@
#include "log.h"
#include "modularity/ioc.h"
#if defined(Q_OS_MACOS)
#include "internal/platform/macos/macosshortcutsinstancemodel.h"
#else
#include "internal/shortcutsinstancemodel.h"
#endif
#include "internal/shortcutsregister.h"
#include "internal/shortcutscontroller.h"
#include "internal/midiremote.h"
@ -75,7 +81,12 @@ void ShortcutsModule::registerResources()
void ShortcutsModule::registerUiTypes()
{
#if defined(Q_OS_MACOS)
qmlRegisterType<MacOSShortcutsInstanceModel>("MuseScore.Shortcuts", 1, 0, "ShortcutsInstanceModel");
#else
qmlRegisterType<ShortcutsInstanceModel>("MuseScore.Shortcuts", 1, 0, "ShortcutsInstanceModel");
#endif
qmlRegisterType<ShortcutsModel>("MuseScore.Shortcuts", 1, 0, "ShortcutsModel");
qmlRegisterType<EditShortcutModel>("MuseScore.Shortcuts", 1, 0, "EditShortcutModel");
qmlRegisterType<MidiDeviceMappingModel>("MuseScore.Shortcuts", 1, 0, "MidiDeviceMappingModel");