platforminputcontexts: use libxkbcommon compose key API

Our implementation of compose table parser was added on Mar, 2013.
libxkbcommon added APIs for the same thing in Oct, 2014 (ver: 0.5.0).
After removing RHEL 6.6 from the list of supported platforms we were
able to move the minimal required libxkbcommon version to 0.5.0. Now
we can use the xkbcommon-compose APIs on all supported platforms.

With this patch we can drop nearly 1000 lines of maintenance burden.
This patch fixes user reported issues with our implementation.

Known issues:

- Testing revealed that xkbcommon-compose does not support non-utf8 locales,
and that is by design - https://github.com/xkbcommon/libxkbcommon/issues/76
Our implementation did work for those locales too, but it is unclear
if anyone actually uses non-utf8 locales. It is a corner case (work-arounds
existing) and likely a configuration error on the users' system.

- Looking at the release notes for versions above 0.6.1, only one issue
that stands out. Compose input does not work on system with tr_TR.UTF-8
locale, fixed in 0.7.1. Compose input works fine when using e.g. en_US.UTF-8
locale with Turkish keyboard layout.

Note:

With Qt 5.13 we have removed Ubuntu 16.04 and openSUSE 42.3 from CI:

Ubuntu 16.04  - 0.5.0
openSUSE 42.3 - 0.6.1

CI for Qt 5.13 has:

Ubuntu 18.04  - 0.8.0
RHEL-7.4      - 0.7.1
openSUSE 15.0 - 0.8.1

Currently the minimal required libxkbcommon version in src/gui/configure.json
is set to 0.5.0, but we could bump it to 0.7.1 to avoid known issues from above,
but that is a decision for a separate patch.

[ChangeLog][plugins][platforminputcontexts] Now using libxkbcommon-compose
APIs for compose key input, instead of Qt's own implementation.

Fixes: QTBUG-42181
Fixes: QTBUG-53663
Fixes: QTBUG-48657
Change-Id: I79aafe2bc601293844066e7e5f5eddd3719c6bba
Reviewed-by: Giulio Camuffo <giulio.camuffo@kdab.com>
Reviewed-by: Johan Helsing <johan.helsing@qt.io>
This commit is contained in:
Gatis Paeglis 2014-10-24 16:31:48 +02:00
parent a34e81ab8b
commit 2065bc070d
14 changed files with 210 additions and 1051 deletions

View File

@ -41,7 +41,12 @@
#include <private/qmakearray_p.h>
#include <QtCore/QMetaMethod>
#include <QtGui/QKeyEvent>
#include <QtGui/private/qguiapplication_p.h>
#include <qpa/qplatforminputcontext.h>
#include <qpa/qplatformintegration.h>
QT_BEGIN_NAMESPACE
@ -794,4 +799,30 @@ xkb_keysym_t QXkbCommon::lookupLatinKeysym(xkb_state *state, xkb_keycode_t keyco
return sym;
}
void QXkbCommon::setXkbContext(QPlatformInputContext *inputContext, struct xkb_context *context)
{
if (!inputContext || !context)
return;
const char *const inputContextClassName = "QComposeInputContext";
const char *const normalizedSignature = "setXkbContext(xkb_context*)";
if (inputContext->objectName() != QLatin1String(inputContextClassName))
return;
static const QMetaMethod setXkbContext = [&]() {
int methodIndex = inputContext->metaObject()->indexOfMethod(normalizedSignature);
QMetaMethod method = inputContext->metaObject()->method(methodIndex);
Q_ASSERT(method.isValid());
if (!method.isValid())
qCWarning(lcXkbcommon) << normalizedSignature << "not found on" << inputContextClassName;
return method;
}();
if (!setXkbContext.isValid())
return;
setXkbContext.invoke(inputContext, Qt::DirectConnection, Q_ARG(struct xkb_context*, context));
}
QT_END_NAMESPACE

View File

@ -64,7 +64,9 @@ QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcXkbcommon)
class QEvent;
class QKeyEvent;
class QPlatformInputContext;
class QXkbCommon
{
@ -99,6 +101,8 @@ public:
return sym >= XKB_KEY_KP_Space && sym <= XKB_KEY_KP_9;
}
static void setXkbContext(QPlatformInputContext *inputContext, struct xkb_context *context);
struct XKBStateDeleter {
void operator()(struct xkb_state *state) const { return xkb_state_unref(state); }
};

View File

@ -3,18 +3,14 @@ TARGET = composeplatforminputcontextplugin
QT += core-private gui-private
SOURCES += $$PWD/qcomposeplatforminputcontextmain.cpp \
$$PWD/qcomposeplatforminputcontext.cpp \
$$PWD/generator/qtablegenerator.cpp \
$$PWD/qcomposeplatforminputcontext.cpp
HEADERS += $$PWD/qcomposeplatforminputcontext.h \
$$PWD/generator/qtablegenerator.h \
HEADERS += $$PWD/qcomposeplatforminputcontext.h
QMAKE_USE_PRIVATE += xkbcommon
include($$OUT_PWD/../../../gui/qtgui-config.pri)
DEFINES += X11_PREFIX='\\"$$QMAKE_X11_PREFIX\\"'
OTHER_FILES += $$PWD/compose.json
PLUGIN_TYPE = platforminputcontexts

View File

