Files
konsole/src/KeyboardTranslator.cpp
Alex Richardson a83db71590 Port to KF5/Qt5
TerminalDisplayAccessible is disabled for Qt5 currently since I don't
have any experience with accessible stuff and it is more complicated
than just changing a few includes

REVIEW: 111937
2013-08-20 23:34:35 +02:00

689 lines
22 KiB
C++

/*
This source file is part of Konsole, a terminal emulator.
Copyright 2007-2008 by Robert Knight <robertknight@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.
*/
// Own
#include "KeyboardTranslator.h"
// System
#include <ctype.h>
#include <stdio.h>
// Qt
#include <QtCore/QBuffer>
#include <QtCore/QTextStream>
#include <QtGui/QKeySequence>
// KDE
#include <KDebug>
#include <KLocalizedString>
using namespace Konsole;
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 = '\"' + entry.resultToString() + '\"';
*_writer << "key " << entry.conditionToString() << " : " << result << '\n';
}
// each line of the keyboard translation file is one of:
//
// - keyboard "name"
// - key KeySequence : "characters"
// - key KeySequence : CommandName
//
// KeySequence begins with the name of the key ( taken from the Qt::Key enum )
// and is followed by the keyboard modifiers and state flags ( with + or - in front
// of each modifier or flag to indicate whether it is required ). All keyboard modifiers
// and flags are optional, if a particular modifier or state is not specified it is
// assumed not to be a part of the sequence. The key sequence may contain whitespace
//
// eg: "key Up+Shift : scrollLineUp"
// "key PgDown-Shift : "\E[6~"
//
// (lines containing only whitespace are ignored, parseLine assumes that comments have
// already been removed)
//
KeyboardTranslatorReader::KeyboardTranslatorReader(QIODevice* source)
: _source(source)
, _hasNext(false)
{
// read input until we find the description
while (_description.isEmpty() && !source->atEnd()) {
QList<Token> tokens = tokenize(QString::fromLocal8Bit(source->readLine()));
if (!tokens.isEmpty() && tokens.first().type == Token::TitleKeyword)
_description = i18n(tokens[1].text.toUtf8().constData());
}
// read first entry (if any)
readNext();
}
void KeyboardTranslatorReader::readNext()
{
// find next entry
while (!_source->atEnd()) {
const QList<Token>& tokens = tokenize(QString::fromLocal8Bit(_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;
// get text or command
if (tokens[2].type == Token::OutputText) {
text = tokens[2].text.toLocal8Bit();
} else if (tokens[2].type == Token::Command) {
// identify command
if (!parseAsCommand(tokens[2].text, command))
kWarning() << "Key" << tokens[1].text << ", 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("erase", Qt::CaseInsensitive) == 0)
command = KeyboardTranslator::EraseCommand;
else if (text.compare("scrollpageup", Qt::CaseInsensitive) == 0)
command = KeyboardTranslator::ScrollPageUpCommand;
else if (text.compare("scrollpagedown", Qt::CaseInsensitive) == 0)
command = KeyboardTranslator::ScrollPageDownCommand;
else if (text.compare("scrolllineup", Qt::CaseInsensitive) == 0)
command = KeyboardTranslator::ScrollLineUpCommand;
else if (text.compare("scrolllinedown", Qt::CaseInsensitive) == 0)
command = KeyboardTranslator::ScrollLineDownCommand;
else if (text.compare("scrolluptotop", Qt::CaseInsensitive) == 0)
command = KeyboardTranslator::ScrollUpToTopCommand;
else if (text.compare("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.count() ; i++) {
const QChar& ch = text[i];
const bool isFirstLetter = (i == 0);
const bool isLastLetter = (i == text.count() - 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 {
kWarning() << "Unable to parse key binding item:" << buffer;
}
buffer.clear();
}
// check if this is a wanted / not-wanted flag and update the
// state ready for the next item
if (ch == '+')
isWanted = true;
else if (ch == '-')
isWanted = false;
}
modifiers = tempModifiers;
modifierMask = tempModifierMask;
flags = tempFlags;
flagMask = tempFlagMask;
return true;
}
bool KeyboardTranslatorReader::parseAsModifier(const QString& item , Qt::KeyboardModifier& modifier)
{
if (item == "shift")
modifier = Qt::ShiftModifier;
else if (item == "ctrl" || item == "control")
modifier = Qt::ControlModifier;
else if (item == "alt")
modifier = Qt::AltModifier;
else if (item == "meta")
modifier = Qt::MetaModifier;
else if (item == "keypad")
modifier = Qt::KeypadModifier;
else
return false;
return true;
}
bool KeyboardTranslatorReader::parseAsStateFlag(const QString& item , KeyboardTranslator::State& flag)
{
if (item == "appcukeys" || item == "appcursorkeys")
flag = KeyboardTranslator::CursorKeysState;
else if (item == "ansi")
flag = KeyboardTranslator::AnsiState;
else if (item == "newline")
flag = KeyboardTranslator::NewLineState;
else if (item == "appscreen")
flag = KeyboardTranslator::AlternateScreenState;
else if (item == "anymod" || item == "anymodifier")
flag = KeyboardTranslator::AnyModifierState;
else if (item == "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];
if (sequence.count() > 1) {
kWarning() << "Unhandled key codes in sequence: " << item;
}
// additional cases implemented for backwards compatibility with KDE 3
} else if (item == "prior") { // TODO: remove it in the future
keyCode = Qt::Key_PageUp;
} else if (item == "next") { // TODO: remove it in the future
keyCode = Qt::Key_PageDown;
} else {
return false;
}
return true;
}
QString KeyboardTranslatorReader::description() const
{
return _description;
}
bool KeyboardTranslatorReader::hasNextEntry()
{
return _hasNext;
}
KeyboardTranslator::Entry KeyboardTranslatorReader::createEntry(const QString& condition ,
const QString& result)
{
QString entryString("keyboard \"temporary\"\nkey ");
entryString.append(condition);
entryString.append(" : ");
// if 'result' is the name of a command then the entry result will be that command,
// otherwise the result will be treated as a string to echo when the key sequence
// specified by 'condition' is pressed
KeyboardTranslator::Command command;
if (parseAsCommand(result, command))
entryString.append(result);
else
entryString.append('\"' + result + '\"');
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::Token> KeyboardTranslatorReader::tokenize(const QString& line)
{
QString text = line;
// remove comments
bool inQuotes = false;
int commentPos = -1;
for (int i = text.length() - 1; i >= 0; i--) {
QChar ch = text[i];
if (ch == '\"')
inQuotes = !inQuotes;
else if (ch == '#' && !inQuotes)
commentPos = i;
}
if (commentPos != -1)
text.remove(commentPos, text.length());
text = text.simplified();
// title line: keyboard "title"
static QRegExp title("keyboard\\s+\"(.*)\"");
// key line: key KeySequence : "output"
// key line: key KeySequence : command
static QRegExp key("key\\s+([\\w\\+\\s\\-\\*\\.]+)\\s*:\\s*(\"(.*)\"|\\w+)");
QList<Token> list;
if (text.isEmpty()) {
return list;
}
if (title.exactMatch(text)) {
Token titleToken = { Token::TitleKeyword , QString() };
Token textToken = { Token::TitleText , title.capturedTexts()[1] };
list << titleToken << textToken;
} else if (key.exactMatch(text)) {
Token keyToken = { Token::KeyKeyword , QString() };
Token sequenceToken = { Token::KeySequence , key.capturedTexts()[1].remove(' ') };
list << keyToken << sequenceToken;
if (key.capturedTexts()[3].isEmpty()) {
// capturedTexts()[2] is a command
Token commandToken = { Token::Command , key.capturedTexts()[2] };
list << commandToken;
} else {
// capturedTexts()[3] is the output string
Token outputToken = { Token::OutputText , key.capturedTexts()[3] };
list << outputToken;
}
} else {
kWarning() << "Line in keyboard translator file could not be understood:" << text;
}
return list;
}
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 testKeyCode,
Qt::KeyboardModifiers testKeyboardModifiers,
States testState) const
{
if (_keyCode != testKeyCode)
return false;
if ((testKeyboardModifiers & _modifierMask) != (_modifiers & _modifierMask))
return false;
// if testKeyboardModifiers is non-zero, the 'any modifier' state is implicit
if (testKeyboardModifiers != 0)
testState |= AnyModifierState;
if ((testState & _stateMask) != (_state & _stateMask))
return false;
// special handling for the 'Any Modifier' state, which checks for the presence of
// any or no modifiers. In this context, the 'keypad' modifier does not count.
bool anyModifiersSet = (testKeyboardModifiers != 0)
&& (testKeyboardModifiers != 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 keyboardModifiers) const
{
QByteArray result(text(expandWildCards, keyboardModifiers));
for (int i = 0 ; i < result.count() ; i++) {
const 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:
// any character which is not printable is replaced by an equivalent
// \xhh escape sequence (where 'hh' are the corresponding hex digits)
if (!QChar(ch).isPrint())
replacement = 'x';
}
if (replacement == 'x') {
result.replace(i, 1, "\\x" + QByteArray(1, ch).toHex());
} 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.count() - 1 ; i++) {
QByteRef 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' : {
// format is \xh or \xhh where 'h' is a hexadecimal
// digit from 0-9 or A-F which should be replaced
// with the corresponding character value
char hexDigits[3] = {0};
if ((i < result.count() - 2) && isxdigit(result[i + 2]))
hexDigits[0] = result[i + 2];
if ((i < result.count() - 3) && isxdigit(result[i + 3]))
hexDigits[1] = result[i + 3];
unsigned charValue = 0;
sscanf(hexDigits, "%2x", &charValue);
replacement[0] = static_cast<char>(charValue);
charsToRemove = 2 + qstrlen(hexDigits);
}
break;
default:
escapedChar = false;
}
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 += '+';
else
item += '-';
if (modifier == Qt::ShiftModifier)
item += "Shift";
else if (modifier == Qt::ControlModifier)
item += "Ctrl";
else if (modifier == Qt::AltModifier)
item += "Alt";
else if (modifier == Qt::MetaModifier)
item += "Meta";
else if (modifier == Qt::KeypadModifier)
item += "KeyPad";
}
void KeyboardTranslator::Entry::insertState(QString& item, int aState) const
{
if (!(aState & _stateMask))
return;
if (aState & _state)
item += '+';
else
item += '-';
if (aState == KeyboardTranslator::AlternateScreenState)
item += "AppScreen";
else if (aState == KeyboardTranslator::NewLineState)
item += "NewLine";
else if (aState == KeyboardTranslator::AnsiState)
item += "Ansi";
else if (aState == KeyboardTranslator::CursorKeysState)
item += "AppCursorKeys";
else if (aState == KeyboardTranslator::AnyModifierState)
item += "AnyModifier";
else if (aState == KeyboardTranslator::ApplicationKeypadState)
item += "AppKeypad";
}
QString KeyboardTranslator::Entry::resultToString(bool expandWildCards,
Qt::KeyboardModifiers keyboardModifiers) const
{
if (!_text.isEmpty())
return escapedText(expandWildCards, keyboardModifiers);
else if (_command == EraseCommand)
return "Erase";
else if (_command == ScrollPageUpCommand)
return "ScrollPageUp";
else if (_command == ScrollPageDownCommand)
return "ScrollPageDown";
else if (_command == ScrollLineUpCommand)
return "ScrollLineUp";
else if (_command == ScrollLineDownCommand)
return "ScrollLineDown";
else if (_command == ScrollUpToTopCommand)
return "ScrollUpToTop";
else if (_command == ScrollDownToBottomCommand)
return "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& aName)
: _name(aName)
{
}
FallbackKeyboardTranslator::FallbackKeyboardTranslator()
: KeyboardTranslator("fallback")
{
setDescription("Fallback Keyboard Translator");
// Key "TAB" should send out '\t'
KeyboardTranslator::Entry entry;
entry.setKeyCode(Qt::Key_Tab);
entry.setText("\t");
addEntry(entry);
}
void KeyboardTranslator::setDescription(const QString& aDescription)
{
_description = aDescription;
}
QString KeyboardTranslator::description() const
{
return _description;
}
void KeyboardTranslator::setName(const QString& aName)
{
_name = aName;
}
QString KeyboardTranslator::name() const
{
return _name;
}
QList<KeyboardTranslator::Entry> 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
{
foreach(const Entry & entry, _entries.values(keyCode)) {
if (entry.matches(keyCode, modifiers, state))
return entry;
}
return Entry(); // No matching entry
}