diff --git a/OSBindings/Qt/ClockSignal.pro b/OSBindings/Qt/ClockSignal.pro index 65055ae4d..c31d162d6 100644 --- a/OSBindings/Qt/ClockSignal.pro +++ b/OSBindings/Qt/ClockSignal.pro @@ -12,7 +12,13 @@ CONFIG += object_parallel_to_source INCLUDEPATH += $$[QT_INSTALL_PREFIX]/src/3rdparty/zlib LIBS += -lz -# Add flags (i) to identify that this is a Qt build; and +# If targetting X11, link against that. +linux { + QT += x11extras + LIBS += -lX11 +} + +# Add flags (i) to identify that this is a Qt build; and # (ii) to disable asserts in release builds. DEFINES += TARGET_QT QMAKE_CXXFLAGS_RELEASE += -DNDEBUG @@ -134,10 +140,11 @@ SOURCES += \ $$SRC/Storage/Tape/Formats/*.cpp \ $$SRC/Storage/Tape/Parsers/*.cpp \ \ - main.cpp \ - mainwindow.cpp \ - scantargetwidget.cpp \ - timer.cpp + main.cpp \ + mainwindow.cpp \ + scantargetwidget.cpp \ + timer.cpp \ + keyboard.cpp HEADERS += \ $$SRC/Activity/*.hpp \ @@ -275,12 +282,13 @@ HEADERS += \ $$SRC/Storage/Tape/Formats/*.hpp \ $$SRC/Storage/Tape/Parsers/*.hpp \ \ - audiobuffer.h \ - functionthread.h \ - mainwindow.h \ - scantargetwidget.h \ - settings.h \ - timer.h + audiobuffer.h \ + functionthread.h \ + mainwindow.h \ + scantargetwidget.h \ + settings.h \ + keyboard.h \ + timer.h FORMS += \ mainwindow.ui diff --git a/OSBindings/Qt/keyboard.cpp b/OSBindings/Qt/keyboard.cpp new file mode 100644 index 000000000..48fc00c4b --- /dev/null +++ b/OSBindings/Qt/keyboard.cpp @@ -0,0 +1,202 @@ +#include "keyboard.h" + +#include +#include + +// Qt is the worst. +// +// Assume your keyboard has a key labelled both . and >, as on US and UK keyboards. Call it the dot key. +// Perform the following: +// 1. press dot key; +// 2. press shift key; +// 3. release dot key; +// 4. release shift key. +// +// Per empirical testing, and key repeat aside, on both macOS and Ubuntu 19.04 that sequence will result in +// _three_ keypress events, but only _two_ key release events. You'll get presses for Qt::Key_Period, Qt::Key_Greater +// and Qt::Key_Shift. You'll get releases only for Qt::Key_Greater and Qt::Key_Shift. +// +// How can you detect at runtime that Key_Greater and Key_Period are the same physical key? +// +// You can't. On Ubuntu they have the same values for QKeyEvent::nativeScanCode(), which are unique to the key, +// but they have different ::nativeVirtualKey()s. +// +// On macOS they have the same ::nativeScanCode() only because on macOS [almost] all keys have the same +// ::nativeScanCode(). So that's not usable. They have the same ::nativeVirtualKey()s, but since that isn't true +// on Ubuntu, that's also not usable. +// +// So how can you track the physical keys on a keyboard via Qt? +// +// You can't. Qt is the worst. SDL doesn't have this problem, including in X11, but I don't want the non-Qt dependency. + +#ifdef Q_OS_LINUX +#define HAS_X11 +#endif + +#ifdef HAS_X11 +#include + +#include +#include +#endif + +KeyboardMapper::KeyboardMapper() { +#ifdef HAS_X11 + struct DesiredMapping { + KeySym source = 0; + Inputs::Keyboard::Key destination; + }; + + using Key = Inputs::Keyboard::Key; + constexpr DesiredMapping mappings[] = { + {XK_Escape, Key::Escape}, + {XK_F1, Key::F1}, {XK_F2, Key::F2}, {XK_F3, Key::F3}, {XK_F4, Key::F4}, {XK_F5, Key::F5}, + {XK_F6, Key::F6}, {XK_F7, Key::F7}, {XK_F8, Key::F8}, {XK_F9, Key::F9}, {XK_F10, Key::F10}, + {XK_F11, Key::F11}, {XK_F12, Key::F12}, + {XK_Sys_Req, Key::PrintScreen}, + {XK_Scroll_Lock, Key::ScrollLock}, + {XK_Pause, Key::Pause}, + + {XK_grave, Key::BackTick}, + {XK_1, Key::k1}, {XK_2, Key::k2}, {XK_3, Key::k3}, {XK_4, Key::k4}, {XK_5, Key::k5}, + {XK_6, Key::k6}, {XK_7, Key::k7}, {XK_8, Key::k8}, {XK_9, Key::k9}, {XK_0, Key::k0}, + {XK_minus, Key::Hyphen}, + {XK_equal, Key::Equals}, + {XK_BackSpace, Key::Backspace}, + + {XK_Tab, Key::Tab}, + {XK_Q, Key::Q}, {XK_W, Key::W}, {XK_E, Key::E}, {XK_R, Key::R}, {XK_T, Key::T}, + {XK_Y, Key::Y}, {XK_U, Key::U}, {XK_I, Key::I}, {XK_O, Key::O}, {XK_P, Key::P}, + {XK_bracketleft, Key::OpenSquareBracket}, + {XK_bracketright, Key::CloseSquareBracket}, + {XK_backslash, Key::Backslash}, + + {XK_Caps_Lock, Key::CapsLock}, + {XK_A, Key::A}, {XK_S, Key::S}, {XK_D, Key::D}, {XK_F, Key::F}, {XK_G, Key::G}, + {XK_H, Key::H}, {XK_J, Key::J}, {XK_K, Key::K}, {XK_L, Key::L}, + {XK_semicolon, Key::Semicolon}, + {XK_apostrophe, Key::Quote}, + {XK_Return, Key::Enter}, + + {XK_Shift_L, Key::LeftShift}, + {XK_Z, Key::Z}, {XK_X, Key::X}, {XK_C, Key::C}, {XK_V, Key::V}, + {XK_B, Key::B}, {XK_N, Key::N}, {XK_M, Key::M}, + {XK_comma, Key::Comma}, + {XK_period, Key::FullStop}, + {XK_slash, Key::ForwardSlash}, + {XK_Shift_R, Key::RightShift}, + + {XK_Control_L, Key::LeftControl}, + {XK_Control_R, Key::RightControl}, + {XK_Alt_L, Key::LeftOption}, + {XK_Alt_R, Key::RightOption}, + {XK_Meta_L, Key::LeftMeta}, + {XK_Meta_R, Key::RightMeta}, + {XK_space, Key::Space}, + + {XK_Left, Key::Left}, {XK_Right, Key::Right}, {XK_Up, Key::Up}, {XK_Down, Key::Down}, + + {XK_Insert, Key::Insert}, + {XK_Delete, Key::Delete}, + {XK_Home, Key::Home}, + {XK_End, Key::End}, + + {XK_Num_Lock, Key::NumLock}, + + {XK_KP_Divide, Key::KeypadSlash}, + {XK_KP_Multiply, Key::KeypadAsterisk}, + {XK_KP_Delete, Key::KeypadDelete}, + {XK_KP_7, Key::Keypad7}, {XK_KP_8, Key::Keypad8}, {XK_KP_9, Key::Keypad9}, {XK_KP_Add, Key::KeypadPlus}, + {XK_KP_4, Key::Keypad4}, {XK_KP_5, Key::Keypad5}, {XK_KP_6, Key::Keypad6}, {XK_KP_Subtract, Key::KeypadMinus}, + {XK_KP_1, Key::Keypad1}, {XK_KP_2, Key::Keypad2}, {XK_KP_3, Key::Keypad3}, {XK_KP_Enter, Key::KeypadEnter}, + {XK_KP_0, Key::Keypad0}, + {XK_KP_Decimal, Key::KeypadDecimalPoint}, + {XK_KP_Equal, Key::KeypadEquals}, + + {XK_Help, Key::Help}, + + {} + }; + + // Extra level of nonsense here: + // + // (1) assume a PC-esque keyboard, with a close-to-US/UK layout; + // (2) from there, use any of the X11 KeySyms I'd expect to be achievable from each physical key to + // look up the X11 KeyCode; + // (3) henceforth, map from X11 KeyCode to the Inputs::Keyboard::Key. + const DesiredMapping *mapping = mappings; + while(mapping->source != 0) { + const auto code = XKeysymToKeycode(QX11Info::display(), mapping->source); + keyByKeySym[code] = mapping->destination; + ++mapping; + } +#endif +} + +std::optional KeyboardMapper::keyForEvent(QKeyEvent *event) { +#ifdef HAS_X11 + if(QGuiApplication::platformName() == QLatin1String("xcb")) { + const auto key = keyByKeySym.find(event->nativeScanCode()); + if(key == keyByKeySym.end()) return std::nullopt; + return key->second; + } +#endif + + // Fall back on a limited, faulty adaptation. +#define BIND2(qtKey, clkKey) case Qt::qtKey: return Inputs::Keyboard::Key::clkKey; +#define BIND(key) BIND2(Key_##key, key) + + switch(event->key()) { + default: return {}; + + BIND(Escape); + BIND(F1); BIND(F2); BIND(F3); BIND(F4); BIND(F5); BIND(F6); + BIND(F7); BIND(F8); BIND(F9); BIND(F10); BIND(F11); BIND(F12); + BIND2(Key_Print, PrintScreen); + BIND(ScrollLock); BIND(Pause); + + BIND2(Key_AsciiTilde, BackTick); + BIND2(Key_1, k1); BIND2(Key_2, k2); BIND2(Key_3, k3); BIND2(Key_4, k4); BIND2(Key_5, k5); + BIND2(Key_6, k6); BIND2(Key_7, k7); BIND2(Key_8, k8); BIND2(Key_9, k9); BIND2(Key_0, k0); + BIND2(Key_Minus, Hyphen); + BIND2(Key_Plus, Equals); + BIND(Backspace); + + BIND(Tab); BIND(Q); BIND(W); BIND(E); BIND(R); BIND(T); BIND(Y); + BIND(U); BIND(I); BIND(O); BIND(P); + BIND2(Key_BraceLeft, OpenSquareBracket); + BIND2(Key_BraceRight, CloseSquareBracket); + BIND(Backslash); + + BIND(CapsLock); BIND(A); BIND(S); BIND(D); BIND(F); BIND(G); + BIND(H); BIND(J); BIND(K); BIND(L); + BIND(Semicolon); + BIND2(Key_Apostrophe, Quote); + BIND2(Key_QuoteDbl, Quote); + // TODO: something to hash? + BIND2(Key_Return, Enter); + + BIND2(Key_Shift, LeftShift); + BIND(Z); BIND(X); BIND(C); BIND(V); + BIND(B); BIND(N); BIND(M); + BIND(Comma); + BIND2(Key_Period, FullStop); + BIND2(Key_Slash, ForwardSlash); + // Omitted: right shift. + + BIND2(Key_Control, LeftControl); + BIND2(Key_Alt, LeftOption); + BIND2(Key_Meta, LeftMeta); + BIND(Space); + BIND2(Key_AltGr, RightOption); + + BIND(Left); BIND(Right); BIND(Up); BIND(Down); + + BIND(Insert); BIND(Home); BIND(PageUp); BIND(Delete); BIND(End); BIND(PageDown); + + BIND(NumLock); + } + +#undef BIND +#undef BIND2 +} diff --git a/OSBindings/Qt/keyboard.h b/OSBindings/Qt/keyboard.h new file mode 100644 index 000000000..c0bd1b9b0 --- /dev/null +++ b/OSBindings/Qt/keyboard.h @@ -0,0 +1,18 @@ +#ifndef KEYBOARD_H +#define KEYBOARD_H + +#include +#include +#include +#include "../../Inputs/Keyboard.hpp" + +class KeyboardMapper { + public: + KeyboardMapper(); + std::optional keyForEvent(QKeyEvent *); + + private: + std::map keyByKeySym; +}; + +#endif // MAINWINDOW_H diff --git a/OSBindings/Qt/mainwindow.cpp b/OSBindings/Qt/mainwindow.cpp index 6836844f1..8b96cbcdb 100644 --- a/OSBindings/Qt/mainwindow.cpp +++ b/OSBindings/Qt/mainwindow.cpp @@ -851,176 +851,10 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event) { processEvent(event); } -// Qt is the worst. -// -// Assume your keyboard has a key labelled both . and >, as they do on US and UK keyboards. Call it the dot key. -// Perform the following: -// 1. press dot key; -// 2. press shift key; -// 3. release dot key; -// 4. release shift key. -// -// Per empirical testing, and key repeat aside, on both macOS and Ubuntu 19.04 that sequence will result in -// _three_ keypress events, but only _two_ key release events. You'll get presses for Qt::Key_Period, Qt::Key_Greater -// and Qt::Key_Shift. You'll get releases only for Qt::Key_Greater and Qt::Key_Shift. -// -// How can you detect at runtime that Key_Greater and Key_Period are the same physical key? -// -// You can't. On Ubuntu they have the same values for QKeyEvent::nativeScanCode(), which are unique to the key, -// but they have different ::nativeVirtualKey()s. -// -// On macOS they have the same ::nativeScanCode() only because on macOS [almost] all keys have the same -// ::nativeScanCode(). So that's not usable. They have the same ::nativeVirtualKey()s, but since that isn't true -// on Ubuntu, that's also not usable. -// -// So how can you track the physical keys on a keyboard via Qt? -// -// You can't. Qt is the worst. SDL doesn't have this problem, including in X11, but I'm not sure I want the extra -// dependency. I may need to reassess. - -std::optional MainWindow::keyForEvent(QKeyEvent *event) { - // Workaround for X11: assume PC-esque mapping from ::nativeScanCode to symbols. - // - // Yucky, ugly, harcoded yuck. TODO: work out how `xmodmap -pke` seems to derive these codes at runtime. - - if(QGuiApplication::platformName() == QLatin1String("xcb")) { -#define BIND(code, key) case code: return Inputs::Keyboard::Key::key; - - switch(event->nativeScanCode()) { - default: qDebug() << "Unmapped" << event->nativeScanCode(); return {}; - - BIND(1, Escape); - BIND(67, F1); BIND(68, F2); BIND(69, F3); BIND(70, F4); BIND(71, F5); - BIND(72, F6); BIND(73, F7); BIND(74, F8); BIND(75, F9); BIND(76, F10); - BIND(95, F11); BIND(96, F12); - BIND(107, PrintScreen); - BIND(78, ScrollLock); - BIND(127, Pause); - - BIND(49, BackTick); - BIND(10, k1); BIND(11, k2); BIND(12, k3); BIND(13, k4); BIND(14, k5); - BIND(15, k6); BIND(16, k7); BIND(17, k8); BIND(18, k9); BIND(19, k0); - BIND(20, Hyphen); - BIND(21, Equals); - BIND(22, Backspace); - - BIND(23, Tab); - BIND(24, Q); BIND(25, W); BIND(26, E); BIND(27, R); BIND(28, T); - BIND(29, Y); BIND(30, U); BIND(31, I); BIND(32, O); BIND(33, P); - BIND(34, OpenSquareBracket); - BIND(35, CloseSquareBracket); - BIND(51, Backslash); - - BIND(66, CapsLock); - BIND(38, A); BIND(39, S); BIND(40, D); BIND(41, F); BIND(42, G); - BIND(43, H); BIND(44, J); BIND(45, K); BIND(46, L); - BIND(47, Semicolon); - BIND(48, Quote); - BIND(36, Enter); - - BIND(50, LeftShift); - BIND(52, Z); BIND(53, X); BIND(54, C); BIND(55, V); - BIND(56, B); BIND(57, N); BIND(58, M); - BIND(59, Comma); - BIND(60, FullStop); - BIND(61, ForwardSlash); - BIND(62, RightShift); - - BIND(105, LeftControl); - BIND(204, LeftOption); - BIND(205, LeftMeta); - BIND(65, Space); - BIND(108, RightOption); - - BIND(113, Left); BIND(114, Right); BIND(111, Up); BIND(116, Down); - - BIND(118, Insert); - BIND(119, Delete); - BIND(110, Home); - BIND(115, End); - - BIND(77, NumLock); - - BIND(106, KeypadSlash); - BIND(63, KeypadAsterisk); - BIND(91, KeypadDelete); - BIND(79, Keypad7); BIND(80, Keypad8); BIND(81, Keypad9); BIND(86, KeypadPlus); - BIND(83, Keypad4); BIND(84, Keypad5); BIND(85, Keypad6); BIND(82, KeypadMinus); - BIND(87, Keypad1); BIND(88, Keypad2); BIND(89, Keypad3); BIND(104, KeypadEnter); - BIND(90, Keypad0); - BIND(129, KeypadDecimalPoint); - BIND(125, KeypadEquals); - - BIND(146, Help); - } - -#undef BIND - } - - // Fall back on a limited, faulty adaptation. -#define BIND2(qtKey, clkKey) case Qt::qtKey: return Inputs::Keyboard::Key::clkKey; -#define BIND(key) BIND2(Key_##key, key) - - switch(event->key()) { - default: return {}; - - BIND(Escape); - BIND(F1); BIND(F2); BIND(F3); BIND(F4); BIND(F5); BIND(F6); - BIND(F7); BIND(F8); BIND(F9); BIND(F10); BIND(F11); BIND(F12); - BIND2(Key_Print, PrintScreen); - BIND(ScrollLock); BIND(Pause); - - BIND2(Key_AsciiTilde, BackTick); - BIND2(Key_1, k1); BIND2(Key_2, k2); BIND2(Key_3, k3); BIND2(Key_4, k4); BIND2(Key_5, k5); - BIND2(Key_6, k6); BIND2(Key_7, k7); BIND2(Key_8, k8); BIND2(Key_9, k9); BIND2(Key_0, k0); - BIND2(Key_Minus, Hyphen); - BIND2(Key_Plus, Equals); - BIND(Backspace); - - BIND(Tab); BIND(Q); BIND(W); BIND(E); BIND(R); BIND(T); BIND(Y); - BIND(U); BIND(I); BIND(O); BIND(P); - BIND2(Key_BraceLeft, OpenSquareBracket); - BIND2(Key_BraceRight, CloseSquareBracket); - BIND(Backslash); - - BIND(CapsLock); BIND(A); BIND(S); BIND(D); BIND(F); BIND(G); - BIND(H); BIND(J); BIND(K); BIND(L); - BIND(Semicolon); - BIND2(Key_Apostrophe, Quote); - BIND2(Key_QuoteDbl, Quote); - // TODO: something to hash? - BIND2(Key_Return, Enter); - - BIND2(Key_Shift, LeftShift); - BIND(Z); BIND(X); BIND(C); BIND(V); - BIND(B); BIND(N); BIND(M); - BIND(Comma); - BIND2(Key_Period, FullStop); - BIND2(Key_Slash, ForwardSlash); - // Omitted: right shift. - - BIND2(Key_Control, LeftControl); - BIND2(Key_Alt, LeftOption); - BIND2(Key_Meta, LeftMeta); - BIND(Space); - BIND2(Key_AltGr, RightOption); - - BIND(Left); BIND(Right); BIND(Up); BIND(Down); - - BIND(Insert); BIND(Home); BIND(PageUp); BIND(Delete); BIND(End); BIND(PageDown); - - BIND(NumLock); - } - -#undef BIND -#undef BIND2 -} - - bool MainWindow::processEvent(QKeyEvent *event) { if(!machine) return true; - const auto key = keyForEvent(event); + const auto key = keyMapper.keyForEvent(event); if(!key) return true; const bool isPressed = event->type() == QEvent::KeyPress; diff --git a/OSBindings/Qt/mainwindow.h b/OSBindings/Qt/mainwindow.h index 441612213..ec25e1470 100644 --- a/OSBindings/Qt/mainwindow.h +++ b/OSBindings/Qt/mainwindow.h @@ -12,6 +12,7 @@ #include "timer.h" #include "ui_mainwindow.h" #include "functionthread.h" +#include "keyboard.h" #include "../../Analyser/Static/StaticAnalyser.hpp" #include "../../Machines/Utility/MachineForTarget.hpp" @@ -144,7 +145,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat QMenu *inputMenu = nullptr; - std::optional keyForEvent(QKeyEvent *); + KeyboardMapper keyMapper; void register_led(const std::string &) override; void set_led_status(const std::string &, bool) override;