@ -1,658 +0,0 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qtablegenerator.h"
#include <QtCore/QByteArray>
#include <QtCore/QTextCodec>
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QStringList>
#include <QtCore/QString>
#include <QtCore/QSaveFile>
#include <QtCore/QStandardPaths>
#include <private/qcore_unix_p.h>
#include <algorithm>
#include <xkbcommon/xkbcommon.h>
#include <locale.h> // LC_CTYPE
#include <string.h> // strchr, strncmp, etc.
#include <strings.h> // strncasecmp
#include <clocale> // LC_CTYPE
static const quint32 SupportedCacheVersion = 1;
/*
In short on how and why the "Compose" file is cached:
The "Compose" file is large, for en_US it's likely located at:
/usr/share/X11/locale/en_US.UTF-8/Compose
and it has about 6000 string lines.
Q(Gui)Applications parse this file each time they're created. On modern CPUs
it incurs a 4-10 ms startup penalty of each Qt gui app, on older CPUs -
tens of ms or more.
Since the "Compose" file (almost) never changes using a pre-parsed
cache file instead of the "Compose" file is a good idea to improve Qt5
application startup time by about 5+ ms (or tens of ms on older CPUs).
The cache file contains the contents of the QComposeCacheFileHeader struct at the
beginning followed by the pre-parsed contents of the "Compose" file.
struct QComposeCacheFileHeader stores
(a) The cache version - in the unlikely event that some day one might need
to break compatibility.
(b) The (cache) file size.
(c) The lastModified field tracks if anything changed since the last time
the cache file was saved.
If anything did change then we read the compose file and save (cache) it
in binary/pre-parsed format, which should happen extremely rarely if at all.
*/
struct QComposeCacheFileHeader
{
quint32 cacheVersion;
// The compiler will add 4 padding bytes anyway.
// Reserve them explicitly to possibly use in the future.
quint32 reserved;
quint64 fileSize;
qint64 lastModified;
};
// localHostName() copied from qtbase/src/corelib/io/qlockfile_unix.cpp
static QByteArray localHostName()
{
QByteArray hostName(512, Qt::Uninitialized);
if (gethostname(hostName.data(), hostName.size()) == -1)
return QByteArray();
hostName.truncate(strlen(hostName.data()));
return hostName;
}
/*
Reads metadata about the Compose file. Later used to determine if the
compose cache should be updated. The fileSize field will be zero on failure.
*/
static QComposeCacheFileHeader readFileMetadata(const QString &path)
{
quint64 fileSize = 0;
qint64 lastModified = 0;
const QByteArray pathBytes = QFile::encodeName(path);
QT_STATBUF st;
if (QT_STAT(pathBytes.data(), &st) == 0) {
lastModified = st.st_mtime;
fileSize = st.st_size;
}
QComposeCacheFileHeader info = { 0, 0, fileSize, lastModified };
return info;
}
static const QString getCacheFilePath()
{
QFile machineIdFile("/var/lib/dbus/machine-id");
QString machineId;
if (machineIdFile.exists()) {
if (machineIdFile.open(QIODevice::ReadOnly))
machineId = QString::fromLatin1(machineIdFile.readAll().trimmed());
}
if (machineId.isEmpty())
machineId = localHostName();
const QString dirPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
if (QSysInfo::ByteOrder == QSysInfo::BigEndian)
return dirPath + QLatin1String("/qt_compose_cache_big_endian_") + machineId;
return dirPath + QLatin1String("/qt_compose_cache_little_endian_") + machineId;
}
// Returns empty vector on failure
static QVector<QComposeTableElement> loadCache(const QComposeCacheFileHeader &composeInfo)
{
QVector<QComposeTableElement> vec;
const QString cacheFilePath = getCacheFilePath();
QFile inputFile(cacheFilePath);
if (!inputFile.open(QIODevice::ReadOnly))
return vec;
QComposeCacheFileHeader cacheInfo;
// use a "buffer" variable to make the line after this one more readable.
char *buffer = reinterpret_cast<char*>(&cacheInfo);
if (inputFile.read(buffer, sizeof cacheInfo) != sizeof cacheInfo)
return vec;
if (cacheInfo.fileSize == 0)
return vec;
// using "!=" just in case someone replaced with a backup that existed before
if (cacheInfo.lastModified != composeInfo.lastModified)
return vec;
if (cacheInfo.cacheVersion != SupportedCacheVersion)
return vec;
const QByteArray pathBytes = QFile::encodeName(cacheFilePath);
QT_STATBUF st;
if (QT_STAT(pathBytes.data(), &st) != 0)
return vec;
const off_t fileSize = st.st_size;
if (fileSize > 1024 * 1024 * 5) {
// The cache file size is usually about 150KB, so if its size is over
// say 5MB then somebody inflated the file, abort.
return vec;
}
const off_t bufferSize = fileSize - (sizeof cacheInfo);
const size_t elemSize = sizeof (struct QComposeTableElement);
const int elemCount = bufferSize / elemSize;
const QByteArray ba = inputFile.read(bufferSize);
const char *data = ba.data();
// Since we know the number of the (many) elements and their size in
// advance calling vector.reserve(..) seems reasonable.
vec.reserve(elemCount);
for (int i = 0; i < elemCount; i++) {
const QComposeTableElement *elem =
reinterpret_cast<const QComposeTableElement*>(data + (i * elemSize));
vec.push_back(*elem);
}
return vec;
}
// Returns true on success, false otherwise.
static bool saveCache(const QComposeCacheFileHeader &info, const QVector<QComposeTableElement> &vec)
{
const QString filePath = getCacheFilePath();
#if QT_CONFIG(temporaryfile)
QSaveFile outputFile(filePath);
#else
QFile outputFile(filePath);
#endif
if (!outputFile.open(QIODevice::WriteOnly))
return false;
const char *data = reinterpret_cast<const char*>(&info);
if (outputFile.write(data, sizeof info) != sizeof info)
return false;
data = reinterpret_cast<const char*>(vec.constData());
const qint64 size = vec.size() * (sizeof (struct QComposeTableElement));
if (outputFile.write(data, size) != size)
return false;
#if QT_CONFIG(temporaryfile)
return outputFile.commit();
#else
return true;
#endif
}
TableGenerator::TableGenerator() : m_state(NoErrors),
m_systemComposeDir(QString())
{
initPossibleLocations();
QString composeFilePath = findComposeFile();
#ifdef DEBUG_GENERATOR
// don't use cache when in debug mode.
if (!composeFilePath.isEmpty())
qDebug() << "Using Compose file from: " << composeFilePath;
#else
QComposeCacheFileHeader fileInfo = readFileMetadata(composeFilePath);
if (fileInfo.fileSize != 0)
m_composeTable = loadCache(fileInfo);
#endif
if (m_composeTable.isEmpty() && cleanState()) {
if (composeFilePath.isEmpty()) {
m_state = MissingComposeFile;
} else {
QFile composeFile(composeFilePath);
composeFile.open(QIODevice::ReadOnly);
parseComposeFile(&composeFile);
orderComposeTable();
if (m_composeTable.isEmpty()) {
m_state = EmptyTable;
#ifndef DEBUG_GENERATOR
// don't save cache when in debug mode
} else {
fileInfo.cacheVersion = SupportedCacheVersion;
saveCache(fileInfo, m_composeTable);
#endif
}
}
}
#ifdef DEBUG_GENERATOR
printComposeTable();
#endif
}
void TableGenerator::initPossibleLocations()
{
// Compose files come as a part of Xlib library. Xlib doesn't provide
// a mechanism how to retrieve the location of these files reliably, since it was
// never meant for external software to parse compose tables directly. Best we
// can do is to hardcode search paths. To add an extra system path use
// the QTCOMPOSE environment variable
m_possibleLocations.reserve(7);
if (qEnvironmentVariableIsSet("QTCOMPOSE"))
m_possibleLocations.append(QString::fromLocal8Bit(qgetenv("QTCOMPOSE")));
m_possibleLocations.append(QStringLiteral("/usr/share/X11/locale"));
m_possibleLocations.append(QStringLiteral("/usr/local/share/X11/locale"));
m_possibleLocations.append(QStringLiteral("/usr/lib/X11/locale"));
m_possibleLocations.append(QStringLiteral("/usr/local/lib/X11/locale"));
m_possibleLocations.append(QStringLiteral(X11_PREFIX "/share/X11/locale"));
m_possibleLocations.append(QStringLiteral(X11_PREFIX "/lib/X11/locale"));
}
QString TableGenerator::findComposeFile()
{
// check if XCOMPOSEFILE points to a Compose file
if (qEnvironmentVariableIsSet("XCOMPOSEFILE")) {
const QString path = QFile::decodeName(qgetenv("XCOMPOSEFILE"));
if (QFile::exists(path))
return path;
else
qWarning("$XCOMPOSEFILE doesn't point to an existing file");
}
// check if users home directory has a file named .XCompose
if (cleanState()) {
QString path = qgetenv("HOME") + QLatin1String("/.XCompose");
if (QFile::exists(path))
return path;
}
// check for the system provided compose files
if (cleanState()) {
QString table = composeTableForLocale();
if (cleanState()) {
if (table.isEmpty())
// no table mappings for the system's locale in the compose.dir
m_state = UnsupportedLocale;
else {
QString path = QDir(systemComposeDir()).filePath(table);
if (QFile::exists(path))
return path;
}
}
}
return QString();
}
QString TableGenerator::composeTableForLocale()
{
QByteArray loc = locale().toUpper().toUtf8();
QString table = readLocaleMappings(loc);
if (table.isEmpty())
table = readLocaleMappings(readLocaleAliases(loc));
return table;
}
bool TableGenerator::findSystemComposeDir()
{
bool found = false;
for (int i = 0; i < m_possibleLocations.size(); ++i) {
QString path = m_possibleLocations.at(i);
if (QFile::exists(path + QLatin1String("/compose.dir"))) {
m_systemComposeDir = path;
found = true;
break;
}
}
if (!found) {
// should we ask to report this in the qt bug tracker?
m_state = UnknownSystemComposeDir;
qWarning("Qt Warning: Could not find a location of the system's Compose files. "
"Consider setting the QTCOMPOSE environment variable.");
}
return found;
}
QString TableGenerator::systemComposeDir()
{
if (m_systemComposeDir.isNull()
&& !findSystemComposeDir()) {
return QLatin1String("$QTCOMPOSE");
}
return m_systemComposeDir;
}
QString TableGenerator::locale() const
{
char *name = setlocale(LC_CTYPE, (char *)0);
return QLatin1String(name);
}
QString TableGenerator::readLocaleMappings(const QByteArray &locale)
{
QString file;
if (locale.isEmpty())
return file;
QFile mappings(systemComposeDir() + QLatin1String("/compose.dir"));
if (mappings.open(QIODevice::ReadOnly)) {
const int localeNameLength = locale.size();
const char * const localeData = locale.constData();
char l[1024];
// formating of compose.dir has some inconsistencies
while (!mappings.atEnd()) {
int read = mappings.readLine(l, sizeof(l));
if (read <= 0)
break;
char *line = l;
if (*line >= 'a' && *line <= 'z') {
// file name
while (*line && *line != ':' && *line != ' ' && *line != '\t')
++line;
if (!*line)
continue;
const char * const composeFileNameEnd = line;
*line = '\0';
++line;
// locale name
while (*line && (*line == ' ' || *line == '\t'))
++line;
const char * const lc = line;
while (*line && *line != ' ' && *line != '\t' && *line != '\n')
++line;
*line = '\0';
if (localeNameLength == (line - lc) && !strncasecmp(lc, localeData, line - lc)) {
file = QString::fromLocal8Bit(l, composeFileNameEnd - l);
break;
}
}
}
mappings.close();
}
return file;
}
QByteArray TableGenerator::readLocaleAliases(const QByteArray &locale)
{
QFile aliases(systemComposeDir() + QLatin1String("/locale.alias"));
QByteArray fullLocaleName;
if (aliases.open(QIODevice::ReadOnly)) {
while (!aliases.atEnd()) {
char l[1024];
int read = aliases.readLine(l, sizeof(l));
char *line = l;
if (read && ((*line >= 'a' && *line <= 'z') ||
(*line >= 'A' && *line <= 'Z'))) {
const char *alias = line;
while (*line && *line != ':' && *line != ' ' && *line != '\t')
++line;
if (!*line)
continue;
*line = 0;
if (locale.size() == (line - alias)
&& !strncasecmp(alias, locale.constData(), line - alias)) {
// found a match for alias, read the real locale name
++line;
while (*line && (*line == ' ' || *line == '\t'))
++line;
const char *fullName = line;
while (*line && *line != ' ' && *line != '\t' && *line != '\n')
++line;
*line = 0;
fullLocaleName = fullName;
#ifdef DEBUG_GENERATOR
qDebug() << "Alias for: " << alias << "is: " << fullLocaleName;
break;
#endif
}
}
}
aliases.close();
}
return fullLocaleName;
}
bool TableGenerator::processFile(const QString &composeFileName)
{
QFile composeFile(composeFileName);
if (composeFile.open(QIODevice::ReadOnly)) {
parseComposeFile(&composeFile);
return true;
}
qWarning() << QString(QLatin1String("Qt Warning: Compose file: \"%1\" can't be found"))
.arg(composeFile.fileName());
return false;
}
TableGenerator::~TableGenerator()
{
}
QVector<QComposeTableElement> TableGenerator::composeTable() const
{
return m_composeTable;
}
void TableGenerator::parseComposeFile(QFile *composeFile)
{
#ifdef DEBUG_GENERATOR
qDebug() << "TableGenerator::parseComposeFile: " << composeFile->fileName();
#endif
char line[1024];
while (!composeFile->atEnd()) {
composeFile->readLine(line, sizeof(line));
if (*line == '<')
parseKeySequence(line);
else if (!strncmp(line, "include", 7))
parseIncludeInstruction(QString::fromLocal8Bit(line));
}
composeFile->close();
}
void TableGenerator::parseIncludeInstruction(QString line)
{
// Parse something that looks like:
// include "/usr/share/X11/locale/en_US.UTF-8/Compose"
QString quote = QStringLiteral("\"");
line.remove(0, line.indexOf(quote) + 1);
line.chop(line.length() - line.indexOf(quote));
// expand substitutions if present
line.replace(QLatin1String("%H"), QString(qgetenv("HOME")));
line.replace(QLatin1String("%L"), systemComposeDir() + QLatin1Char('/') + composeTableForLocale());
line.replace(QLatin1String("%S"), systemComposeDir());
processFile(line);
}
ushort TableGenerator::keysymToUtf8(quint32 sym)
{
QByteArray chars;
int bytes;
chars.resize(8);
bytes = xkb_keysym_to_utf8(sym, chars.data(), chars.size());
if (bytes == -1)
qWarning("TableGenerator::keysymToUtf8 - buffer too small");
chars.resize(bytes-1);
#ifdef DEBUG_GENERATOR
QTextCodec *codec = QTextCodec::codecForLocale();
qDebug() << QString("keysym - 0x%1 : utf8 - %2").arg(QString::number(sym, 16))
.arg(codec->toUnicode(chars));
#endif
return QString::fromUtf8(chars).at(0).unicode();
}
static inline int fromBase8(const char *s, const char *end)
{
int result = 0;
while (*s && s != end) {
if (*s < '0' || *s > '7')
return 0;
result *= 8;
result += *s - '0';
++s;
}
return result;
}
static inline int fromBase16(const char *s, const char *end)
{
int result = 0;
while (*s && s != end) {
result *= 16;
if (*s >= '0' && *s <= '9')
result += *s - '0';
else if (*s >= 'a' && *s <= 'f')
result += *s - 'a' + 10;
else if (*s >= 'A' && *s <= 'F')
result += *s - 'A' + 10;
else
return 0;
++s;
}
return result;
}
void TableGenerator::parseKeySequence(char *line)
{
// we are interested in the lines with the following format:
// <Multi_key> <numbersign> <S> : "♬" U266c # BEAMED SIXTEENTH NOTE
char *keysEnd = strchr(line, ':');
if (!keysEnd)
return;
QComposeTableElement elem;
// find the composed value - strings may be direct text encoded in the locale
// for which the compose file is to be used, or an escaped octal or hexadecimal
// character code. Octal codes are specified as "\123" and hexadecimal codes as "\0x123a".
char *composeValue = strchr(keysEnd, '"');
if (!composeValue)
return;
++composeValue;
char *composeValueEnd = strchr(composeValue, '"');
if (!composeValueEnd)
return;
// if composed value is a quotation mark adjust the end pointer
if (composeValueEnd[1] == '"')
++composeValueEnd;
if (*composeValue == '\\' && composeValue[1] >= '0' && composeValue[1] <= '9') {
// handle octal and hex code values
char detectBase = composeValue[2];
if (detectBase == 'x') {
// hexadecimal character code
elem.value = keysymToUtf8(fromBase16(composeValue + 3, composeValueEnd));
} else {
// octal character code
elem.value = keysymToUtf8(fromBase8(composeValue + 1, composeValueEnd));
}
} else {
// handle direct text encoded in the locale
if (*composeValue == '\\')
++composeValue;
elem.value = QString::fromLocal8Bit(composeValue, composeValueEnd - composeValue).at(0).unicode();
++composeValue;
}
#ifdef DEBUG_GENERATOR
// find the comment
elem.comment = QString::fromLocal8Bit(composeValueEnd + 1).trimmed();
#endif
// find the key sequence and convert to X11 keysym
char *k = line;
const char *kend = keysEnd;
for (int i = 0; i < QT_KEYSEQUENCE_MAX_LEN; i++) {
// find the next pair of angle brackets and get the contents within
while (k < kend && *k != '<')
++k;
char *sym = ++k;
while (k < kend && *k != '>')
++k;
*k = '\0';
if (k < kend) {
elem.keys[i] = xkb_keysym_from_name(sym, (xkb_keysym_flags)0);
if (elem.keys[i] == XKB_KEY_NoSymbol) {
if (!strcmp(sym, "dead_inverted_breve"))
elem.keys[i] = XKB_KEY_dead_invertedbreve;
else if (!strcmp(sym, "dead_double_grave"))
elem.keys[i] = XKB_KEY_dead_doublegrave;
#ifdef DEBUG_GENERATOR
else
qWarning() << QString("Qt Warning - invalid keysym: %1").arg(sym);
#endif
}
} else {
elem.keys[i] = 0;
}
}
m_composeTable.append(elem);
}
void TableGenerator::printComposeTable() const
{
#ifdef DEBUG_GENERATOR
# ifndef QT_NO_DEBUG_STREAM
if (m_composeTable.isEmpty())
return;
QDebug ds = qDebug() << "output:\n";
ds.nospace();
const int tableSize = m_composeTable.size();
for (int i = 0; i < tableSize; ++i) {
const QComposeTableElement &elem = m_composeTable.at(i);
ds << "{ {";
for (int j = 0; j < QT_KEYSEQUENCE_MAX_LEN; j++) {
ds << hex << showbase << elem.keys[j] << ", ";
}
ds << "}, " << hex << showbase << elem.value << ", \"\" }, // " << elem.comment << " \n";
}
# endif
#endif
}
void TableGenerator::orderComposeTable()
{
// Stable-sorting to ensure that the item that appeared before the other in the
// original container will still appear first after the sort. This property is
// needed to handle the cases when user re-defines already defined key sequence
std::stable_sort(m_composeTable.begin(), m_composeTable.end(), ByKeys());
}

