macOS: Modernize QCocoaKeyMapper
Paying off technical debt from Qt 4 times, and preparation for removing Carbon dependency. - Proper variable names (m_ prefix, titleCase, fullyWrittenOut) - Modern data structures for lookups - Removal of dead code/variables - Categorized logging - Built in constants instead of magic numbers - Typed variables instead of naked integers Change-Id: Ie14621e0da8ed61e2185fa05373047204dc4ea62 Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
parent
d0ccacaf28
commit
d962de314b
@ -70,7 +70,6 @@ QT_BEGIN_NAMESPACE
|
||||
16. Meta + Alt + Control + Shift
|
||||
*/
|
||||
struct KeyboardLayoutItem {
|
||||
bool dirty;
|
||||
quint32 qtKey[16]; // Can by any Qt::Key_<foo>, or unicode character
|
||||
};
|
||||
|
||||
@ -82,19 +81,20 @@ public:
|
||||
~QCocoaKeyMapper();
|
||||
static Qt::KeyboardModifiers queryKeyboardModifiers();
|
||||
QList<int> possibleKeys(const QKeyEvent *event) const;
|
||||
|
||||
private:
|
||||
bool updateKeyboard();
|
||||
void deleteLayouts();
|
||||
void updateKeyMap(unsigned short macVirtualKey, QChar unicodeKey);
|
||||
void clearMappings();
|
||||
|
||||
private:
|
||||
QCFType<TISInputSourceRef> currentInputSource = nullptr;
|
||||
QCFType<TISInputSourceRef> m_currentInputSource = nullptr;
|
||||
|
||||
enum { NullMode, UnicodeMode, OtherMode } keyboard_mode = NullMode;
|
||||
const UCKeyboardLayout *keyboard_layout_format = nullptr;
|
||||
KeyboardLayoutKind keyboard_kind = kKLKCHRuchrKind;
|
||||
UInt32 keyboard_dead = 0;
|
||||
KeyboardLayoutItem *keyLayout[256];
|
||||
enum { NullMode, UnicodeMode, OtherMode } m_keyboardMode = NullMode;
|
||||
const UCKeyboardLayout *m_keyboardLayoutFormat = nullptr;
|
||||
KeyboardLayoutKind m_keyboardKind = kKLKCHRuchrKind;
|
||||
UInt32 m_deadKeyState = 0; // Maintains dead key state beween calls to UCKeyTranslate
|
||||
KeyboardLayoutItem *m_keyLayout[256];
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
@ -1,6 +1,6 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Copyright (C) 2020 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the plugins of the Qt Toolkit.
|
||||
@ -41,308 +41,246 @@
|
||||
|
||||
#include "qcocoakeymapper.h"
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/qloggingcategory.h>
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
// QCocoaKeyMapper debug facilities
|
||||
//#define DEBUG_KEY_BINDINGS
|
||||
//#define DEBUG_KEY_BINDINGS_MODIFIERS
|
||||
//#define DEBUG_KEY_MAPS
|
||||
Q_LOGGING_CATEGORY(lcQpaKeyMapper, "qt.qpa.keymapper");
|
||||
Q_LOGGING_CATEGORY(lcQpaKeyMapperKeys, "qt.qpa.keymapper.keys");
|
||||
Q_LOGGING_CATEGORY(lcQpaKeyMapperModifiers, "qt.qpa.keymapper.modifiers");
|
||||
|
||||
// Possible modifier states.
|
||||
// NOTE: The order of these states match the order in updatePossibleKeyCodes()!
|
||||
static const Qt::KeyboardModifiers ModsTbl[] = {
|
||||
Qt::NoModifier, // 0
|
||||
Qt::ShiftModifier, // 1
|
||||
Qt::ControlModifier, // 2
|
||||
Qt::ControlModifier | Qt::ShiftModifier, // 3
|
||||
Qt::AltModifier, // 4
|
||||
Qt::AltModifier | Qt::ShiftModifier, // 5
|
||||
Qt::AltModifier | Qt::ControlModifier, // 6
|
||||
Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 7
|
||||
Qt::MetaModifier, // 8
|
||||
Qt::MetaModifier | Qt::ShiftModifier, // 9
|
||||
Qt::MetaModifier | Qt::ControlModifier, // 10
|
||||
Qt::MetaModifier | Qt::ControlModifier | Qt::ShiftModifier,// 11
|
||||
Qt::MetaModifier | Qt::AltModifier, // 12
|
||||
Qt::MetaModifier | Qt::AltModifier | Qt::ShiftModifier, // 13
|
||||
Qt::MetaModifier | Qt::AltModifier | Qt::ControlModifier, // 14
|
||||
Qt::MetaModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 15
|
||||
static constexpr std::tuple<int, Qt::KeyboardModifier> carbonModifierMap[] = {
|
||||
{ shiftKey, Qt::ShiftModifier },
|
||||
{ rightShiftKey, Qt::ShiftModifier },
|
||||
{ controlKey, Qt::MetaModifier },
|
||||
{ rightControlKey, Qt::MetaModifier },
|
||||
{ cmdKey, Qt::ControlModifier },
|
||||
{ optionKey, Qt::AltModifier },
|
||||
{ rightOptionKey, Qt::AltModifier },
|
||||
{ kEventKeyModifierNumLockMask, Qt::KeypadModifier }
|
||||
};
|
||||
|
||||
bool qt_mac_eat_unicode_key = false;
|
||||
using CarbonModifiers = UInt32; // As opposed to EventModifiers which is UInt16
|
||||
|
||||
|
||||
/* key maps */
|
||||
struct qt_mac_enum_mapper
|
||||
Qt::KeyboardModifiers fromCarbonModifiers(CarbonModifiers carbonModifiers)
|
||||
{
|
||||
int mac_code;
|
||||
int qt_code;
|
||||
#if defined(DEBUG_KEY_BINDINGS)
|
||||
# define QT_MAC_MAP_ENUM(x) x, #x
|
||||
const char *desc;
|
||||
#else
|
||||
# define QT_MAC_MAP_ENUM(x) x
|
||||
#endif
|
||||
};
|
||||
qCDebug(lcQpaKeyMapperModifiers, "Mapping carbon modifiers: %d (0x%04x)",
|
||||
carbonModifiers, carbonModifiers);
|
||||
|
||||
//modifiers
|
||||
static qt_mac_enum_mapper qt_mac_modifier_symbols[] = {
|
||||
{ shiftKey, QT_MAC_MAP_ENUM(Qt::ShiftModifier) },
|
||||
{ rightShiftKey, QT_MAC_MAP_ENUM(Qt::ShiftModifier) },
|
||||
{ controlKey, QT_MAC_MAP_ENUM(Qt::MetaModifier) },
|
||||
{ rightControlKey, QT_MAC_MAP_ENUM(Qt::MetaModifier) },
|
||||
{ cmdKey, QT_MAC_MAP_ENUM(Qt::ControlModifier) },
|
||||
{ optionKey, QT_MAC_MAP_ENUM(Qt::AltModifier) },
|
||||
{ rightOptionKey, QT_MAC_MAP_ENUM(Qt::AltModifier) },
|
||||
{ kEventKeyModifierNumLockMask, QT_MAC_MAP_ENUM(Qt::KeypadModifier) },
|
||||
{ 0, QT_MAC_MAP_ENUM(0) }
|
||||
};
|
||||
Qt::KeyboardModifiers qt_mac_get_modifiers(int keys)
|
||||
{
|
||||
#ifdef DEBUG_KEY_BINDINGS_MODIFIERS
|
||||
qDebug("Qt: internal: **Mapping modifiers: %d (0x%04x)", keys, keys);
|
||||
#endif
|
||||
Qt::KeyboardModifiers ret = Qt::NoModifier;
|
||||
for (int i = 0; qt_mac_modifier_symbols[i].qt_code; i++) {
|
||||
if (keys & qt_mac_modifier_symbols[i].mac_code) {
|
||||
#ifdef DEBUG_KEY_BINDINGS_MODIFIERS
|
||||
qDebug("Qt: internal: got modifier: %s", qt_mac_modifier_symbols[i].desc);
|
||||
#endif
|
||||
ret |= Qt::KeyboardModifier(qt_mac_modifier_symbols[i].qt_code);
|
||||
Qt::KeyboardModifiers qtModifiers = Qt::NoModifier;
|
||||
for (const auto &[carbonModifier, qtModifier] : carbonModifierMap) {
|
||||
if (carbonModifiers & carbonModifier) {
|
||||
qCDebug(lcQpaKeyMapperModifiers) << "Got modifier" << qtModifier;
|
||||
qtModifiers |= qtModifier;
|
||||
}
|
||||
}
|
||||
|
||||
if (qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) {
|
||||
Qt::KeyboardModifiers oldModifiers = ret;
|
||||
ret &= ~(Qt::MetaModifier | Qt::ControlModifier);
|
||||
Qt::KeyboardModifiers oldModifiers = qtModifiers;
|
||||
qtModifiers &= ~(Qt::MetaModifier | Qt::ControlModifier);
|
||||
if (oldModifiers & Qt::ControlModifier)
|
||||
ret |= Qt::MetaModifier;
|
||||
qtModifiers |= Qt::MetaModifier;
|
||||
if (oldModifiers & Qt::MetaModifier)
|
||||
ret |= Qt::ControlModifier;
|
||||
qtModifiers |= Qt::ControlModifier;
|
||||
}
|
||||
return ret;
|
||||
|
||||
return qtModifiers;
|
||||
}
|
||||
static int qt_mac_get_mac_modifiers(Qt::KeyboardModifiers keys)
|
||||
|
||||
static CarbonModifiers toCarbonModifiers(Qt::KeyboardModifiers qtModifiers)
|
||||
{
|
||||
#ifdef DEBUG_KEY_BINDINGS_MODIFIERS
|
||||
qDebug("Qt: internal: **Mapping modifiers: %d (0x%04x)", (int)keys, (int)keys);
|
||||
#endif
|
||||
int ret = 0;
|
||||
for (int i = 0; qt_mac_modifier_symbols[i].qt_code; i++) {
|
||||
if (keys & qt_mac_modifier_symbols[i].qt_code) {
|
||||
#ifdef DEBUG_KEY_BINDINGS_MODIFIERS
|
||||
qDebug("Qt: internal: got modifier: %s", qt_mac_modifier_symbols[i].desc);
|
||||
#endif
|
||||
ret |= qt_mac_modifier_symbols[i].mac_code;
|
||||
qCDebug(lcQpaKeyMapperModifiers).verbosity(1) << "Mapping" << qtModifiers;
|
||||
|
||||
CarbonModifiers carbonModifiers = 0;
|
||||
for (const auto &[carbonModifier, qtModifier] : carbonModifierMap) {
|
||||
if (qtModifiers & qtModifier) {
|
||||
qCDebug(lcQpaKeyMapperModifiers) << "Got carbon modifier" << carbonModifier;
|
||||
carbonModifiers |= carbonModifier;
|
||||
}
|
||||
}
|
||||
|
||||
if (qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) {
|
||||
int oldModifiers = ret;
|
||||
ret &= ~(controlKeyBit | cmdKeyBit);
|
||||
int oldModifiers = carbonModifiers;
|
||||
carbonModifiers &= ~(controlKeyBit | cmdKeyBit);
|
||||
if (oldModifiers & controlKeyBit)
|
||||
ret |= cmdKeyBit;
|
||||
carbonModifiers |= cmdKeyBit;
|
||||
if (oldModifiers & cmdKeyBit)
|
||||
ret |= controlKeyBit;
|
||||
carbonModifiers |= controlKeyBit;
|
||||
}
|
||||
return ret;
|
||||
return carbonModifiers;
|
||||
}
|
||||
|
||||
//keyboard keys (non-modifiers)
|
||||
static qt_mac_enum_mapper qt_mac_keyboard_symbols[] = {
|
||||
{ kHomeCharCode, QT_MAC_MAP_ENUM(Qt::Key_Home) },
|
||||
{ kEnterCharCode, QT_MAC_MAP_ENUM(Qt::Key_Enter) },
|
||||
{ kEndCharCode, QT_MAC_MAP_ENUM(Qt::Key_End) },
|
||||
{ kBackspaceCharCode, QT_MAC_MAP_ENUM(Qt::Key_Backspace) },
|
||||
{ kTabCharCode, QT_MAC_MAP_ENUM(Qt::Key_Tab) },
|
||||
{ kPageUpCharCode, QT_MAC_MAP_ENUM(Qt::Key_PageUp) },
|
||||
{ kPageDownCharCode, QT_MAC_MAP_ENUM(Qt::Key_PageDown) },
|
||||
{ kReturnCharCode, QT_MAC_MAP_ENUM(Qt::Key_Return) },
|
||||
{ kEscapeCharCode, QT_MAC_MAP_ENUM(Qt::Key_Escape) },
|
||||
{ kLeftArrowCharCode, QT_MAC_MAP_ENUM(Qt::Key_Left) },
|
||||
{ kRightArrowCharCode, QT_MAC_MAP_ENUM(Qt::Key_Right) },
|
||||
{ kUpArrowCharCode, QT_MAC_MAP_ENUM(Qt::Key_Up) },
|
||||
{ kDownArrowCharCode, QT_MAC_MAP_ENUM(Qt::Key_Down) },
|
||||
{ kHelpCharCode, QT_MAC_MAP_ENUM(Qt::Key_Help) },
|
||||
{ kDeleteCharCode, QT_MAC_MAP_ENUM(Qt::Key_Delete) },
|
||||
//ascii maps, for debug
|
||||
{ ':', QT_MAC_MAP_ENUM(Qt::Key_Colon) },
|
||||
{ ';', QT_MAC_MAP_ENUM(Qt::Key_Semicolon) },
|
||||
{ '<', QT_MAC_MAP_ENUM(Qt::Key_Less) },
|
||||
{ '=', QT_MAC_MAP_ENUM(Qt::Key_Equal) },
|
||||
{ '>', QT_MAC_MAP_ENUM(Qt::Key_Greater) },
|
||||
{ '?', QT_MAC_MAP_ENUM(Qt::Key_Question) },
|
||||
{ '@', QT_MAC_MAP_ENUM(Qt::Key_At) },
|
||||
{ ' ', QT_MAC_MAP_ENUM(Qt::Key_Space) },
|
||||
{ '!', QT_MAC_MAP_ENUM(Qt::Key_Exclam) },
|
||||
{ '"', QT_MAC_MAP_ENUM(Qt::Key_QuoteDbl) },
|
||||
{ '#', QT_MAC_MAP_ENUM(Qt::Key_NumberSign) },
|
||||
{ '$', QT_MAC_MAP_ENUM(Qt::Key_Dollar) },
|
||||
{ '%', QT_MAC_MAP_ENUM(Qt::Key_Percent) },
|
||||
{ '&', QT_MAC_MAP_ENUM(Qt::Key_Ampersand) },
|
||||
{ '\'', QT_MAC_MAP_ENUM(Qt::Key_Apostrophe) },
|
||||
{ '(', QT_MAC_MAP_ENUM(Qt::Key_ParenLeft) },
|
||||
{ ')', QT_MAC_MAP_ENUM(Qt::Key_ParenRight) },
|
||||
{ '*', QT_MAC_MAP_ENUM(Qt::Key_Asterisk) },
|
||||
{ '+', QT_MAC_MAP_ENUM(Qt::Key_Plus) },
|
||||
{ ',', QT_MAC_MAP_ENUM(Qt::Key_Comma) },
|
||||
{ '-', QT_MAC_MAP_ENUM(Qt::Key_Minus) },
|
||||
{ '.', QT_MAC_MAP_ENUM(Qt::Key_Period) },
|
||||
{ '/', QT_MAC_MAP_ENUM(Qt::Key_Slash) },
|
||||
{ '[', QT_MAC_MAP_ENUM(Qt::Key_BracketLeft) },
|
||||
{ ']', QT_MAC_MAP_ENUM(Qt::Key_BracketRight) },
|
||||
{ '\\', QT_MAC_MAP_ENUM(Qt::Key_Backslash) },
|
||||
{ '_', QT_MAC_MAP_ENUM(Qt::Key_Underscore) },
|
||||
{ '`', QT_MAC_MAP_ENUM(Qt::Key_QuoteLeft) },
|
||||
{ '{', QT_MAC_MAP_ENUM(Qt::Key_BraceLeft) },
|
||||
{ '}', QT_MAC_MAP_ENUM(Qt::Key_BraceRight) },
|
||||
{ '|', QT_MAC_MAP_ENUM(Qt::Key_Bar) },
|
||||
{ '~', QT_MAC_MAP_ENUM(Qt::Key_AsciiTilde) },
|
||||
{ '^', QT_MAC_MAP_ENUM(Qt::Key_AsciiCircum) },
|
||||
{ 0, QT_MAC_MAP_ENUM(0) }
|
||||
// Keyboard keys (non-modifiers)
|
||||
static QHash<QChar, Qt::Key> standardKeys = {
|
||||
{ kHomeCharCode, Qt::Key_Home },
|
||||
{ kEnterCharCode, Qt::Key_Enter },
|
||||
{ kEndCharCode, Qt::Key_End },
|
||||
{ kBackspaceCharCode, Qt::Key_Backspace },
|
||||
{ kTabCharCode, Qt::Key_Tab },
|
||||
{ kPageUpCharCode, Qt::Key_PageUp },
|
||||
{ kPageDownCharCode, Qt::Key_PageDown },
|
||||
{ kReturnCharCode, Qt::Key_Return },
|
||||
{ kEscapeCharCode, Qt::Key_Escape },
|
||||
{ kLeftArrowCharCode, Qt::Key_Left },
|
||||
{ kRightArrowCharCode, Qt::Key_Right },
|
||||
{ kUpArrowCharCode, Qt::Key_Up },
|
||||
{ kDownArrowCharCode, Qt::Key_Down },
|
||||
{ kHelpCharCode, Qt::Key_Help },
|
||||
{ kDeleteCharCode, Qt::Key_Delete },
|
||||
// ASCII maps, for debugging
|
||||
{ ':', Qt::Key_Colon },
|
||||
{ ';', Qt::Key_Semicolon },
|
||||
{ '<', Qt::Key_Less },
|
||||
{ '=', Qt::Key_Equal },
|
||||
{ '>', Qt::Key_Greater },
|
||||
{ '?', Qt::Key_Question },
|
||||
{ '@', Qt::Key_At },
|
||||
{ ' ', Qt::Key_Space },
|
||||
{ '!', Qt::Key_Exclam },
|
||||
{ '"', Qt::Key_QuoteDbl },
|
||||
{ '#', Qt::Key_NumberSign },
|
||||
{ '$', Qt::Key_Dollar },
|
||||
{ '%', Qt::Key_Percent },
|
||||
{ '&', Qt::Key_Ampersand },
|
||||
{ '\'', Qt::Key_Apostrophe },
|
||||
{ '(', Qt::Key_ParenLeft },
|
||||
{ ')', Qt::Key_ParenRight },
|
||||
{ '*', Qt::Key_Asterisk },
|
||||
{ '+', Qt::Key_Plus },
|
||||
{ ',', Qt::Key_Comma },
|
||||
{ '-', Qt::Key_Minus },
|
||||
{ '.', Qt::Key_Period },
|
||||
{ '/', Qt::Key_Slash },
|
||||
{ '[', Qt::Key_BracketLeft },
|
||||
{ ']', Qt::Key_BracketRight },
|
||||
{ '\\', Qt::Key_Backslash },
|
||||
{ '_', Qt::Key_Underscore },
|
||||
{ '`', Qt::Key_QuoteLeft },
|
||||
{ '{', Qt::Key_BraceLeft },
|
||||
{ '}', Qt::Key_BraceRight },
|
||||
{ '|', Qt::Key_Bar },
|
||||
{ '~', Qt::Key_AsciiTilde },
|
||||
{ '^', Qt::Key_AsciiCircum }
|
||||
};
|
||||
|
||||
static qt_mac_enum_mapper qt_mac_keyvkey_symbols[] = { //real scan codes
|
||||
{ kVK_F1, QT_MAC_MAP_ENUM(Qt::Key_F1) },
|
||||
{ kVK_F2, QT_MAC_MAP_ENUM(Qt::Key_F2) },
|
||||
{ kVK_F3, QT_MAC_MAP_ENUM(Qt::Key_F3) },
|
||||
{ kVK_F4, QT_MAC_MAP_ENUM(Qt::Key_F4) },
|
||||
{ kVK_F5, QT_MAC_MAP_ENUM(Qt::Key_F5) },
|
||||
{ kVK_F6, QT_MAC_MAP_ENUM(Qt::Key_F6) },
|
||||
{ kVK_F7, QT_MAC_MAP_ENUM(Qt::Key_F7) },
|
||||
{ kVK_F8, QT_MAC_MAP_ENUM(Qt::Key_F8) },
|
||||
{ kVK_F9, QT_MAC_MAP_ENUM(Qt::Key_F9) },
|
||||
{ kVK_F10, QT_MAC_MAP_ENUM(Qt::Key_F10) },
|
||||
{ kVK_F11, QT_MAC_MAP_ENUM(Qt::Key_F11) },
|
||||
{ kVK_F12, QT_MAC_MAP_ENUM(Qt::Key_F12) },
|
||||
{ kVK_F13, QT_MAC_MAP_ENUM(Qt::Key_F13) },
|
||||
{ kVK_F14, QT_MAC_MAP_ENUM(Qt::Key_F14) },
|
||||
{ kVK_F15, QT_MAC_MAP_ENUM(Qt::Key_F15) },
|
||||
{ kVK_F16, QT_MAC_MAP_ENUM(Qt::Key_F16) },
|
||||
{ kVK_Return, QT_MAC_MAP_ENUM(Qt::Key_Return) },
|
||||
{ kVK_Tab, QT_MAC_MAP_ENUM(Qt::Key_Tab) },
|
||||
{ kVK_Escape, QT_MAC_MAP_ENUM(Qt::Key_Escape) },
|
||||
{ kVK_Help, QT_MAC_MAP_ENUM(Qt::Key_Help) },
|
||||
{ kVK_UpArrow, QT_MAC_MAP_ENUM(Qt::Key_Up) },
|
||||
{ kVK_DownArrow, QT_MAC_MAP_ENUM(Qt::Key_Down) },
|
||||
{ kVK_LeftArrow, QT_MAC_MAP_ENUM(Qt::Key_Left) },
|
||||
{ kVK_RightArrow, QT_MAC_MAP_ENUM(Qt::Key_Right) },
|
||||
{ kVK_PageUp, QT_MAC_MAP_ENUM(Qt::Key_PageUp) },
|
||||
{ kVK_PageDown, QT_MAC_MAP_ENUM(Qt::Key_PageDown) },
|
||||
{ 0, QT_MAC_MAP_ENUM(0) }
|
||||
static QHash<QChar, Qt::Key> virtualKeys = {
|
||||
{ kVK_F1, Qt::Key_F1 },
|
||||
{ kVK_F2, Qt::Key_F2 },
|
||||
{ kVK_F3, Qt::Key_F3 },
|
||||
{ kVK_F4, Qt::Key_F4 },
|
||||
{ kVK_F5, Qt::Key_F5 },
|
||||
{ kVK_F6, Qt::Key_F6 },
|
||||
{ kVK_F7, Qt::Key_F7 },
|
||||
{ kVK_F8, Qt::Key_F8 },
|
||||
{ kVK_F9, Qt::Key_F9 },
|
||||
{ kVK_F10, Qt::Key_F10 },
|
||||
{ kVK_F11, Qt::Key_F11 },
|
||||
{ kVK_F12, Qt::Key_F12 },
|
||||
{ kVK_F13, Qt::Key_F13 },
|
||||
{ kVK_F14, Qt::Key_F14 },
|
||||
{ kVK_F15, Qt::Key_F15 },
|
||||
{ kVK_F16, Qt::Key_F16 },
|
||||
{ kVK_Return, Qt::Key_Return },
|
||||
{ kVK_Tab, Qt::Key_Tab },
|
||||
{ kVK_Escape, Qt::Key_Escape },
|
||||
{ kVK_Help, Qt::Key_Help },
|
||||
{ kVK_UpArrow, Qt::Key_Up },
|
||||
{ kVK_DownArrow, Qt::Key_Down },
|
||||
{ kVK_LeftArrow, Qt::Key_Left },
|
||||
{ kVK_RightArrow, Qt::Key_Right },
|
||||
{ kVK_PageUp, Qt::Key_PageUp },
|
||||
{ kVK_PageDown, Qt::Key_PageDown }
|
||||
};
|
||||
|
||||
static qt_mac_enum_mapper qt_mac_private_unicode[] = {
|
||||
{ 0xF700, QT_MAC_MAP_ENUM(Qt::Key_Up) }, //NSUpArrowFunctionKey
|
||||
{ 0xF701, QT_MAC_MAP_ENUM(Qt::Key_Down) }, //NSDownArrowFunctionKey
|
||||
{ 0xF702, QT_MAC_MAP_ENUM(Qt::Key_Left) }, //NSLeftArrowFunctionKey
|
||||
{ 0xF703, QT_MAC_MAP_ENUM(Qt::Key_Right) }, //NSRightArrowFunctionKey
|
||||
{ 0xF727, QT_MAC_MAP_ENUM(Qt::Key_Insert) }, //NSInsertFunctionKey
|
||||
{ 0xF728, QT_MAC_MAP_ENUM(Qt::Key_Delete) }, //NSDeleteFunctionKey
|
||||
{ 0xF729, QT_MAC_MAP_ENUM(Qt::Key_Home) }, //NSHomeFunctionKey
|
||||
{ 0xF72B, QT_MAC_MAP_ENUM(Qt::Key_End) }, //NSEndFunctionKey
|
||||
{ 0xF72C, QT_MAC_MAP_ENUM(Qt::Key_PageUp) }, //NSPageUpFunctionKey
|
||||
{ 0xF72D, QT_MAC_MAP_ENUM(Qt::Key_PageDown) }, //NSPageDownFunctionKey
|
||||
{ 0xF72E, QT_MAC_MAP_ENUM(Qt::Key_Print) }, //NSPrintScreenFunctionKey
|
||||
{ 0xF72F, QT_MAC_MAP_ENUM(Qt::Key_ScrollLock) }, //NSScrollLockFunctionKey
|
||||
{ 0xF730, QT_MAC_MAP_ENUM(Qt::Key_Pause) }, //NSPauseFunctionKey
|
||||
{ 0xF731, QT_MAC_MAP_ENUM(Qt::Key_SysReq) }, //NSSysReqFunctionKey
|
||||
{ 0xF735, QT_MAC_MAP_ENUM(Qt::Key_Menu) }, //NSMenuFunctionKey
|
||||
{ 0xF738, QT_MAC_MAP_ENUM(Qt::Key_Printer) }, //NSPrintFunctionKey
|
||||
{ 0xF73A, QT_MAC_MAP_ENUM(Qt::Key_Clear) }, //NSClearDisplayFunctionKey
|
||||
{ 0xF73D, QT_MAC_MAP_ENUM(Qt::Key_Insert) }, //NSInsertCharFunctionKey
|
||||
{ 0xF73E, QT_MAC_MAP_ENUM(Qt::Key_Delete) }, //NSDeleteCharFunctionKey
|
||||
{ 0xF741, QT_MAC_MAP_ENUM(Qt::Key_Select) }, //NSSelectFunctionKey
|
||||
{ 0xF742, QT_MAC_MAP_ENUM(Qt::Key_Execute) }, //NSExecuteFunctionKey
|
||||
{ 0xF743, QT_MAC_MAP_ENUM(Qt::Key_Undo) }, //NSUndoFunctionKey
|
||||
{ 0xF744, QT_MAC_MAP_ENUM(Qt::Key_Redo) }, //NSRedoFunctionKey
|
||||
{ 0xF745, QT_MAC_MAP_ENUM(Qt::Key_Find) }, //NSFindFunctionKey
|
||||
{ 0xF746, QT_MAC_MAP_ENUM(Qt::Key_Help) }, //NSHelpFunctionKey
|
||||
{ 0xF747, QT_MAC_MAP_ENUM(Qt::Key_Mode_switch) }, //NSModeSwitchFunctionKey
|
||||
{ 0, QT_MAC_MAP_ENUM(0) }
|
||||
static QHash<QChar, Qt::Key> functionKeys = {
|
||||
{ NSUpArrowFunctionKey, Qt::Key_Up },
|
||||
{ NSDownArrowFunctionKey, Qt::Key_Down },
|
||||
{ NSLeftArrowFunctionKey, Qt::Key_Left },
|
||||
{ NSRightArrowFunctionKey, Qt::Key_Right },
|
||||
// F1-35 function keys handled manually below
|
||||
{ NSInsertFunctionKey, Qt::Key_Insert },
|
||||
{ NSDeleteFunctionKey, Qt::Key_Delete },
|
||||
{ NSHomeFunctionKey, Qt::Key_Home },
|
||||
{ NSEndFunctionKey, Qt::Key_End },
|
||||
{ NSPageUpFunctionKey, Qt::Key_PageUp },
|
||||
{ NSPageDownFunctionKey, Qt::Key_PageDown },
|
||||
{ NSPrintScreenFunctionKey, Qt::Key_Print },
|
||||
{ NSScrollLockFunctionKey, Qt::Key_ScrollLock },
|
||||
{ NSPauseFunctionKey, Qt::Key_Pause },
|
||||
{ NSSysReqFunctionKey, Qt::Key_SysReq },
|
||||
{ NSMenuFunctionKey, Qt::Key_Menu },
|
||||
{ NSPrintFunctionKey, Qt::Key_Printer },
|
||||
{ NSClearDisplayFunctionKey, Qt::Key_Clear },
|
||||
{ NSInsertCharFunctionKey, Qt::Key_Insert },
|
||||
{ NSDeleteCharFunctionKey, Qt::Key_Delete },
|
||||
{ NSSelectFunctionKey, Qt::Key_Select },
|
||||
{ NSExecuteFunctionKey, Qt::Key_Execute },
|
||||
{ NSUndoFunctionKey, Qt::Key_Undo },
|
||||
{ NSRedoFunctionKey, Qt::Key_Redo },
|
||||
{ NSFindFunctionKey, Qt::Key_Find },
|
||||
{ NSHelpFunctionKey, Qt::Key_Help },
|
||||
{ NSModeSwitchFunctionKey, Qt::Key_Mode_switch }
|
||||
};
|
||||
|
||||
static int qt_mac_get_key(int modif, const QChar &key, int virtualKey)
|
||||
static int toKeyCode(const QChar &key, int virtualKey, int modifiers)
|
||||
{
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("**Mapping key: %d (0x%04x) - %d (0x%04x)", key.unicode(), key.unicode(), virtualKey, virtualKey);
|
||||
#endif
|
||||
qCDebug(lcQpaKeyMapperKeys, "Mapping key: %d (0x%04x) / vk %d (0x%04x)",
|
||||
key.unicode(), key.unicode(), virtualKey, virtualKey);
|
||||
|
||||
if (key == kClearCharCode && virtualKey == 0x47)
|
||||
return Qt::Key_Clear;
|
||||
|
||||
if (key.isDigit()) {
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("%d: got key: %d", __LINE__, key.digitValue());
|
||||
#endif
|
||||
qCDebug(lcQpaKeyMapperKeys, "Got digit key: %d", key.digitValue());
|
||||
return key.digitValue() + Qt::Key_0;
|
||||
}
|
||||
|
||||
if (key.isLetter()) {
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("%d: got key: %d", __LINE__, (key.toUpper().unicode() - 'A'));
|
||||
#endif
|
||||
qCDebug(lcQpaKeyMapperKeys, "Got letter key: %d", (key.toUpper().unicode() - 'A'));
|
||||
return (key.toUpper().unicode() - 'A') + Qt::Key_A;
|
||||
}
|
||||
if (key.isSymbol()) {
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("%d: got key: %d", __LINE__, (key.unicode()));
|
||||
#endif
|
||||
qCDebug(lcQpaKeyMapperKeys, "Got symbol key: %d", (key.unicode()));
|
||||
return key.unicode();
|
||||
}
|
||||
|
||||
for (int i = 0; qt_mac_keyboard_symbols[i].qt_code; i++) {
|
||||
if (qt_mac_keyboard_symbols[i].mac_code == key) {
|
||||
/* To work like Qt for X11 we issue Backtab when Shift + Tab are pressed */
|
||||
if (qt_mac_keyboard_symbols[i].qt_code == Qt::Key_Tab && (modif & Qt::ShiftModifier)) {
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("%d: got key: Qt::Key_Backtab", __LINE__);
|
||||
#endif
|
||||
return Qt::Key_Backtab;
|
||||
}
|
||||
if (auto qtKey = standardKeys.value(key)) {
|
||||
// To work like Qt for X11 we issue Backtab when Shift + Tab are pressed
|
||||
if (qtKey == Qt::Key_Tab && (modifiers & Qt::ShiftModifier)) {
|
||||
qCDebug(lcQpaKeyMapperKeys, "Got key: Qt::Key_Backtab");
|
||||
return Qt::Key_Backtab;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("%d: got key: %s", __LINE__, qt_mac_keyboard_symbols[i].desc);
|
||||
#endif
|
||||
return qt_mac_keyboard_symbols[i].qt_code;
|
||||
qCDebug(lcQpaKeyMapperKeys) << "Got" << qtKey;
|
||||
return qtKey;
|
||||
}
|
||||
|
||||
// Last ditch try to match the scan code
|
||||
if (auto qtKey = virtualKeys.value(virtualKey)) {
|
||||
qCDebug(lcQpaKeyMapperKeys) << "Got scancode" << qtKey;
|
||||
return qtKey;
|
||||
}
|
||||
|
||||
// Check if they belong to key codes in private unicode range
|
||||
if (key >= NSUpArrowFunctionKey && key <= NSModeSwitchFunctionKey) {
|
||||
if (auto qtKey = functionKeys.value(key)) {
|
||||
qCDebug(lcQpaKeyMapperKeys) << "Got" << qtKey;
|
||||
return qtKey;
|
||||
} else if (key >= NSF1FunctionKey && key <= NSF35FunctionKey) {
|
||||
auto functionKey = Qt::Key_F1 + (key.unicode() - NSF1FunctionKey) ;
|
||||
qCDebug(lcQpaKeyMapperKeys) << "Got" << functionKey;
|
||||
return functionKey;
|
||||
}
|
||||
}
|
||||
|
||||
//last ditch try to match the scan code
|
||||
for (int i = 0; qt_mac_keyvkey_symbols[i].qt_code; i++) {
|
||||
if (qt_mac_keyvkey_symbols[i].mac_code == virtualKey) {
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("%d: got key: %s", __LINE__, qt_mac_keyvkey_symbols[i].desc);
|
||||
#endif
|
||||
return qt_mac_keyvkey_symbols[i].qt_code;
|
||||
}
|
||||
}
|
||||
|
||||
// check if they belong to key codes in private unicode range
|
||||
if (key >= 0xf700 && key <= 0xf747) {
|
||||
if (key >= 0xf704 && key <= 0xf726) {
|
||||
return Qt::Key_F1 + (key.unicode() - 0xf704) ;
|
||||
}
|
||||
for (int i = 0; qt_mac_private_unicode[i].qt_code; i++) {
|
||||
if (qt_mac_private_unicode[i].mac_code == key) {
|
||||
return qt_mac_private_unicode[i].qt_code;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//oh well
|
||||
#ifdef DEBUG_KEY_BINDINGS
|
||||
qDebug("Unknown case.. %s:%d %d[%d] %d", __FILE__, __LINE__, key.unicode(), key.toLatin1(), virtualKey);
|
||||
#endif
|
||||
qCDebug(lcQpaKeyMapperKeys, "Unknown case.. %d[%d] %d", key.unicode(), key.toLatin1(), virtualKey);
|
||||
return Qt::Key_unknown;
|
||||
}
|
||||
|
||||
QCocoaKeyMapper::QCocoaKeyMapper()
|
||||
{
|
||||
memset(keyLayout, 0, sizeof(keyLayout));
|
||||
memset(m_keyLayout, 0, sizeof(m_keyLayout));
|
||||
}
|
||||
|
||||
QCocoaKeyMapper::~QCocoaKeyMapper()
|
||||
@ -352,48 +290,49 @@ QCocoaKeyMapper::~QCocoaKeyMapper()
|
||||
|
||||
Qt::KeyboardModifiers QCocoaKeyMapper::queryKeyboardModifiers()
|
||||
{
|
||||
return qt_mac_get_modifiers(GetCurrentKeyModifiers());
|
||||
return fromCarbonModifiers(GetCurrentKeyModifiers());
|
||||
}
|
||||
|
||||
bool QCocoaKeyMapper::updateKeyboard()
|
||||
{
|
||||
const UCKeyboardLayout *uchrData = nullptr;
|
||||
QCFType<TISInputSourceRef> source = TISCopyInputMethodKeyboardLayoutOverride();
|
||||
if (!source)
|
||||
source = TISCopyCurrentKeyboardInputSource();
|
||||
if (keyboard_mode != NullMode && source == currentInputSource) {
|
||||
|
||||
if (m_keyboardMode != NullMode && source == m_currentInputSource)
|
||||
return false;
|
||||
}
|
||||
|
||||
Q_ASSERT(source);
|
||||
CFDataRef data = static_cast<CFDataRef>(TISGetInputSourceProperty(source,
|
||||
kTISPropertyUnicodeKeyLayoutData));
|
||||
uchrData = data ? reinterpret_cast<const UCKeyboardLayout *>(CFDataGetBytePtr(data)) : nullptr;
|
||||
m_currentInputSource = source;
|
||||
m_keyboardKind = LMGetKbdType();
|
||||
m_deadKeyState = 0;
|
||||
|
||||
keyboard_kind = LMGetKbdType();
|
||||
if (uchrData) {
|
||||
keyboard_layout_format = uchrData;
|
||||
keyboard_mode = UnicodeMode;
|
||||
} else {
|
||||
keyboard_layout_format = nullptr;
|
||||
keyboard_mode = NullMode;
|
||||
}
|
||||
currentInputSource = source;
|
||||
keyboard_dead = 0;
|
||||
|
||||
const auto newMode = keyboard_mode;
|
||||
deleteLayouts();
|
||||
keyboard_mode = newMode;
|
||||
|
||||
if (auto data = CFDataRef(TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData))) {
|
||||
const UCKeyboardLayout *uchrData = reinterpret_cast<const UCKeyboardLayout *>(CFDataGetBytePtr(data));
|
||||
Q_ASSERT(uchrData);
|
||||
m_keyboardLayoutFormat = uchrData;
|
||||
m_keyboardMode = UnicodeMode;
|
||||
} else {
|
||||
m_keyboardLayoutFormat = nullptr;
|
||||
m_keyboardMode = NullMode;
|
||||
}
|
||||
|
||||
qCDebug(lcQpaKeyMapper) << "Updated keyboard to"
|
||||
<< QString::fromCFString(CFStringRef(TISGetInputSourceProperty(
|
||||
m_currentInputSource, kTISPropertyLocalizedName)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QCocoaKeyMapper::deleteLayouts()
|
||||
{
|
||||
keyboard_mode = NullMode;
|
||||
m_keyboardMode = NullMode;
|
||||
for (int i = 0; i < 255; ++i) {
|
||||
if (keyLayout[i]) {
|
||||
delete keyLayout[i];
|
||||
keyLayout[i] = nullptr;
|
||||
if (m_keyLayout[i]) {
|
||||
delete m_keyLayout[i];
|
||||
m_keyLayout[i] = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -404,69 +343,98 @@ void QCocoaKeyMapper::clearMappings()
|
||||
updateKeyboard();
|
||||
}
|
||||
|
||||
static constexpr Qt::KeyboardModifiers modifierCombinations[] = {
|
||||
Qt::NoModifier, // 0
|
||||
Qt::ShiftModifier, // 1
|
||||
Qt::ControlModifier, // 2
|
||||
Qt::ControlModifier | Qt::ShiftModifier, // 3
|
||||
Qt::AltModifier, // 4
|
||||
Qt::AltModifier | Qt::ShiftModifier, // 5
|
||||
Qt::AltModifier | Qt::ControlModifier, // 6
|
||||
Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 7
|
||||
Qt::MetaModifier, // 8
|
||||
Qt::MetaModifier | Qt::ShiftModifier, // 9
|
||||
Qt::MetaModifier | Qt::ControlModifier, // 10
|
||||
Qt::MetaModifier | Qt::ControlModifier | Qt::ShiftModifier, // 11
|
||||
Qt::MetaModifier | Qt::AltModifier, // 12
|
||||
Qt::MetaModifier | Qt::AltModifier | Qt::ShiftModifier, // 13
|
||||
Qt::MetaModifier | Qt::AltModifier | Qt::ControlModifier, // 14
|
||||
Qt::MetaModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 15
|
||||
};
|
||||
|
||||
/*
|
||||
Updates the key map for the given \macVirtualKey by applying
|
||||
all possible modifier combinations.
|
||||
*/
|
||||
void QCocoaKeyMapper::updateKeyMap(unsigned short macVirtualKey, QChar unicodeKey)
|
||||
{
|
||||
updateKeyboard();
|
||||
|
||||
if (keyLayout[macVirtualKey])
|
||||
if (m_keyLayout[macVirtualKey])
|
||||
return;
|
||||
|
||||
UniCharCount buffer_size = 10;
|
||||
UniChar buffer[buffer_size];
|
||||
keyLayout[macVirtualKey] = new KeyboardLayoutItem;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
UniCharCount out_buffer_size = 0;
|
||||
keyLayout[macVirtualKey]->qtKey[i] = 0;
|
||||
qCDebug(lcQpaKeyMapper, "Updating key map for virtual key = 0x%02x!", (uint)macVirtualKey);
|
||||
|
||||
UniCharCount maxStringLength = 10;
|
||||
UniChar unicodeString[maxStringLength];
|
||||
m_keyLayout[macVirtualKey] = new KeyboardLayoutItem;
|
||||
|
||||
const UInt32 keyModifier = ((qt_mac_get_mac_modifiers(ModsTbl[i]) >> 8) & 0xFF);
|
||||
OSStatus err = UCKeyTranslate(keyboard_layout_format, macVirtualKey, kUCKeyActionDown, keyModifier,
|
||||
keyboard_kind, 0, &keyboard_dead, buffer_size, &out_buffer_size, buffer);
|
||||
if (err == noErr && out_buffer_size) {
|
||||
const QChar unicode(buffer[0]);
|
||||
int qtkey = qt_mac_get_key(keyModifier, unicode, macVirtualKey);
|
||||
if (qtkey == Qt::Key_unknown)
|
||||
qtkey = unicode.unicode();
|
||||
keyLayout[macVirtualKey]->qtKey[i] = qtkey;
|
||||
} else {
|
||||
int qtkey = qt_mac_get_key(keyModifier, unicodeKey, macVirtualKey);
|
||||
if (qtkey == Qt::Key_unknown)
|
||||
qtkey = unicodeKey.unicode();
|
||||
keyLayout[macVirtualKey]->qtKey[i] = qtkey;
|
||||
}
|
||||
}
|
||||
#ifdef DEBUG_KEY_MAPS
|
||||
qDebug("updateKeyMap for virtual key = 0x%02x!", (uint)macVirtualKey);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
qDebug(" [%d] (%d,0x%02x,'%c')", i,
|
||||
keyLayout[macVirtualKey]->qtKey[i],
|
||||
keyLayout[macVirtualKey]->qtKey[i],
|
||||
keyLayout[macVirtualKey]->qtKey[i]);
|
||||
UniCharCount actualStringLength = 0;
|
||||
m_keyLayout[macVirtualKey]->qtKey[i] = 0;
|
||||
|
||||
auto qtModifiers = modifierCombinations[i];
|
||||
auto carbonModifiers = toCarbonModifiers(qtModifiers);
|
||||
const UInt32 modifierKeyState = (carbonModifiers >> 8) & 0xFF;
|
||||
OSStatus err = UCKeyTranslate(m_keyboardLayoutFormat, macVirtualKey,
|
||||
kUCKeyActionDown, modifierKeyState, m_keyboardKind, OptionBits(0),
|
||||
&m_deadKeyState, maxStringLength, &actualStringLength, unicodeString);
|
||||
|
||||
// Use translated unicode key if valid
|
||||
if (err == noErr && actualStringLength)
|
||||
unicodeKey = QChar(unicodeString[0]);
|
||||
|
||||
int qtkey = toKeyCode(unicodeKey, macVirtualKey, modifierKeyState);
|
||||
if (qtkey == Qt::Key_unknown)
|
||||
qtkey = unicodeKey.unicode();
|
||||
|
||||
m_keyLayout[macVirtualKey]->qtKey[i] = qtkey;
|
||||
|
||||
qCDebug(lcQpaKeyMapper, " [%d] (%d,0x%02x,'%c')", i, qtkey, qtkey, qtkey);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QList<int> QCocoaKeyMapper::possibleKeys(const QKeyEvent *event) const
|
||||
{
|
||||
QList<int> ret;
|
||||
const_cast<QCocoaKeyMapper *>(this)->updateKeyMap(event->nativeVirtualKey(), QChar(event->key()));
|
||||
|
||||
KeyboardLayoutItem *kbItem = keyLayout[event->nativeVirtualKey()];
|
||||
auto virtualKey = event->nativeVirtualKey();
|
||||
const_cast<QCocoaKeyMapper *>(this)->updateKeyMap(virtualKey, QChar(event->key()));
|
||||
KeyboardLayoutItem *keyLayout = m_keyLayout[virtualKey];
|
||||
|
||||
if (!kbItem) // Key is not in any keyboard layout (e.g. eisu-key on Japanese keyboard)
|
||||
if (!keyLayout) // Key is not in any keyboard layout (e.g. eisu-key on Japanese keyboard)
|
||||
return ret;
|
||||
|
||||
int baseKey = kbItem->qtKey[0];
|
||||
Qt::KeyboardModifiers keyMods = event->modifiers();
|
||||
int baseKey = keyLayout->qtKey[Qt::NoModifier];
|
||||
auto eventModifiers = event->modifiers();
|
||||
|
||||
ret << int(baseKey + keyMods); // The base key is _always_ valid, of course
|
||||
// The base key is always valid
|
||||
ret << int(baseKey + eventModifiers);
|
||||
|
||||
for (int i = 1; i < 8; ++i) {
|
||||
Qt::KeyboardModifiers neededMods = ModsTbl[i];
|
||||
int key = kbItem->qtKey[i];
|
||||
if (key && key != baseKey && ((keyMods & neededMods) == neededMods)) {
|
||||
ret << int(key + (keyMods & ~neededMods));
|
||||
}
|
||||
int keyAfterApplyingModifiers = keyLayout->qtKey[i];
|
||||
if (!keyAfterApplyingModifiers)
|
||||
continue;
|
||||
if (keyAfterApplyingModifiers == baseKey)
|
||||
continue;
|
||||
|
||||
// Include key if event modifiers includes, or matches
|
||||
// perfectly, the current candidate modifiers.
|
||||
auto candidateModifiers = modifierCombinations[i];
|
||||
if ((eventModifiers & candidateModifiers) == candidateModifiers)
|
||||
ret << int(keyAfterApplyingModifiers + (eventModifiers & ~candidateModifiers));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user