#include "KeyboardTranslator.h" #include #include #include #include #include #include #include #include #include #include const QByteArray KeyboardTranslatorManager::defaultTranslatorText( "keyboard \"Fallback Key Translator\"\n" "key Tab : \"\\t\"" ); #ifdef Q_OS_MAC const Qt::KeyboardModifier KeyboardTranslator::CTRL_MOD = Qt::MetaModifier; #else const Qt::KeyboardModifier KeyboardTranslator::CTRL_MOD = Qt::ControlModifier; #endif KeyboardTranslatorManager::KeyboardTranslatorManager() : _haveLoadedAll(false) { } KeyboardTranslatorManager::~KeyboardTranslatorManager() { qDeleteAll(_translators); } QString KeyboardTranslatorManager::get_kb_layout_dir() { return QString(":/konsole/kb-layouts/"); } QString KeyboardTranslatorManager::findTranslatorPath(const QString &name) { return QString(get_kb_layout_dir() + name + QString(".keytab")); } void KeyboardTranslatorManager::findTranslators() { QDir dir(get_kb_layout_dir()); QStringList filters; filters << QString("*.keytab"); dir.setNameFilters(filters); QStringList list = dir.entryList(filters); QStringListIterator listIter(list); while (listIter.hasNext()) { QString translatorPath = listIter.next(); QString name = QFileInfo(translatorPath).baseName(); if (!_translators.contains(name)) _translators.insert(name, 0); } _haveLoadedAll = true; } const KeyboardTranslator * KeyboardTranslatorManager::findTranslator(const QString &name) { if (name.isEmpty()) return defaultTranslator(); if (_translators.contains(name) && _translators[name] != 0) return _translators[name]; KeyboardTranslator *translator = loadTranslator(name); if (translator != nullptr) _translators[name] = translator; else if (!name.isEmpty()) qDebug() << "Unable to load translator" << name; return translator; } bool KeyboardTranslatorManager::saveTranslator(const KeyboardTranslator *translator) { Q_UNUSED(translator); return true; } KeyboardTranslator * KeyboardTranslatorManager::loadTranslator(const QString &name) { const QString &path = findTranslatorPath(name); QFile source(path); if (name.isEmpty() || !source.open(QIODevice::ReadOnly | QIODevice::Text)) return nullptr; return loadTranslator(&source, name); } const KeyboardTranslator *KeyboardTranslatorManager::defaultTranslator() { #if defined(Q_OS_WIN) #if defined(Q_CC_MSVC) const KeyboardTranslator *translator = findTranslator(QLatin1String("windows_conpty")); #else const KeyboardTranslator *translator = findTranslator(QLatin1String("windows_winpty")); #endif #elif defined(Q_OS_MAC) const KeyboardTranslator *translator = findTranslator(QLatin1String("macos_default")); #else const KeyboardTranslator *translator = findTranslator(QLatin1String("linux_default")); #endif if (!translator) { QBuffer textBuffer; textBuffer.setData(defaultTranslatorText); textBuffer.open(QIODevice::ReadOnly); translator = loadTranslator(&textBuffer, QLatin1String("fallback")); } return translator; } KeyboardTranslator * KeyboardTranslatorManager::loadTranslator(QIODevice *source, const QString &name) { KeyboardTranslator *translator = new KeyboardTranslator(name); KeyboardTranslatorReader reader(source); translator->setDescription(reader.description()); while (reader.hasNextEntry()) translator->addEntry(reader.nextEntry()); source->close(); if (!reader.parseError()) { return translator; } else { delete translator; return nullptr; } } KeyboardTranslatorWriter::KeyboardTranslatorWriter(QIODevice *destination) : _destination(destination) { Q_ASSERT(destination && destination->isWritable()); _writer = new QTextStream(_destination); } KeyboardTranslatorWriter::~KeyboardTranslatorWriter() { delete _writer; } void KeyboardTranslatorWriter::writeHeader(const QString &description) { *_writer << "keyboard \"" << description << '\"' << '\n'; } void KeyboardTranslatorWriter::writeEntry(const KeyboardTranslator::Entry &entry) { QString result; if (entry.command() != KeyboardTranslator::NoCommand) result = entry.resultToString(); else result = QLatin1Char('\"') + entry.resultToString() + QLatin1Char('\"'); *_writer << QLatin1String("key ") << entry.conditionToString() << QLatin1String(" : ") << result << QLatin1Char('\n'); } KeyboardTranslatorReader::KeyboardTranslatorReader(QIODevice *source) : _source(source), _hasNext(false) { while (_description.isEmpty() && !source->atEnd()) { QList tokens = tokenize(QString::fromUtf8(source->readLine())); if (!tokens.isEmpty() && tokens.first().type == Token::TitleKeyword) _description = tokens[1].text; } readNext(); } void KeyboardTranslatorReader::readNext() { while (!_source->atEnd()) { const QList &tokens = tokenize(QString::fromUtf8(_source->readLine())); if (!tokens.isEmpty() && tokens.first().type == Token::KeyKeyword) { KeyboardTranslator::States flags = KeyboardTranslator::NoState; KeyboardTranslator::States flagMask = KeyboardTranslator::NoState; Qt::KeyboardModifiers modifiers = Qt::NoModifier; Qt::KeyboardModifiers modifierMask = Qt::NoModifier; int keyCode = Qt::Key_unknown; decodeSequence(tokens[1].text.toLower(), keyCode, modifiers, modifierMask, flags, flagMask); KeyboardTranslator::Command command = KeyboardTranslator::NoCommand; QByteArray text; if (tokens[2].type == Token::OutputText) { text = tokens[2].text.toLocal8Bit(); } else if (tokens[2].type == Token::Command) { if (!parseAsCommand(tokens[2].text, command)) qDebug() << "Command" << tokens[2].text << "not understood."; } KeyboardTranslator::Entry newEntry; newEntry.setKeyCode(keyCode); newEntry.setState(flags); newEntry.setStateMask(flagMask); newEntry.setModifiers(modifiers); newEntry.setModifierMask(modifierMask); newEntry.setText(text); newEntry.setCommand(command); _nextEntry = newEntry; _hasNext = true; return; } } _hasNext = false; } bool KeyboardTranslatorReader::parseAsCommand( const QString &text, KeyboardTranslator::Command &command) { if (text.compare(QLatin1String("erase"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::EraseCommand; else if (text.compare(QLatin1String("scrollpageup"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollPageUpCommand; else if (text.compare(QLatin1String("scrollpagedown"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollPageDownCommand; else if (text.compare(QLatin1String("scrolllineup"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollLineUpCommand; else if (text.compare(QLatin1String("scrolllinedown"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollLineDownCommand; else if (text.compare(QLatin1String("scrolllock"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollLockCommand; else if (text.compare(QLatin1String("scrolluptotop"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollUpToTopCommand; else if (text.compare(QLatin1String("scrolldowntobottom"), Qt::CaseInsensitive) == 0) command = KeyboardTranslator::ScrollDownToBottomCommand; else return false; return true; } bool KeyboardTranslatorReader::decodeSequence( const QString &text, int &keyCode, Qt::KeyboardModifiers &modifiers, Qt::KeyboardModifiers &modifierMask, KeyboardTranslator::States &flags, KeyboardTranslator::States &flagMask) { bool isWanted = true; bool endOfItem = false; QString buffer; Qt::KeyboardModifiers tempModifiers = modifiers; Qt::KeyboardModifiers tempModifierMask = modifierMask; KeyboardTranslator::States tempFlags = flags; KeyboardTranslator::States tempFlagMask = flagMask; for (int i = 0; i < text.size(); i++) { const QChar &ch = text[i]; bool isFirstLetter = i == 0; bool isLastLetter = (i == text.size() - 1); endOfItem = true; if (ch.isLetterOrNumber()) { endOfItem = false; buffer.append(ch); } else if (isFirstLetter) { buffer.append(ch); } if ((endOfItem || isLastLetter) && !buffer.isEmpty()) { Qt::KeyboardModifier itemModifier = Qt::NoModifier; int itemKeyCode = 0; KeyboardTranslator::State itemFlag = KeyboardTranslator::NoState; if (parseAsModifier(buffer, itemModifier)) { tempModifierMask |= itemModifier; if (isWanted) tempModifiers |= itemModifier; } else if (parseAsStateFlag(buffer, itemFlag)) { tempFlagMask |= itemFlag; if (isWanted) tempFlags |= itemFlag; } else if (parseAsKeyCode(buffer, itemKeyCode)) keyCode = itemKeyCode; else qDebug() << "Unable to parse key binding item:" << buffer; buffer.clear(); } if (ch == QLatin1Char('+')) isWanted = true; else if (ch == QLatin1Char('-')) isWanted = false; } modifiers = tempModifiers; modifierMask = tempModifierMask; flags = tempFlags; flagMask = tempFlagMask; return true; } bool KeyboardTranslatorReader::parseAsModifier(const QString &item, Qt::KeyboardModifier &modifier) { if (item == QLatin1String("shift")) modifier = Qt::ShiftModifier; else if (item == QLatin1String("ctrl") || item == QLatin1String("control")) modifier = Qt::ControlModifier; else if (item == QLatin1String("alt")) modifier = Qt::AltModifier; else if (item == QLatin1String("meta")) modifier = Qt::MetaModifier; else if (item == QLatin1String("keypad")) modifier = Qt::KeypadModifier; else return false; return true; } bool KeyboardTranslatorReader::parseAsStateFlag(const QString &item, KeyboardTranslator::State &flag) { if (item == QLatin1String("appcukeys") || item == QLatin1String("appcursorkeys")) flag = KeyboardTranslator::CursorKeysState; else if (item == QLatin1String("ansi")) flag = KeyboardTranslator::AnsiState; else if (item == QLatin1String("newline")) flag = KeyboardTranslator::NewLineState; else if (item == QLatin1String("appscreen")) flag = KeyboardTranslator::AlternateScreenState; else if (item == QLatin1String("anymod") || item == QLatin1String("anymodifier")) flag = KeyboardTranslator::AnyModifierState; else if (item == QLatin1String("appkeypad")) flag = KeyboardTranslator::ApplicationKeypadState; else return false; return true; } bool KeyboardTranslatorReader::parseAsKeyCode(const QString &item, int &keyCode) { QKeySequence sequence = QKeySequence::fromString(item); if (!sequence.isEmpty()) { keyCode = sequence[0].toCombined(); if (sequence.count() > 1) { qDebug() << "Unhandled key codes in sequence: " << item; } } else if (item == QLatin1String("prior")) keyCode = Qt::Key_PageUp; else if (item == QLatin1String("next")) keyCode = Qt::Key_PageDown; else return false; return true; } QString KeyboardTranslatorReader::description() const { return _description; } bool KeyboardTranslatorReader::hasNextEntry() const { return _hasNext; } KeyboardTranslator::Entry KeyboardTranslatorReader::createEntry(const QString &condition, const QString &result) { QString entryString = QString::fromLatin1("keyboard \"temporary\"\nkey "); entryString.append(condition); entryString.append(QLatin1String(" : ")); KeyboardTranslator::Command command; if (parseAsCommand(result, command)) entryString.append(result); else entryString.append(QLatin1Char('\"') + result + QLatin1Char('\"')); QByteArray array = entryString.toUtf8(); QBuffer buffer(&array); buffer.open(QIODevice::ReadOnly); KeyboardTranslatorReader reader(&buffer); KeyboardTranslator::Entry entry; if (reader.hasNextEntry()) entry = reader.nextEntry(); return entry; } KeyboardTranslator::Entry KeyboardTranslatorReader::nextEntry() { Q_ASSERT(_hasNext); KeyboardTranslator::Entry entry = _nextEntry; readNext(); return entry; } bool KeyboardTranslatorReader::parseError() { return false; } QList KeyboardTranslatorReader::tokenize(const QString &line) { QString text = line; bool inQuotes = false; int commentPos = -1; for (int i = text.length() - 1; i >= 0; i--) { QChar ch = text[i]; if (ch == QLatin1Char('\"')) inQuotes = !inQuotes; else if (ch == QLatin1Char('#') && !inQuotes) commentPos = i; } if (commentPos != -1) text.remove(commentPos, text.length()); text = text.simplified(); static QRegularExpression title(QLatin1String("keyboard\\s+\"(.*)\"")); static QRegularExpression key( QLatin1String("key\\s+([\\w\\+\\s\\-\\*\\.]+)\\s*:\\s*(\"(.*)\"|\\w+)")); QList list; if (text.isEmpty()) { return list; } const auto titleMatch = title.match(text); const auto keyMatch = key.match(text); if (titleMatch.hasMatch()) { Token titleToken = {Token::TitleKeyword, QString()}; Token textToken = {Token::TitleText, titleMatch.captured(1)}; list << titleToken << textToken; } else if (keyMatch.hasMatch()) { Token keyToken = {Token::KeyKeyword, QString()}; Token sequenceToken = {Token::KeySequence, keyMatch.captured(1).remove(QLatin1Char(' '))}; list << keyToken << sequenceToken; if (keyMatch.captured(3).isEmpty()) { Token commandToken = {Token::Command, keyMatch.captured(2)}; list << commandToken; } else { Token outputToken = {Token::OutputText, keyMatch.captured(3)}; list << outputToken; } } else { qDebug() << "Line in keyboard translator file could not be understood:" << text; } return list; } QList KeyboardTranslatorManager::allTranslators() { if (!_haveLoadedAll) { findTranslators(); } return _translators.keys(); } KeyboardTranslator::Entry::Entry() : _keyCode(0), _modifiers(Qt::NoModifier), _modifierMask(Qt::NoModifier), _state(NoState), _stateMask(NoState), _command(NoCommand) { } bool KeyboardTranslator::Entry::operator==(const Entry &rhs) const { return _keyCode == rhs._keyCode && _modifiers == rhs._modifiers && _modifierMask == rhs._modifierMask && _state == rhs._state && _stateMask == rhs._stateMask && _command == rhs._command && _text == rhs._text; } bool KeyboardTranslator::Entry::matches(int keyCode, Qt::KeyboardModifiers modifiers, States testState) const { #ifdef Q_OS_MAC modifiers &= ~Qt::KeypadModifier; #endif if (_keyCode != keyCode) return false; if ((modifiers & _modifierMask) != (_modifiers & _modifierMask)) return false; if ((modifiers & ~Qt::KeypadModifier) != 0) testState |= AnyModifierState; if ((testState & _stateMask) != (_state & _stateMask)) return false; bool anyModifiersSet = modifiers != 0 && modifiers != Qt::KeypadModifier; bool wantAnyModifier = _state & KeyboardTranslator::AnyModifierState; if (_stateMask & KeyboardTranslator::AnyModifierState) { if (wantAnyModifier != anyModifiersSet) return false; } return true; } QByteArray KeyboardTranslator::Entry::escapedText(bool expandWildCards, Qt::KeyboardModifiers modifiers) const { QByteArray result(text(expandWildCards, modifiers)); for (int i = 0; i < result.size(); i++) { char ch = result[i]; char replacement = 0; switch (ch) { case 27: replacement = 'E'; break; case 8: replacement = 'b'; break; case 12: replacement = 'f'; break; case 9: replacement = 't'; break; case 13: replacement = 'r'; break; case 10: replacement = 'n'; break; default: if (!QChar(QLatin1Char(ch)).isPrint()) replacement = 'x'; break; } if (replacement == 'x') { QByteArray escaped("\\x"); escaped += QByteArray(1, ch).toHex(); result.replace(i, 1, QByteArrayView(escaped)); } else if (replacement != 0) { result.remove(i, 1); result.insert(i, '\\'); result.insert(i + 1, replacement); } } return result; } QByteArray KeyboardTranslator::Entry::unescape(const QByteArray &input) const { QByteArray result(input); for (int i = 0; i < result.size() - 1; i++) { char ch = result[i]; if (ch == '\\') { char replacement[2] = {0, 0}; int charsToRemove = 2; bool escapedChar = true; switch (result[i + 1]) { case 'E': replacement[0] = 27; break; case 'b': replacement[0] = 8; break; case 'f': replacement[0] = 12; break; case 't': replacement[0] = 9; break; case 'r': replacement[0] = 13; break; case 'n': replacement[0] = 10; break; case 'x': { char hexDigits[3] = {0}; if ((i < result.size() - 2) && isxdigit(result[i + 2])) hexDigits[0] = result[i + 2]; if ((i < result.size() - 3) && isxdigit(result[i + 3])) hexDigits[1] = result[i + 3]; unsigned charValue = 0; sscanf(hexDigits, "%x", &charValue); replacement[0] = (char)charValue; charsToRemove = 2 + static_cast(strlen(hexDigits)); break; } default: escapedChar = false; break; } if (escapedChar) result.replace(i, charsToRemove, replacement); } } return result; } void KeyboardTranslator::Entry::insertModifier(QString &item, int modifier) const { if (!(modifier & _modifierMask)) return; if (modifier & _modifiers) item += QLatin1Char('+'); else item += QLatin1Char('-'); if (modifier == Qt::ShiftModifier) item += QLatin1String("Shift"); else if (modifier == Qt::ControlModifier) item += QLatin1String("Ctrl"); else if (modifier == Qt::AltModifier) item += QLatin1String("Alt"); else if (modifier == Qt::MetaModifier) item += QLatin1String("Meta"); else if (modifier == Qt::KeypadModifier) item += QLatin1String("KeyPad"); } void KeyboardTranslator::Entry::insertState(QString &item, int state) const { if (!(state & _stateMask)) return; if (state & _state) item += QLatin1Char('+'); else item += QLatin1Char('-'); if (state == KeyboardTranslator::AlternateScreenState) item += QLatin1String("AppScreen"); else if (state == KeyboardTranslator::NewLineState) item += QLatin1String("NewLine"); else if (state == KeyboardTranslator::AnsiState) item += QLatin1String("Ansi"); else if (state == KeyboardTranslator::CursorKeysState) item += QLatin1String("AppCursorKeys"); else if (state == KeyboardTranslator::AnyModifierState) item += QLatin1String("AnyModifier"); else if (state == KeyboardTranslator::ApplicationKeypadState) item += QLatin1String("AppKeypad"); } QString KeyboardTranslator::Entry::resultToString( bool expandWildCards, Qt::KeyboardModifiers modifiers) const { if (!_text.isEmpty()) return QString::fromLatin1(escapedText(expandWildCards, modifiers)); else if (_command == EraseCommand) return QLatin1String("Erase"); else if (_command == ScrollPageUpCommand) return QLatin1String("ScrollPageUp"); else if (_command == ScrollPageDownCommand) return QLatin1String("ScrollPageDown"); else if (_command == ScrollLineUpCommand) return QLatin1String("ScrollLineUp"); else if (_command == ScrollLineDownCommand) return QLatin1String("ScrollLineDown"); else if (_command == ScrollLockCommand) return QLatin1String("ScrollLock"); else if (_command == ScrollUpToTopCommand) return QLatin1String("ScrollUpToTop"); else if (_command == ScrollDownToBottomCommand) return QLatin1String("ScrollDownToBottom"); return QString(); } QString KeyboardTranslator::Entry::conditionToString() const { QString result = QKeySequence(_keyCode).toString(); insertModifier(result, Qt::ShiftModifier); insertModifier(result, Qt::ControlModifier); insertModifier(result, Qt::AltModifier); insertModifier(result, Qt::MetaModifier); insertModifier(result, Qt::KeypadModifier); insertState(result, KeyboardTranslator::AlternateScreenState); insertState(result, KeyboardTranslator::NewLineState); insertState(result, KeyboardTranslator::AnsiState); insertState(result, KeyboardTranslator::CursorKeysState); insertState(result, KeyboardTranslator::AnyModifierState); insertState(result, KeyboardTranslator::ApplicationKeypadState); return result; } KeyboardTranslator::KeyboardTranslator(const QString &name) : _name(name) { } void KeyboardTranslator::setDescription(const QString &description) { _description = description; } QString KeyboardTranslator::description() const { return _description; } void KeyboardTranslator::setName(const QString &name) { _name = name; } QString KeyboardTranslator::name() const { return _name; } QList KeyboardTranslator::entries() const { return _entries.values(); } void KeyboardTranslator::addEntry(const Entry &entry) { const int keyCode = entry.keyCode(); _entries.insert(keyCode, entry); } void KeyboardTranslator::replaceEntry(const Entry &existing, const Entry &replacement) { if (!existing.isNull()) _entries.remove(existing.keyCode(), existing); _entries.insert(replacement.keyCode(), replacement); } void KeyboardTranslator::removeEntry(const Entry &entry) { _entries.remove(entry.keyCode(), entry); } KeyboardTranslator::Entry KeyboardTranslator::findEntry(int keyCode, Qt::KeyboardModifiers modifiers, States state) const { for (auto it = _entries.cbegin(), end = _entries.cend(); it != end; ++it) { if (it.key() == keyCode) if (it.value().matches(keyCode, modifiers, state)) return *it; } return Entry(); } void KeyboardTranslatorManager::addTranslator(KeyboardTranslator *translator) { _translators.insert(translator->name(), translator); if (!saveTranslator(translator)) qDebug() << "Unable to save translator" << translator->name() << "to disk."; } bool KeyboardTranslatorManager::deleteTranslator(const QString &name) { Q_ASSERT(_translators.contains(name)); QString path = findTranslatorPath(name); if (QFile::remove(path)) { _translators.remove(name); return true; } else { qDebug() << "Failed to remove translator - " << path; return false; } } Q_GLOBAL_STATIC(KeyboardTranslatorManager, theKeyboardTranslatorManager) KeyboardTranslatorManager *KeyboardTranslatorManager::instance() { return theKeyboardTranslatorManager; }