View File

@ -1,145 +0,0 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QTABLEGENERATOR_H
#define QTABLEGENERATOR_H
#include <QtCore/QVector>
#include <QtCore/QFile>
#include <QtCore/QMap>
#include <QtCore/QString>
#include <algorithm>
static Q_CONSTEXPR int QT_KEYSEQUENCE_MAX_LEN = 6;
//#define DEBUG_GENERATOR
/* Whenever QComposeTableElement gets modified supportedCacheVersion
from qtablegenerator.cpp must be bumped. */
struct QComposeTableElement {
uint keys[QT_KEYSEQUENCE_MAX_LEN];
uint value;
#ifdef DEBUG_GENERATOR
QString comment;
#endif
};
#ifndef DEBUG_GENERATOR
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(QComposeTableElement, Q_PRIMITIVE_TYPE);
QT_END_NAMESPACE
#endif
struct ByKeys
{
using uint_array = uint[QT_KEYSEQUENCE_MAX_LEN];
using result_type = bool;
bool operator()(const uint_array &lhs, const uint_array &rhs) const Q_DECL_NOTHROW
{
return std::lexicographical_compare(lhs, lhs + QT_KEYSEQUENCE_MAX_LEN,
rhs, rhs + QT_KEYSEQUENCE_MAX_LEN);
}
bool operator()(const uint_array &lhs, const QComposeTableElement &rhs) const Q_DECL_NOTHROW
{
return operator()(lhs, rhs.keys);
}
bool operator()(const QComposeTableElement &lhs, const uint_array &rhs) const Q_DECL_NOTHROW
{
return operator()(lhs.keys, rhs);
}
bool operator()(const QComposeTableElement &lhs, const QComposeTableElement &rhs) const Q_DECL_NOTHROW
{
return operator()(lhs.keys, rhs.keys);
}
};
class TableGenerator
{
public:
enum TableState
{
UnsupportedLocale,
EmptyTable,
UnknownSystemComposeDir,
MissingComposeFile,
NoErrors
};
TableGenerator();
~TableGenerator();
void parseComposeFile(QFile *composeFile);
void printComposeTable() const;
void orderComposeTable();
QVector<QComposeTableElement> composeTable() const;
TableState tableState() const { return m_state; }
protected:
bool processFile(const QString &composeFileName);
void parseKeySequence(char *line);
void parseIncludeInstruction(QString line);
QString findComposeFile();
bool findSystemComposeDir();
QString systemComposeDir();
QString composeTableForLocale();
ushort keysymToUtf8(quint32 sym);
QString readLocaleMappings(const QByteArray &locale);
QByteArray readLocaleAliases(const QByteArray &locale);
void initPossibleLocations();
bool cleanState() const { return m_state == NoErrors; }
QString locale() const;
private:
QVector<QComposeTableElement> m_composeTable;
TableState m_state;
QString m_systemComposeDir;
QList<QString> m_possibleLocations;
};
#endif // QTABLEGENERATOR_H

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
@ -36,131 +36,100 @@
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qcomposeplatforminputcontext.h"
#include <QtCore/QCoreApplication>
#include <QtGui/QKeyEvent>
#include <QtCore/QDebug>
#include <algorithm>
#include <locale.h>
QT_BEGIN_NAMESPACE
//#define DEBUG_COMPOSING
static const int ignoreKeys[] = {
Qt::Key_Shift,
Qt::Key_Control,
Qt::Key_Meta,
Qt::Key_Alt,
Qt::Key_CapsLock,
Qt::Key_Super_L,
Qt::Key_Super_R,
Qt::Key_Hyper_L,
Qt::Key_Hyper_R,
Qt::Key_Mode_switch
};
static const int composingKeys[] = {
Qt::Key_Multi_key,
Qt::Key_Dead_Grave,
Qt::Key_Dead_Acute,
Qt::Key_Dead_Circumflex,
Qt::Key_Dead_Tilde,
Qt::Key_Dead_Macron,
Qt::Key_Dead_Breve,
Qt::Key_Dead_Abovedot,
Qt::Key_Dead_Diaeresis,
Qt::Key_Dead_Abovering,
Qt::Key_Dead_Doubleacute,
Qt::Key_Dead_Caron,
Qt::Key_Dead_Cedilla,
Qt::Key_Dead_Ogonek,
Qt::Key_Dead_Iota,
Qt::Key_Dead_Voiced_Sound,
Qt::Key_Dead_Semivoiced_Sound,
Qt::Key_Dead_Belowdot,
Qt::Key_Dead_Hook,
Qt::Key_Dead_Horn,
Qt::Key_Dead_Stroke,
Qt::Key_Dead_Abovecomma,
Qt::Key_Dead_Abovereversedcomma,
Qt::Key_Dead_Doublegrave,
Qt::Key_Dead_Belowring,
Qt::Key_Dead_Belowmacron,
Qt::Key_Dead_Belowcircumflex,
Qt::Key_Dead_Belowtilde,
Qt::Key_Dead_Belowbreve,
Qt::Key_Dead_Belowdiaeresis,
Qt::Key_Dead_Invertedbreve,
Qt::Key_Dead_Belowcomma,
Qt::Key_Dead_Currency,
Qt::Key_Dead_a,
Qt::Key_Dead_A,
Qt::Key_Dead_e,
Qt::Key_Dead_E,
Qt::Key_Dead_i,
Qt::Key_Dead_I,
Qt::Key_Dead_o,
Qt::Key_Dead_O,
Qt::Key_Dead_u,
Qt::Key_Dead_U,
Qt::Key_Dead_Small_Schwa,
Qt::Key_Dead_Capital_Schwa,
Qt::Key_Dead_Greek,
Qt::Key_Dead_Lowline,
Qt::Key_Dead_Aboveverticalline,
Qt::Key_Dead_Belowverticalline,
Qt::Key_Dead_Longsolidusoverlay
};
Q_LOGGING_CATEGORY(lcXkbCompose, "qt.xkb.compose")
QComposeInputContext::QComposeInputContext()
: m_tableState(TableGenerator::EmptyTable)
, m_compositionTableInitialized(false)
{
clearComposeBuffer();
setObjectName(QStringLiteral("QComposeInputContext"));
qCDebug(lcXkbCompose, "using xkb compose input context");
}
QComposeInputContext::~QComposeInputContext()
{
xkb_compose_state_unref(m_composeState);
xkb_compose_table_unref(m_composeTable);
}
void QComposeInputContext::ensureInitialized()
{
if (m_initialized)
return;
if (!m_XkbContext) {
qCWarning(lcXkbCompose) << "error: xkb context has not been set on" << metaObject()->className();
return;
}
m_initialized = true;
const char *const locale = setlocale(LC_CTYPE, "");
qCDebug(lcXkbCompose) << "detected locale (LC_CTYPE):" << locale;
m_composeTable = xkb_compose_table_new_from_locale(m_XkbContext, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
if (m_composeTable)
m_composeState = xkb_compose_state_new(m_composeTable, XKB_COMPOSE_STATE_NO_FLAGS);
if (!m_composeTable) {
qCWarning(lcXkbCompose, "failed to create compose table");
return;
}
if (!m_composeState) {
qCWarning(lcXkbCompose, "failed to create compose state");
return;
}
}
bool QComposeInputContext::filterEvent(const QEvent *event)
{
const QKeyEvent *keyEvent = (const QKeyEvent *)event;
// should pass only the key presses
if (keyEvent->type() != QEvent::KeyPress) {
return false;
}
// if there were errors when generating the compose table input
// context should not try to filter anything, simply return false
if (m_compositionTableInitialized && (m_tableState & TableGenerator::NoErrors) != TableGenerator::NoErrors)
auto keyEvent = static_cast<const QKeyEvent *>(event);
if (keyEvent->type() != QEvent::KeyPress)
return false;
int keyval = keyEvent->key();
int keysym = 0;
if (ignoreKey(keyval))
if (!inputMethodAccepted())
return false;
if (!composeKey(keyval) && keyEvent->text().isEmpty())
// lazy initialization - we don't want to do this on an app startup
ensureInitialized();
if (!m_composeTable || !m_composeState)
return false;
keysym = keyEvent->nativeVirtualKey();
xkb_compose_state_feed(m_composeState, keyEvent->nativeVirtualKey());
int nCompose = 0;
while (nCompose < QT_KEYSEQUENCE_MAX_LEN && m_composeBuffer[nCompose] != 0)
nCompose++;
if (nCompose == QT_KEYSEQUENCE_MAX_LEN) {
reset();
nCompose = 0;
}
m_composeBuffer[nCompose] = keysym;
// check sequence
if (checkComposeTable())
switch (xkb_compose_state_get_status(m_composeState)) {
case XKB_COMPOSE_COMPOSING:
return true;
case XKB_COMPOSE_CANCELLED:
reset();
return false;
case XKB_COMPOSE_COMPOSED:
{
const int size = xkb_compose_state_get_utf8(m_composeState, nullptr, 0);
QVarLengthArray<char, 32> buffer(size + 1);
xkb_compose_state_get_utf8(m_composeState, buffer.data(), buffer.size());
QString composedText = QString::fromUtf8(buffer.constData());
return false;
QInputMethodEvent event;
event.setCommitString(composedText);
QCoreApplication::sendEvent(m_focusObject, &event);
reset();
return true;
}
case XKB_COMPOSE_NOTHING:
return false;
default:
Q_UNREACHABLE();
return false;
}
}
bool QComposeInputContext::isValid() const
@ -175,7 +144,8 @@ void QComposeInputContext::setFocusObject(QObject *object)
void QComposeInputContext::reset()
{
clearComposeBuffer();
if (m_composeState)
xkb_compose_state_reset(m_composeState);
}
void QComposeInputContext::update(Qt::InputMethodQueries q)
@ -183,125 +153,4 @@ void QComposeInputContext::update(Qt::InputMethodQueries q)
QPlatformInputContext::update(q);
}
static bool isDuplicate(const QComposeTableElement &lhs, const QComposeTableElement &rhs)
{
return std::equal(lhs.keys, lhs.keys + QT_KEYSEQUENCE_MAX_LEN,
QT_MAKE_CHECKED_ARRAY_ITERATOR(rhs.keys, QT_KEYSEQUENCE_MAX_LEN));
}
bool QComposeInputContext::checkComposeTable()
{
if (!m_compositionTableInitialized) {
TableGenerator reader;
m_tableState = reader.tableState();
m_compositionTableInitialized = true;
if ((m_tableState & TableGenerator::NoErrors) == TableGenerator::NoErrors) {
m_composeTable = reader.composeTable();
} else {
#ifdef DEBUG_COMPOSING
qDebug( "### FAILED_PARSING ###" );
#endif
// if we have errors, don' try to look things up anyways.
reset();
return false;
}
}
Q_ASSERT(!m_composeTable.isEmpty());
QVector<QComposeTableElement>::const_iterator it =
std::lower_bound(m_composeTable.constBegin(), m_composeTable.constEnd(), m_composeBuffer, ByKeys());
// prevent dereferencing an 'end' iterator, which would result in a crash
if (it == m_composeTable.constEnd())
it -= 1;
QComposeTableElement elem = *it;
// would be nicer if qLowerBound had API that tells if the item was actually found
if (m_composeBuffer[0] != elem.keys[0]) {
#ifdef DEBUG_COMPOSING
qDebug( "### no match ###" );
#endif
reset();
return false;
}
// check if compose buffer is matched
for (int i=0; i < QT_KEYSEQUENCE_MAX_LEN; i++) {
// check if partial match
if (m_composeBuffer[i] == 0 && elem.keys[i]) {
#ifdef DEBUG_COMPOSING
qDebug("### partial match ###");
#endif
return true;
}
if (m_composeBuffer[i] != elem.keys[i]) {
#ifdef DEBUG_COMPOSING
qDebug("### different entry ###");
#endif
reset();
return i != 0;
}
}
#ifdef DEBUG_COMPOSING
qDebug("### match exactly ###");
#endif
// check if the key sequence is overwriten - see the comment in
// TableGenerator::orderComposeTable()
int next = 1;
do {
// if we are at the end of the table, then we have nothing to do here
if (it + next != m_composeTable.constEnd()) {
QComposeTableElement nextElem = *(it + next);
if (isDuplicate(elem, nextElem)) {
elem = nextElem;
next++;
continue;
} else {
break;
}
}
break;
} while (true);
commitText(elem.value);
reset();
return true;
}
void QComposeInputContext::commitText(uint character) const
{
QInputMethodEvent event;
event.setCommitString(QChar(character));
QCoreApplication::sendEvent(m_focusObject, &event);
}
bool QComposeInputContext::ignoreKey(int keyval) const
{
for (uint i = 0; i < (sizeof(ignoreKeys) / sizeof(ignoreKeys[0])); i++)
if (keyval == ignoreKeys[i])
return true;
return false;
}
bool QComposeInputContext::composeKey(int keyval) const
{
for (uint i = 0; i < (sizeof(composingKeys) / sizeof(composingKeys[0])); i++)
if (keyval == composingKeys[i])
return true;
return false;
}
void QComposeInputContext::clearComposeBuffer()
{
for (uint i=0; i < (sizeof(m_composeBuffer) / sizeof(int)); i++)
m_composeBuffer[i] = 0;
}
QComposeInputContext::~QComposeInputContext() {}
QT_END_NAMESPACE

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
@ -36,24 +36,24 @@
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QCOMPOSEPLATFORMINPUTCONTEXT_H
#define QCOMPOSEPLATFORMINPUTCONTEXT_H
#include <QtCore/QLoggingCategory>
#include <qpa/qplatforminputcontext.h>
#include <QtCore/QList>
#include "generator/qtablegenerator.h"
#include <xkbcommon/xkbcommon-compose.h>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcXkbCompose)
class QEvent;
class QComposeInputContext : public QPlatformInputContext
{
Q_OBJECT
public:
QComposeInputContext();
~QComposeInputContext();
@ -62,21 +62,22 @@ public:
void setFocusObject(QObject *object) override;
void reset() override;
void update(Qt::InputMethodQueries) override;
bool filterEvent(const QEvent *event) override;
// This invokable is called from QXkbCommon::setXkbContext().
Q_INVOKABLE void setXkbContext(struct xkb_context *context) { m_XkbContext = context; }
protected:
void clearComposeBuffer();
bool ignoreKey(int keyval) const;
bool composeKey(int keyval) const;
bool checkComposeTable();
void commitText(uint character) const;
void ensureInitialized();
private:
QObject *m_focusObject;
QVector<QComposeTableElement> m_composeTable;
uint m_composeBuffer[QT_KEYSEQUENCE_MAX_LEN];
TableGenerator::TableState m_tableState;
bool m_compositionTableInitialized;
bool m_initialized = false;
xkb_context *m_context = nullptr;
xkb_compose_table *m_composeTable = nullptr;
xkb_compose_state *m_composeState = nullptr;
QObject *m_focusObject = nullptr;
struct xkb_context *m_XkbContext = nullptr;
};
QT_END_NAMESPACE

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
@ -61,7 +61,7 @@ QComposeInputContext *QComposePlatformInputContextPlugin::create(const QString &
if (system.compare(system, QLatin1String("compose"), Qt::CaseInsensitive) == 0
|| system.compare(system, QLatin1String("xim"), Qt::CaseInsensitive) == 0)
return new QComposeInputContext;
return 0;
return nullptr;
}
QT_END_NAMESPACE

