Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard: automatically reload mapping file and update tooltips #13082

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4f9207d
move handling of keyboard mapping file to KeyboardEventFilter
ronso0 May 1, 2024
cffa779
Skins: update tooltips with keyboard shortcuts when shortcuts are tog…
ronso0 May 1, 2024
bbe1e2b
Menubar: update shortcuts when the keyboard mapping file changed
ronso0 May 1, 2024
ea676fc
KeyboardEventFilter: restore default ON, auto*, disconnect shorthand
ronso0 May 19, 2024
d715454
fixup! Skins: update tooltips with keyboard shortcuts when shortcuts …
ronso0 Oct 13, 2024
36c8a49
fixup! Skins: update tooltips with keyboard shortcuts when shortcuts …
ronso0 Oct 13, 2024
44dc1c8
fixup! move handling of keyboard mapping file to KeyboardEventFilter
ronso0 Oct 13, 2024
44f47e9
KeyboardEventFilter: use mixxx::Logger
ronso0 Oct 13, 2024
19b4434
KeyboardEventFilter: move menubar toggle connection to WMainMenubar
ronso0 Oct 13, 2024
e3212d9
fixup! KeyboardEventFilter: move menubar toggle connection to WMainMe…
ronso0 Oct 13, 2024
49afca2
fixup! move handling of keyboard mapping file to KeyboardEventFilter
ronso0 Oct 16, 2024
4103f0f
fixup! Skins: update tooltips with keyboard shortcuts when shortcuts …
ronso0 Oct 23, 2024
4ed1a9c
fixup! Menubar: update shortcuts when the keyboard mapping file changed
ronso0 Oct 23, 2024
465ba9a
fixup! KeyboardEventFilter: use mixxx::Logger
ronso0 Oct 23, 2024
693015b
fixup! Skins: update tooltips with keyboard shortcuts when shortcuts …
ronso0 Oct 24, 2024
4eed28c
Tooltips: create keyboard shortcuts for hotcue buttons and ControlPot…
ronso0 Oct 24, 2024
c85011c
KeyboardEventFilter: add file path helper, polish
ronso0 Oct 26, 2024
79b8559
Merge remote-tracking branch 'mixxx/main' into kbd-mapping-reload-too…
ronso0 Jan 9, 2025
f7271f2
KeyboardEventFilter: use std::as_const()
ronso0 Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/control/controlpotmeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ void ControlPotmeter::privateValueChanged(double dValue, QObject* pSender) {

PotmeterControls::PotmeterControls(const ConfigKey& key)
: m_control(key, this),
// When adding an additional control here, do not forget to also add
// it to the `PotmeterControls::addAlias()` method, too.
// When adding an additional control here, remember to also add
// it to the PotmeterControls::addAlias() method.
// Also remember to add it to LegacySkinParser::setupConnections()
// for constructing the keyboard shortcut tooltip strings.
m_controlUp(configKeyFromBaseKey(key, QStringLiteral("_up"))),
m_controlDown(configKeyFromBaseKey(key, QStringLiteral("_down"))),
m_controlUpSmall(configKeyFromBaseKey(key, QStringLiteral("_up_small"))),
Expand Down
262 changes: 221 additions & 41 deletions src/controllers/keyboard/keyboardeventfilter.cpp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is mostly copy pasted, right? Do you want to improve it or leave it?

Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
#include "controllers/keyboard/keyboardeventfilter.h"

#include <QAction>
#include <QEvent>
#include <QKeyEvent>
#include <QtDebug>

#include "moc_keyboardeventfilter.cpp"
#include "util/cmdlineargs.h"
#include "util/logger.h"
#include "util/timer.h"
#include "widget/wbasewidget.h"

KeyboardEventFilter::KeyboardEventFilter(ConfigObject<ConfigValueKbd>* pKbdConfigObject,
QObject* parent,
const char* name)
namespace {
const ConfigKey kKbdEnabledCfgKey =
ConfigKey(QStringLiteral("[Keyboard]"), QStringLiteral("Enabled"));
mixxx::Logger kLogger("KeyboardEventFilter");

const QString mappingFilePath(const QString& dir, const QString& fileName) {
return QDir(dir).filePath(fileName + QStringLiteral(".kbd.cfg"));
}
} // anonymous namespace

KeyboardEventFilter::KeyboardEventFilter(UserSettingsPointer pConfig,
const QLocale& locale,
QObject* parent)
: QObject(parent),
#ifndef __APPLE__
m_altPressedWithoutKey(false),
#endif
m_pKbdConfigObject(nullptr) {
setObjectName(name);
setKeyboardConfig(pKbdConfigObject);
m_pConfig(pConfig),
m_locale(locale),
m_enabled(false),
m_autoReloader(RuntimeLoggingCategory(QStringLiteral("kbd_auto_reload"))) {
// Get the enabled state.
// Set the default if the key/value doesn't exist.
if (pConfig->getValueString(kKbdEnabledCfgKey).isEmpty()) {
pConfig->setValue(kKbdEnabledCfgKey, true);
}
m_enabled = pConfig->getValue(kKbdEnabledCfgKey, true);

createKeyboardConfig();

// For watching the currently loaded mapping file
connect(&m_autoReloader,
&AutoFileReloader::fileChanged,
this,
&KeyboardEventFilter::reloadKeyboardConfig);
}

KeyboardEventFilter::~KeyboardEventFilter() {
Expand All @@ -28,7 +57,7 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) {
// because we might not get Key Release events.
m_qActiveKeyList.clear();
} else if (e->type() == QEvent::KeyPress) {
QKeyEvent* ke = (QKeyEvent *)e;
QKeyEvent* ke = static_cast<QKeyEvent*>(e);

#ifdef __APPLE__
// On Mac OSX the nativeScanCode is empty (const 1) http://doc.qt.nokia.com/4.7/qkeyevent.html#nativeScanCode
Expand All @@ -44,6 +73,12 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) {
}

QKeySequence ks = getKeySeq(ke);

// If inactive, return after logging the key event in getKeySeq()
if (!isEnabled()) {
return true;
}

if (!ks.isEmpty()) {
#ifndef __APPLE__
m_altPressedWithoutKey = false;
Expand All @@ -55,22 +90,25 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) {
for (auto it = m_keySequenceToControlHash.constFind(ksv);
it != m_keySequenceToControlHash.constEnd() && it.key() == ksv; ++it) {
const ConfigKey& configKey = it.value();
if (configKey.group != "[KeyboardShortcuts]") {
ControlObject* control = ControlObject::getControl(configKey);
if (control) {
//qDebug() << configKey << "MidiOpCode::NoteOn" << 1;
// Add key to active key list
m_qActiveKeyList.append(KeyDownInformation(
if (configKey.group == "[KeyboardShortcuts]") {
// We don't handle menubar shortcuts here
continue;
}
ControlObject* control = ControlObject::getControl(configKey);
if (control) {
// kLogger.debug() << configKey << "MidiOpCode::NoteOn" << 1;
// Add key to active key list
m_qActiveKeyList.append(KeyDownInformation(
keyId, ke->modifiers(), control));
// Since setting the value might cause us to go down
// a route that would eventually clear the active
// key list, do that last.
control->setValueFromMidi(MidiOpCode::NoteOn, 1);
result = true;
} else {
qDebug() << "Warning: Keyboard key is configured for nonexistent control:"
<< configKey.group << configKey.item;
}
// Since setting the value might cause us to go down
// a route that would eventually clear the active
// key list, do that last.
control->setValueFromMidi(MidiOpCode::NoteOn, 1);
result = true;
} else {
kLogger.warning() << "Key" << keyId
<< "is configured for nonexistent control:"
<< configKey.group << configKey.item;
}
}
return result;
Expand Down Expand Up @@ -107,11 +145,10 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) {
#else
int keyId = ke->nativeScanCode();
#endif
bool autoRepeat = ke->isAutoRepeat();

//qDebug() << "KeyRelease event =" << ke->key() << "AutoRepeat =" << autoRepeat << "KeyId =" << keyId;
// kLogger.debug() << "KeyRelease event =" << ke->key()
// << "AutoRepeat=" << autoRepeat << "KeyId =" << keyId;

int clearModifiers = 0;
int clearModifiers = Qt::NoModifier;
#ifdef __APPLE__
// OS X apparently doesn't deliver KeyRelease events when you are
// holding Ctrl. So release all key-presses that were triggered with
Expand All @@ -121,15 +158,18 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) {
}
#endif

bool autoRepeat = ke->isAutoRepeat();
bool matched = false;
// Run through list of active keys to see if the released key is active
// Run through list of active keys to see if the released key is active.
// Start from end because we may remove the current item.
for (int i = m_qActiveKeyList.size() - 1; i >= 0; i--) {
const KeyDownInformation& keyDownInfo = m_qActiveKeyList[i];
ControlObject* pControl = keyDownInfo.pControl;
if (keyDownInfo.keyId == keyId ||
(clearModifiers > 0 && keyDownInfo.modifiers == clearModifiers)) {
(clearModifiers != Qt::NoModifier &&
keyDownInfo.modifiers == clearModifiers)) {
if (!autoRepeat) {
//qDebug() << pControl->getKey() << "MidiOpCode::NoteOff" << 0;
// kLogger.debug() << pControl->getKey() << "MidiOpCode::NoteOff" << 0;
pControl->setValueFromMidi(MidiOpCode::NoteOff, 0);
m_qActiveKeyList.removeAt(i);
}
Expand All @@ -142,7 +182,7 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) {
} else if (e->type() == QEvent::KeyboardLayoutChange) {
// This event is not fired on ubunty natty, why?
// TODO(XXX): find a way to support KeyboardLayoutChange Bug #997811
//qDebug() << "QEvent::KeyboardLayoutChange";
// kLogger.debug() << "QEvent::KeyboardLayoutChange";
}
return false;
}
Expand Down Expand Up @@ -180,24 +220,164 @@ QKeySequence KeyboardEventFilter::getKeySeq(QKeyEvent* e) {

if (CmdlineArgs::Instance().getDeveloper()) {
if (e->type() == QEvent::KeyPress) {
qDebug() << "keyboard press: " << k.toString();
kLogger.debug() << "keyboard press: " << k.toString();
} else if (e->type() == QEvent::KeyRelease) {
qDebug() << "keyboard release: " << k.toString();
kLogger.debug() << "keyboard release: " << k.toString();
}
}

return k;
}

void KeyboardEventFilter::setKeyboardConfig(ConfigObject<ConfigValueKbd>* pKbdConfigObject) {
// Keyboard configs are a surjection from ConfigKey to key sequence. We
// invert the mapping to create an injection from key sequence to
// ConfigKey. This allows a key sequence to trigger multiple controls in
// Mixxx.
m_keySequenceToControlHash = pKbdConfigObject->transpose();
m_pKbdConfigObject = pKbdConfigObject;
void KeyboardEventFilter::setEnabled(bool enabled) {
if (enabled) {
kLogger.debug() << "Enable keyboard shortcuts/mappings";
} else {
kLogger.debug() << "Disable keyboard shortcuts/mappings";
}
m_enabled = enabled;
m_pConfig->setValue(kKbdEnabledCfgKey, enabled);
// Shortcuts may be toggled off and on again to make Mixxx discover a new
// Custom.kbd.cfg, so reload now.
// Note: the other way around (removing a loaded Custom.kbd.cfg) is covered
// by the auto-reloader and we'll try to load a built-in mapping then.
if (enabled) {
reloadKeyboardConfig();
}
// Update widget tooltips
emit shortcutsEnabled(enabled);
}

void KeyboardEventFilter::registerShortcutWidget(WBaseWidget* pWidget) {
m_widgets.append(pWidget);

// Tell widgets to reconstruct tooltips when option is toggled.
// WBaseWidget is not a QObject, need to use a lambda
connect(this,
&KeyboardEventFilter::shortcutsEnabled,
this,
[pWidget](bool enabled) {
pWidget->toggleKeyboardShortcutHints(enabled);
});
}

void KeyboardEventFilter::updateWidgetShortcuts() {
// kLogger.debug() << "updateWidgetShortcuts";
ScopedTimer timer(QStringLiteral("KeyboardEventFilter::updateWidgetShortcuts"));
QStringList shortcutHints;
for (auto* pWidget : std::as_const(m_widgets)) {
shortcutHints.clear();
QString keyString;
const QList<std::pair<ConfigKey, QString>> controlsCommands =
pWidget->getShortcutControlsAndCommands();
for (const auto& [control, command] : controlsCommands) {
keyString = m_pKbdConfig->getValueString(control);
if (!keyString.isEmpty()) {
shortcutHints.append(buildShortcutString(keyString, command));
}
}
// might be empty to clear the previous tooltip
pWidget->setShortcutTooltip(shortcutHints.join(QStringLiteral("\n")));
}
// Update widget tooltips (WBaseWidget handles no-ops).
emit shortcutsEnabled(m_enabled);
}

ConfigObject<ConfigValueKbd>* KeyboardEventFilter::getKeyboardConfig() {
return m_pKbdConfigObject;
void KeyboardEventFilter::clearWidgets() {
disconnect();
m_widgets.clear();
}

const QString KeyboardEventFilter::buildShortcutString(
const QString& shortcut, const QString& cmd) const {
if (shortcut.isEmpty()) {
return QString();
}

// translate shortcut to native text
const QString nativeShortcut = QKeySequence(shortcut, QKeySequence::PortableText)
.toString(QKeySequence::NativeText);

QString shortcutTooltip;
shortcutTooltip += tr("Shortcut");
if (!cmd.isEmpty()) {
shortcutTooltip += " ";
shortcutTooltip += cmd;
}
shortcutTooltip += ": ";
shortcutTooltip += nativeShortcut;
return shortcutTooltip;
}

void KeyboardEventFilter::registerMenuBarActionSetShortcut(QAction* pAction,
const ConfigKey& command,
const QString& defaultShortcut) {
m_menuBarActions.emplace(pAction, std::make_pair(command, defaultShortcut));
pAction->setShortcut(QKeySequence(m_pKbdConfig->getValue(command, defaultShortcut)));
pAction->setShortcutContext(Qt::ApplicationShortcut);
}

void KeyboardEventFilter::clearMenuBarActions() {
m_menuBarActions.clear();
}

void KeyboardEventFilter::updateMenuBarActionShortcuts() {
// kLogger.debug() << "updateMenuBarActionShortcuts";
QHashIterator<QAction*, std::pair<ConfigKey, QString>> it(m_menuBarActions);
while (it.hasNext()) {
it.next();
auto* pAction = it.key();
DEBUG_ASSERT(pAction);
const QString keyStr = m_pKbdConfig->getValue(it.value().first, it.value().second);
pAction->setShortcut(QKeySequence(keyStr));
}
}

void KeyboardEventFilter::reloadKeyboardConfig() {
kLogger.debug() << "reloadKeyboardConfig, enabled:" << m_enabled;
ScopedTimer timer(QStringLiteral("KeyboardEventFilter::reload"));
createKeyboardConfig();
updateWidgetShortcuts();
updateMenuBarActionShortcuts();
}

void KeyboardEventFilter::createKeyboardConfig() {
// Remove the previously watched file.
// Could be the user mapping has been removed and we'll need to switch
// to the built-in default mapping.
m_autoReloader.clear();

// Check first in user's Mixxx directory
QString keyboardFile = mappingFilePath(m_pConfig->getSettingsPath(), QStringLiteral("Custom"));
if (QFile::exists(keyboardFile)) {
kLogger.debug() << "Found and will use custom keyboard mapping" << keyboardFile;
} else {
// check if a default keyboard exists
const QString resourcePath = m_pConfig->getResourcePath() + QStringLiteral("keyboard/");
keyboardFile = mappingFilePath(resourcePath, m_locale.name());
if (QFile::exists(keyboardFile)) {
kLogger.debug() << "Found and will use default keyboard mapping" << keyboardFile;
} else {
kLogger.debug() << keyboardFile << " not found, try to use en_US.kbd.cfg";
keyboardFile = mappingFilePath(resourcePath, QStringLiteral("en_US"));
if (!QFile::exists(keyboardFile)) {
kLogger.debug() << keyboardFile << " not found, starting without shortcuts";
keyboardFile = "";
}
}
}
if (!keyboardFile.isEmpty()) {
// Watch the loaded file for changes.
m_autoReloader.addPath(keyboardFile);
}

// Read the keyboard configuration file and set m_pKbdConfig.
// Keyboard configs are a surjection from ConfigKey to key sequence.
// We invert the mapping to create an injection from key sequence to
// ConfigKey. This allows a key sequence to trigger multiple controls in
// Mixxx.
m_pKbdConfig = std::make_shared<ConfigObject<ConfigValueKbd>>(keyboardFile);
// TODO Slightly accelerate lookup in eventFilter() by creating a copy
// and removing [KeyboardShortcut] mappings (menubar) before transposing?
m_keySequenceToControlHash = m_pKbdConfig->transpose();
}
Loading
Loading