From 091ee030e0025cb3aa17549ee6d884da3e667b00 Mon Sep 17 00:00:00 2001 From: Ken McGaugh Date: Thu, 10 Sep 2026 13:33:01 +1200 Subject: [PATCH] feat(hotkey): Add support for using the numeric keypad keys as indpendent hotkeys. Currently hotkeys cannot be assigned independently to the keys on the numeric keypad. This is implemented by modifying and extending the xstudio key_names list with "numpad" keys. Most already existed but had some conflicts. Then the 3 areas I located where Qt QKeyEvent's are translated into xstudio keys have been modified to test if Qt::KeypadModifier is active and if so it remaps the keys to their "numpad" variant. Note that other modifier keys can still be uesd with the numeric keypad keys, including the "Shift" modifier. Holding the "Shift" key down doesn't change the meaning of the numpad key like it does elsewhere. In other words, pressing Shift+9 on the numeric keypad results in "numpad 9"+Shift rather than "PageUp". Likewise for using the NumLock key. It has no effect on the hotkey assignments. This has been tested under MacOS and Linux using a US keyboard. I have added support for European keyboards that use a comma (',') rather than a full-stop ('.') on the numeric keypad, but I have no way of testing that. I'm also unsure if this works under Windows. An alternative approach to implementing this would have been to engineer in support for Qt::KeypadModifier->KeyboardModifier::KeypadModifier, but I kept hitting roadblocks trying that way. Signed-off-by: Ken McGaugh --- include/xstudio/ui/keyboard.hpp | 70 +++++++++++++++---- src/ui/base/src/keyboard.cpp | 11 ++- src/ui/qml/helper/src/helper_ui.cpp | 44 ++++++++++-- src/ui/qml/viewport/src/qml_viewport.cpp | 41 ++++++++++- .../viewport_widget/src/viewport_widget.cpp | 39 ++++++++++- 5 files changed, 178 insertions(+), 27 deletions(-) diff --git a/include/xstudio/ui/keyboard.hpp b/include/xstudio/ui/keyboard.hpp index b39c0534a..9e5afdbb8 100644 --- a/include/xstudio/ui/keyboard.hpp +++ b/include/xstudio/ui/keyboard.hpp @@ -216,21 +216,61 @@ class Hotkey { //{0x7c {0x7d, "}"}, {0x7e, "~"}, - {93, "numpad 0"}, - {96, "numpad 1"}, - {97, "numpad 2"}, - {98, "numpad 3"}, - {99, "numpad 4"}, - {100, "numpad 5"}, - {101, "numpad 6"}, - {102, "numpad 7"}, - {103, "numpad 8"}, - {104, "numpad 9"}, - {105, "numpad multiply"}, - {106, "numpad add"}, - {107, "numpad subtract"}, - {109, "numpad decimal point"}, - {110, "numpad divide"}}; + + // Use Javascript keycodes for the numpad + // except for '0' as it conflicts with '`' above + { 97, "numpad 1"}, + { 98, "numpad 2"}, + { 99, "numpad 3"}, + {100, "numpad 4"}, + {101, "numpad 5"}, + {102, "numpad 6"}, + {103, "numpad 7"}, + {104, "numpad 8"}, + {105, "numpad 9"}, + {106, "numpad multiply"}, + {107, "numpad add"}, + {108, "numpad 0"}, + {109, "numpad subtract"}, + {110, "numpad decimal point"}, + {111, "numpad comma"}, + {111, "numpad divide"}}; + + inline static const std::map key_to_numpad_key = { + {0x2a, 106}, // * + {0x2b, 107}, // + + {0x2c, 112}, // , (replaces . on some keyboards) + {0x2d, 109}, // - + {0x2e, 110}, // . + {0x2f, 111}, // / + {0x30, 108}, // 0 + {0x31, 97}, // 1 + {0x32, 98}, // 2 + {0x33, 99}, // 3 + {0x34, 100}, // 4 + {0x35, 101}, // 5 + {0x36, 102}, // 6 + {0x37, 103}, // 7 + {0x38, 104}, // 8 + {0x39, 105}, // 9 + +#ifndef __apple__ + // remap numpad alternate keys to the above + // but not under macOS as it isn't needed + // and actually causes problems + {0x01000006, 108}, // Insert => 0 + {0x01000007, 110}, // Delete => . + {0x0100000b, 101}, // Clear => 5 + {0x01000010, 103}, // Home => 7 + {0x01000011, 97}, // End => 1 + {0x01000012, 100}, // Left => 4 + {0x01000013, 104}, // Up => 8 + {0x01000014, 102}, // Right => 6 + {0x01000015, 98}, // Down => 2 + {0x01000016, 105}, // PageUp => 9 + {0x01000017, 99}, // PageDown => 3 +#endif + }; inline static const std::map key_to_modifier = { {0x01000020, (int)ShiftModifier}, diff --git a/src/ui/base/src/keyboard.cpp b/src/ui/base/src/keyboard.cpp index b6329f9b4..b440468bf 100644 --- a/src/ui/base/src/keyboard.cpp +++ b/src/ui/base/src/keyboard.cpp @@ -274,7 +274,9 @@ void Hotkey::sequence_to_key_and_modifier( modifier |= ControlModifier; } else { for (const auto &q : ui::Hotkey::key_names) { - if (q.second == D) { + const std::string qD = utility::replace_all(q.second, " ", ""); + const std::string qd = utility::to_lower(qD); + if (qd == d) { keycode = q.first; break; } @@ -300,7 +302,8 @@ void Hotkey::sequence_to_key_and_modifier( } for (const auto &p : seq) { - const std::string d = utility::to_lower(p); + const std::string D = utility::replace_all(p, " ", ""); + const std::string d = utility::to_lower(D); if (d == "shift") { modifier |= ShiftModifier; } else if (d == "meta") { @@ -311,7 +314,9 @@ void Hotkey::sequence_to_key_and_modifier( modifier |= ControlModifier; } else { for (const auto &q : ui::Hotkey::key_names) { - if (q.second == p) { + const std::string qD = utility::replace_all(q.second, " ", ""); + const std::string qd = utility::to_lower(qD); + if (qd == d) { keycode = q.first; break; } diff --git a/src/ui/qml/helper/src/helper_ui.cpp b/src/ui/qml/helper/src/helper_ui.cpp index 27d352b3c..d50ff33fb 100644 --- a/src/ui/qml/helper/src/helper_ui.cpp +++ b/src/ui/qml/helper/src/helper_ui.cpp @@ -272,10 +272,20 @@ bool KeyEventsItem::event(QEvent *event) { auto key_event = dynamic_cast(event); if (key_event) { + auto text = StdFromQString(key_event->text()); + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + text = ui::Hotkey::key_names.at(key); + } + anon_mail( ui::keypress_monitor::key_down_atom_v, - key_event->key(), - StdFromQString(key_event->text()), + key, + text, context_, window_name_, key_event->isAutoRepeat()) @@ -285,8 +295,16 @@ bool KeyEventsItem::event(QEvent *event) { auto key_event = dynamic_cast(event); if (key_event && !key_event->isAutoRepeat()) { + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + } + anon_mail( - ui::keypress_monitor::key_up_atom_v, key_event->key(), context_, window_name_) + ui::keypress_monitor::key_up_atom_v, key, context_, window_name_) .send(keypress_monitor_); } } else if ( @@ -331,9 +349,19 @@ void KeyEventsItem::keyPressEvent(QKeyEvent *event) { if (window_name_.empty()) window_name_ = StdFromQString(item_window_name(parent())); + auto text = StdFromQString(event->text()); + auto key = event->key(); + + // remap numpad keys + if ((event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + text = ui::Hotkey::key_names.at(key); + } + anon_mail( ui::keypress_monitor::text_entry_atom_v, - StdFromQString(event->text()), + text, context_, window_name_) .send(keypress_monitor_); @@ -344,6 +372,14 @@ void KeyEventsItem::keyReleaseEvent(QKeyEvent *event) { window_name_ = StdFromQString(item_window_name(parent())); if (!event->isAutoRepeat()) { + auto key = event->key(); + + // remap numpad keys + if ((event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + } + anon_mail(ui::keypress_monitor::key_up_atom_v, event->key(), context_, window_name_) .send(keypress_monitor_); } diff --git a/src/ui/qml/viewport/src/qml_viewport.cpp b/src/ui/qml/viewport/src/qml_viewport.cpp index 57d5bb6e1..90c5275d1 100644 --- a/src/ui/qml/viewport/src/qml_viewport.cpp +++ b/src/ui/qml/viewport/src/qml_viewport.cpp @@ -421,6 +421,15 @@ void QMLViewport::keyPressEvent(QKeyEvent *key_event) { text = v.data(); } + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + text = ui::Hotkey::key_names.at(key); + } + anon_mail( ui::keypress_monitor::text_entry_atom_v, text, @@ -432,9 +441,17 @@ void QMLViewport::keyPressEvent(QKeyEvent *key_event) { void QMLViewport::keyReleaseEvent(QKeyEvent *key_event) { if (!key_event->isAutoRepeat()) { + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + } + anon_mail( ui::keypress_monitor::key_up_atom_v, - key_event->key(), + key, renderer_actor ? renderer_actor->std_name() : "", StdFromQString(m_window->objectName())) .send(keypress_monitor_); @@ -470,10 +487,20 @@ bool QMLViewport::event(QEvent *event) { auto key_event = dynamic_cast(event); if (key_event) { + auto text = StdFromQString(key_event->text()); + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + text = ui::Hotkey::key_names.at(key); + } + anon_mail( ui::keypress_monitor::key_down_atom_v, - key_event->key(), - StdFromQString(key_event->text()), + key, + text, renderer_actor ? renderer_actor->std_name() : "", StdFromQString(m_window->objectName()), key_event->isAutoRepeat()) @@ -483,6 +510,14 @@ bool QMLViewport::event(QEvent *event) { auto key_event = dynamic_cast(event); if (key_event && !key_event->isAutoRepeat()) { + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + } + anon_mail( ui::keypress_monitor::key_up_atom_v, key_event->key(), diff --git a/src/ui/qt/viewport_widget/src/viewport_widget.cpp b/src/ui/qt/viewport_widget/src/viewport_widget.cpp index 634de069b..20f88115c 100644 --- a/src/ui/qt/viewport_widget/src/viewport_widget.cpp +++ b/src/ui/qt/viewport_widget/src/viewport_widget.cpp @@ -156,10 +156,19 @@ void ViewportGLWidget::mouseDoubleClickEvent(QMouseEvent *event) { } void ViewportGLWidget::keyPressEvent(QKeyEvent *key_event) { + auto text = StdFromQString(key_event->text()); + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + text = ui::Hotkey::key_names.at(key); + } anon_mail( ui::keypress_monitor::text_entry_atom_v, - StdFromQString(key_event->text()), + text, the_viewport_->name(), window_name_) .send(keypress_monitor_); @@ -168,6 +177,14 @@ void ViewportGLWidget::keyPressEvent(QKeyEvent *key_event) { void ViewportGLWidget::keyReleaseEvent(QKeyEvent *key_event) { if (!key_event->isAutoRepeat()) { + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + } + anon_mail( ui::keypress_monitor::key_up_atom_v, key_event->key(), @@ -183,10 +200,20 @@ bool ViewportGLWidget::event(QEvent *event) { auto key_event = dynamic_cast(event); if (key_event) { + auto text = StdFromQString(key_event->text()); + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + text = ui::Hotkey::key_names.at(key); + } + anon_mail( ui::keypress_monitor::key_down_atom_v, key_event->key(), - StdFromQString(key_event->text()), + text, the_viewport_->name(), window_name_, key_event->isAutoRepeat()) @@ -196,6 +223,14 @@ bool ViewportGLWidget::event(QEvent *event) { auto key_event = dynamic_cast(event); if (key_event && !key_event->isAutoRepeat()) { + auto key = key_event->key(); + + // remap numpad keys + if ((key_event->modifiers() & Qt::KeypadModifier) == Qt::KeypadModifier and + ui::Hotkey::key_to_numpad_key.find(key) != ui::Hotkey::key_to_numpad_key.end()) { + key = ui::Hotkey::key_to_numpad_key.at(key); + } + anon_mail( ui::keypress_monitor::key_up_atom_v, key_event->key(),