View File

@ -357,6 +357,8 @@ void QXcbIntegration::initialize()
m_inputContext.reset(QPlatformInputContextFactory::create(icStr));
if (!m_inputContext && icStr != defaultInputContext && icStr != QLatin1String("none"))
m_inputContext.reset(QPlatformInputContextFactory::create(defaultInputContext));
defaultConnection()->keyboard()->initialize();
}
void QXcbIntegration::moveToScreen(QWindow *window, int screen)

View File

@ -565,6 +565,12 @@ QXcbKeyboard::~QXcbKeyboard()
xcb_key_symbols_free(m_key_symbols);
}
void QXcbKeyboard::initialize()
{
auto inputContext = QGuiApplicationPrivate::platformIntegration()->inputContext();
QXkbCommon::setXkbContext(inputContext, m_xkbContext.get());
}
void QXcbKeyboard::selectEvents()
{
#if QT_CONFIG(xkb)

View File

@ -63,6 +63,7 @@ public:
~QXcbKeyboard();
void initialize();
void selectEvents();
void handleKeyPressEvent(const xcb_key_press_event_t *event);

View File

@ -67,3 +67,8 @@ winrt|!qtHaveModule(gui)|!qtConfig(accessibility): SUBDIRS -= qaccessibility
android: SUBDIRS += \
android
qtConfig(xkbcommon): {
SUBDIRS += \
xkbkeyboard
}

View File

@ -0,0 +1,60 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtCore>
#include <QtGui>
#include <QtTest>
#include <qpa/qplatforminputcontextfactory_p.h>
#include <qpa/qplatforminputcontext.h>
class tst_XkbKeyboard : public QObject
{
Q_OBJECT
private slots:
void verifyComposeInputContextInterface();
};
void tst_XkbKeyboard::verifyComposeInputContextInterface()
{
QPlatformInputContext *inputContext = QPlatformInputContextFactory::create(QStringLiteral("compose"));
QVERIFY(inputContext);
const char *const inputContextClassName = "QComposeInputContext";
const char *const normalizedSignature = "setXkbContext(xkb_context*)";
QVERIFY(inputContext->objectName() == QLatin1String(inputContextClassName));
int methodIndex = inputContext->metaObject()->indexOfMethod(normalizedSignature);
QMetaMethod method = inputContext->metaObject()->method(methodIndex);
Q_ASSERT(method.isValid());
}
QTEST_MAIN(tst_XkbKeyboard)
#include "tst_xkbkeyboard.moc"

View File

@ -0,0 +1,7 @@
CONFIG += testcase
TARGET = tst_xkbkeyboard
SOURCES += tst_xkbkeyboard.cpp
QT = core-private gui-private testlib