blob: f2429f24ff01a21a5cd46415b4c7aeb62365787a [file] [log] [blame]
/****************************************************************************
**
** 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 "qibusplatforminputcontext.h"
#include <QtDebug>
#include <QTextCharFormat>
#include <QGuiApplication>
#include <QDBusVariant>
#include <qwindow.h>
#include <qevent.h>
#include <qpa/qplatformcursor.h>
#include <qpa/qplatformscreen.h>
#include <qpa/qwindowsysteminterface_p.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtXkbCommonSupport/private/qxkbcommon_p.h>
#include "qibusproxy.h"
#include "qibusproxyportal.h"
#include "qibusinputcontextproxy.h"
#include "qibustypes.h"
#include <sys/types.h>
#include <signal.h>
#include <QtDBus>
#ifndef IBUS_RELEASE_MASK
#define IBUS_RELEASE_MASK (1 << 30)
#define IBUS_SHIFT_MASK (1 << 0)
#define IBUS_CONTROL_MASK (1 << 2)
#define IBUS_MOD1_MASK (1 << 3)
#define IBUS_META_MASK (1 << 28)
#endif
QT_BEGIN_NAMESPACE
enum { debug = 0 };
class QIBusPlatformInputContextPrivate
{
public:
QIBusPlatformInputContextPrivate();
~QIBusPlatformInputContextPrivate()
{
delete context;
delete bus;
delete portalBus;
delete connection;
}
static QString getSocketPath();
QDBusConnection *createConnection();
void initBus();
void createBusProxy();
QDBusConnection *connection;
QIBusProxy *bus;
QIBusProxyPortal *portalBus; // bus and portalBus are alternative.
QIBusInputContextProxy *context;
QDBusServiceWatcher serviceWatcher;
bool usePortal; // return value of shouldConnectIbusPortal
bool valid;
bool busConnected;
QString predit;
QList<QInputMethodEvent::Attribute> attributes;
bool needsSurroundingText;
QLocale locale;
};
QIBusPlatformInputContext::QIBusPlatformInputContext ()
: d(new QIBusPlatformInputContextPrivate())
{
if (!d->usePortal) {
QString socketPath = QIBusPlatformInputContextPrivate::getSocketPath();
QFile file(socketPath);
if (file.open(QFile::ReadOnly)) {
#if QT_CONFIG(filesystemwatcher)
qCDebug(qtQpaInputMethods) << "socketWatcher.addPath" << socketPath;
// If KDE session save is used or restart ibus-daemon,
// the applications could run before ibus-daemon runs.
// We watch the getSocketPath() to get the launching ibus-daemon.
m_socketWatcher.addPath(socketPath);
connect(&m_socketWatcher, SIGNAL(fileChanged(QString)), this, SLOT(socketChanged(QString)));
#endif
}
m_timer.setSingleShot(true);
connect(&m_timer, SIGNAL(timeout()), this, SLOT(connectToBus()));
}
QObject::connect(&d->serviceWatcher, SIGNAL(serviceRegistered(QString)), this, SLOT(busRegistered(QString)));
QObject::connect(&d->serviceWatcher, SIGNAL(serviceUnregistered(QString)), this, SLOT(busUnregistered(QString)));
connectToContextSignals();
QInputMethod *p = qApp->inputMethod();
connect(p, SIGNAL(cursorRectangleChanged()), this, SLOT(cursorRectChanged()));
m_eventFilterUseSynchronousMode = false;
if (qEnvironmentVariableIsSet("IBUS_ENABLE_SYNC_MODE")) {
bool ok;
int enableSync = qEnvironmentVariableIntValue("IBUS_ENABLE_SYNC_MODE", &ok);
if (ok && enableSync == 1)
m_eventFilterUseSynchronousMode = true;
}
}
QIBusPlatformInputContext::~QIBusPlatformInputContext (void)
{
delete d;
}
bool QIBusPlatformInputContext::isValid() const
{
return d->valid && d->busConnected;
}
bool QIBusPlatformInputContext::hasCapability(Capability capability) const
{
switch (capability) {
case QPlatformInputContext::HiddenTextCapability:
return false; // QTBUG-40691, do not show IME on desktop for password entry fields.
default:
break;
}
return true;
}
void QIBusPlatformInputContext::invokeAction(QInputMethod::Action a, int)
{
if (!d->busConnected)
return;
if (a == QInputMethod::Click)
commit();
}
void QIBusPlatformInputContext::reset()
{
QPlatformInputContext::reset();
if (!d->busConnected)
return;
d->context->Reset();
d->predit = QString();
d->attributes.clear();
}
void QIBusPlatformInputContext::commit()
{
QPlatformInputContext::commit();
if (!d->busConnected)
return;
QObject *input = qApp->focusObject();
if (!input) {
d->predit = QString();
d->attributes.clear();
return;
}
if (!d->predit.isEmpty()) {
QInputMethodEvent event;
event.setCommitString(d->predit);
QCoreApplication::sendEvent(input, &event);
}
d->context->Reset();
d->predit = QString();
d->attributes.clear();
}
void QIBusPlatformInputContext::update(Qt::InputMethodQueries q)
{
QObject *input = qApp->focusObject();
if (d->needsSurroundingText && input
&& (q.testFlag(Qt::ImSurroundingText)
|| q.testFlag(Qt::ImCursorPosition)
|| q.testFlag(Qt::ImAnchorPosition))) {
QInputMethodQueryEvent query(Qt::ImSurroundingText | Qt::ImCursorPosition | Qt::ImAnchorPosition);
QCoreApplication::sendEvent(input, &query);
QString surroundingText = query.value(Qt::ImSurroundingText).toString();
uint cursorPosition = query.value(Qt::ImCursorPosition).toUInt();
uint anchorPosition = query.value(Qt::ImAnchorPosition).toUInt();
QIBusText text;
text.text = surroundingText;
QVariant variant;
variant.setValue(text);
QDBusVariant dbusText(variant);
d->context->SetSurroundingText(dbusText, cursorPosition, anchorPosition);
}
QPlatformInputContext::update(q);
}
void QIBusPlatformInputContext::cursorRectChanged()
{
if (!d->busConnected)
return;
QRect r = qApp->inputMethod()->cursorRectangle().toRect();
if(!r.isValid())
return;
QWindow *inputWindow = qApp->focusWindow();
if (!inputWindow)
return;
r.moveTopLeft(inputWindow->mapToGlobal(r.topLeft()));
if (debug)
qDebug() << "microFocus" << r;
d->context->SetCursorLocation(r.x(), r.y(), r.width(), r.height());
}
void QIBusPlatformInputContext::setFocusObject(QObject *object)
{
if (!d->busConnected)
return;
// It would seem natural here to call FocusOut() on the input method if we
// transition from an IME accepted focus object to one that does not accept it.
// Mysteriously however that is not sufficient to fix bug QTBUG-63066.
if (!inputMethodAccepted())
return;
if (debug)
qDebug() << "setFocusObject" << object;
if (object)
d->context->FocusIn();
else
d->context->FocusOut();
}
void QIBusPlatformInputContext::commitText(const QDBusVariant &text)
{
QObject *input = qApp->focusObject();
if (!input)
return;
const QDBusArgument arg = text.variant().value<QDBusArgument>();
QIBusText t;
if (debug)
qDebug() << arg.currentSignature();
arg >> t;
if (debug)
qDebug() << "commit text:" << t.text;
QInputMethodEvent event;
event.setCommitString(t.text);
QCoreApplication::sendEvent(input, &event);
d->predit = QString();
d->attributes.clear();
}
void QIBusPlatformInputContext::updatePreeditText(const QDBusVariant &text, uint cursorPos, bool visible)
{
if (!qApp)
return;
QObject *input = qApp->focusObject();
if (!input)
return;
const QDBusArgument arg = text.variant().value<QDBusArgument>();
QIBusText t;
arg >> t;
if (debug)
qDebug() << "preedit text:" << t.text;
d->attributes = t.attributes.imAttributes();
if (!t.text.isEmpty())
d->attributes += QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, cursorPos, visible ? 1 : 0, QVariant());
QInputMethodEvent event(t.text, d->attributes);
QCoreApplication::sendEvent(input, &event);
d->predit = t.text;
}
void QIBusPlatformInputContext::forwardKeyEvent(uint keyval, uint keycode, uint state)
{
if (!qApp)
return;
QObject *input = qApp->focusObject();
if (!input)
return;
QEvent::Type type = QEvent::KeyPress;
if (state & IBUS_RELEASE_MASK)
type = QEvent::KeyRelease;
state &= ~IBUS_RELEASE_MASK;
keycode += 8;
Qt::KeyboardModifiers modifiers = Qt::NoModifier;
if (state & IBUS_SHIFT_MASK)
modifiers |= Qt::ShiftModifier;
if (state & IBUS_CONTROL_MASK)
modifiers |= Qt::ControlModifier;
if (state & IBUS_MOD1_MASK)
modifiers |= Qt::AltModifier;
if (state & IBUS_META_MASK)
modifiers |= Qt::MetaModifier;
int qtcode = QXkbCommon::keysymToQtKey(keyval, modifiers);
QString text = QXkbCommon::lookupStringNoKeysymTransformations(keyval);
if (debug)
qDebug() << "forwardKeyEvent" << keyval << keycode << state << modifiers << qtcode << text;
QKeyEvent event(type, qtcode, modifiers, keycode, keyval, state, text);
QCoreApplication::sendEvent(input, &event);
}
void QIBusPlatformInputContext::surroundingTextRequired()
{
if (debug)
qDebug("surroundingTextRequired");
d->needsSurroundingText = true;
update(Qt::ImSurroundingText);
}
void QIBusPlatformInputContext::deleteSurroundingText(int offset, uint n_chars)
{
QObject *input = qApp->focusObject();
if (!input)
return;
if (debug)
qDebug() << "deleteSurroundingText" << offset << n_chars;
QInputMethodEvent event;
event.setCommitString("", offset, n_chars);
QCoreApplication::sendEvent(input, &event);
}
void QIBusPlatformInputContext::hidePreeditText()
{
QObject *input = QGuiApplication::focusObject();
if (!input)
return;
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event(QString(), attributes);
QCoreApplication::sendEvent(input, &event);
}
void QIBusPlatformInputContext::showPreeditText()
{
QObject *input = QGuiApplication::focusObject();
if (!input)
return;
QInputMethodEvent event(d->predit, d->attributes);
QCoreApplication::sendEvent(input, &event);
}
bool QIBusPlatformInputContext::filterEvent(const QEvent *event)
{
if (!d->busConnected)
return false;
if (!inputMethodAccepted())
return false;
const QKeyEvent *keyEvent = static_cast<const QKeyEvent *>(event);
quint32 sym = keyEvent->nativeVirtualKey();
quint32 code = keyEvent->nativeScanCode();
quint32 state = keyEvent->nativeModifiers();
quint32 ibusState = state;
if (keyEvent->type() != QEvent::KeyPress)
ibusState |= IBUS_RELEASE_MASK;
QDBusPendingReply<bool> reply = d->context->ProcessKeyEvent(sym, code - 8, ibusState);
if (m_eventFilterUseSynchronousMode || reply.isFinished()) {
bool filtered = reply.value();
qCDebug(qtQpaInputMethods) << "filterEvent return" << code << sym << state << filtered;
return filtered;
}
Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
const int qtcode = keyEvent->key();
// From QKeyEvent::modifiers()
switch (qtcode) {
case Qt::Key_Shift:
modifiers ^= Qt::ShiftModifier;
break;
case Qt::Key_Control:
modifiers ^= Qt::ControlModifier;
break;
case Qt::Key_Alt:
modifiers ^= Qt::AltModifier;
break;
case Qt::Key_Meta:
modifiers ^= Qt::MetaModifier;
break;
case Qt::Key_AltGr:
modifiers ^= Qt::GroupSwitchModifier;
break;
}
QVariantList args;
args << QVariant::fromValue(keyEvent->timestamp());
args << QVariant::fromValue(static_cast<uint>(keyEvent->type()));
args << QVariant::fromValue(qtcode);
args << QVariant::fromValue(code) << QVariant::fromValue(sym) << QVariant::fromValue(state);
args << QVariant::fromValue(keyEvent->text());
args << QVariant::fromValue(keyEvent->isAutoRepeat());
QIBusFilterEventWatcher *watcher = new QIBusFilterEventWatcher(reply, this, QGuiApplication::focusWindow(), modifiers, args);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &QIBusPlatformInputContext::filterEventFinished);
return true;
}
void QIBusPlatformInputContext::filterEventFinished(QDBusPendingCallWatcher *call)
{
QIBusFilterEventWatcher *watcher = (QIBusFilterEventWatcher *) call;
QDBusPendingReply<bool> reply = *call;
if (reply.isError()) {
call->deleteLater();
return;
}
// Use watcher's window instead of the current focused window
// since there is a time lag until filterEventFinished() returns.
QWindow *window = watcher->window();
if (!window) {
call->deleteLater();
return;
}
Qt::KeyboardModifiers modifiers = watcher->modifiers();
QVariantList args = watcher->arguments();
const ulong time = static_cast<ulong>(args.at(0).toUInt());
const QEvent::Type type = static_cast<QEvent::Type>(args.at(1).toUInt());
const int qtcode = args.at(2).toInt();
const quint32 code = args.at(3).toUInt();
const quint32 sym = args.at(4).toUInt();
const quint32 state = args.at(5).toUInt();
const QString string = args.at(6).toString();
const bool isAutoRepeat = args.at(7).toBool();
// copied from QXcbKeyboard::handleKeyEvent()
bool filtered = reply.value();
qCDebug(qtQpaInputMethods) << "filterEventFinished return" << code << sym << state << filtered;
if (!filtered) {
#ifndef QT_NO_CONTEXTMENU
if (type == QEvent::KeyPress && qtcode == Qt::Key_Menu
&& window != NULL) {
const QPoint globalPos = window->screen()->handle()->cursor()->pos();
const QPoint pos = window->mapFromGlobal(globalPos);
QWindowSystemInterfacePrivate::ContextMenuEvent contextMenuEvent(window, false, pos,
globalPos, modifiers);
QGuiApplicationPrivate::processWindowSystemEvent(&contextMenuEvent);
}
#endif
QWindowSystemInterfacePrivate::KeyEvent keyEvent(window, time, type, qtcode, modifiers,
code, sym, state, string, isAutoRepeat);
QGuiApplicationPrivate::processWindowSystemEvent(&keyEvent);
}
call->deleteLater();
}
QLocale QIBusPlatformInputContext::locale() const
{
// d->locale is not updated when IBus portal is used
if (d->usePortal)
return QPlatformInputContext::locale();
return d->locale;
}
void QIBusPlatformInputContext::socketChanged(const QString &str)
{
qCDebug(qtQpaInputMethods) << "socketChanged";
Q_UNUSED (str);
m_timer.stop();
if (d->context)
disconnect(d->context);
if (d->bus && d->bus->isValid())
disconnect(d->bus);
if (d->connection)
d->connection->disconnectFromBus(QLatin1String("QIBusProxy"));
m_timer.start(100);
}
void QIBusPlatformInputContext::busRegistered(const QString &str)
{
qCDebug(qtQpaInputMethods) << "busRegistered";
Q_UNUSED (str);
if (d->usePortal) {
connectToBus();
}
}
void QIBusPlatformInputContext::busUnregistered(const QString &str)
{
qCDebug(qtQpaInputMethods) << "busUnregistered";
Q_UNUSED (str);
d->busConnected = false;
}
// When getSocketPath() is modified, the bus is not established yet
// so use m_timer.
void QIBusPlatformInputContext::connectToBus()
{
qCDebug(qtQpaInputMethods) << "QIBusPlatformInputContext::connectToBus";
d->initBus();
connectToContextSignals();
#if QT_CONFIG(filesystemwatcher)
if (!d->usePortal && m_socketWatcher.files().size() == 0)
m_socketWatcher.addPath(QIBusPlatformInputContextPrivate::getSocketPath());
#endif
}
void QIBusPlatformInputContext::globalEngineChanged(const QString &engine_name)
{
if (!d->bus || !d->bus->isValid())
return;
QIBusEngineDesc desc = d->bus->getGlobalEngine();
Q_ASSERT(engine_name == desc.engine_name);
QLocale locale(desc.language);
if (d->locale != locale) {
d->locale = locale;
emitLocaleChanged();
}
}
void QIBusPlatformInputContext::connectToContextSignals()
{
if (d->bus && d->bus->isValid()) {
connect(d->bus, SIGNAL(GlobalEngineChanged(QString)), this, SLOT(globalEngineChanged(QString)));
}
if (d->context) {
connect(d->context, SIGNAL(CommitText(QDBusVariant)), SLOT(commitText(QDBusVariant)));
connect(d->context, SIGNAL(UpdatePreeditText(QDBusVariant,uint,bool)), this, SLOT(updatePreeditText(QDBusVariant,uint,bool)));
connect(d->context, SIGNAL(ForwardKeyEvent(uint,uint,uint)), this, SLOT(forwardKeyEvent(uint,uint,uint)));
connect(d->context, SIGNAL(DeleteSurroundingText(int,uint)), this, SLOT(deleteSurroundingText(int,uint)));
connect(d->context, SIGNAL(RequireSurroundingText()), this, SLOT(surroundingTextRequired()));
connect(d->context, SIGNAL(HidePreeditText()), this, SLOT(hidePreeditText()));
connect(d->context, SIGNAL(ShowPreeditText()), this, SLOT(showPreeditText()));
}
}
static inline bool checkRunningUnderFlatpak()
{
return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty();
}
static bool shouldConnectIbusPortal()
{
// honor the same env as ibus-gtk
return (checkRunningUnderFlatpak() || !qgetenv("IBUS_USE_PORTAL").isNull());
}
QIBusPlatformInputContextPrivate::QIBusPlatformInputContextPrivate()
: connection(0),
bus(0),
portalBus(0),
context(0),
usePortal(shouldConnectIbusPortal()),
valid(false),
busConnected(false),
needsSurroundingText(false)
{
if (usePortal) {
valid = true;
if (debug)
qDebug() << "use IBus portal";
} else {
valid = !QStandardPaths::findExecutable(QString::fromLocal8Bit("ibus-daemon"), QStringList()).isEmpty();
}
if (!valid)
return;
initBus();
if (bus && bus->isValid()) {
QIBusEngineDesc desc = bus->getGlobalEngine();
locale = QLocale(desc.language);
}
}
void QIBusPlatformInputContextPrivate::initBus()
{
connection = createConnection();
busConnected = false;
createBusProxy();
}
void QIBusPlatformInputContextPrivate::createBusProxy()
{
if (!connection || !connection->isConnected())
return;
const char* ibusService = usePortal ? "org.freedesktop.portal.IBus" : "org.freedesktop.IBus";
QDBusReply<QDBusObjectPath> ic;
if (usePortal) {
portalBus = new QIBusProxyPortal(QLatin1String(ibusService),
QLatin1String("/org/freedesktop/IBus"),
*connection);
if (!portalBus->isValid()) {
qWarning("QIBusPlatformInputContext: invalid portal bus.");
return;
}
ic = portalBus->CreateInputContext(QLatin1String("QIBusInputContext"));
} else {
bus = new QIBusProxy(QLatin1String(ibusService),
QLatin1String("/org/freedesktop/IBus"),
*connection);
if (!bus->isValid()) {
qWarning("QIBusPlatformInputContext: invalid bus.");
return;
}
ic = bus->CreateInputContext(QLatin1String("QIBusInputContext"));
}
serviceWatcher.removeWatchedService(ibusService);
serviceWatcher.setConnection(*connection);
serviceWatcher.addWatchedService(ibusService);
if (!ic.isValid()) {
qWarning("QIBusPlatformInputContext: CreateInputContext failed.");
return;
}
context = new QIBusInputContextProxy(QLatin1String(ibusService), ic.value().path(), *connection);
if (!context->isValid()) {
qWarning("QIBusPlatformInputContext: invalid input context.");
return;
}
enum Capabilities {
IBUS_CAP_PREEDIT_TEXT = 1 << 0,
IBUS_CAP_AUXILIARY_TEXT = 1 << 1,
IBUS_CAP_LOOKUP_TABLE = 1 << 2,
IBUS_CAP_FOCUS = 1 << 3,
IBUS_CAP_PROPERTY = 1 << 4,
IBUS_CAP_SURROUNDING_TEXT = 1 << 5
};
context->SetCapabilities(IBUS_CAP_PREEDIT_TEXT|IBUS_CAP_FOCUS|IBUS_CAP_SURROUNDING_TEXT);
if (debug)
qDebug(">>>> bus connected!");
busConnected = true;
}
QString QIBusPlatformInputContextPrivate::getSocketPath()
{
QByteArray display(qgetenv("DISPLAY"));
QByteArray host = "unix";
QByteArray displayNumber = "0";
int pos = display.indexOf(':');
if (pos > 0)
host = display.left(pos);
++pos;
int pos2 = display.indexOf('.', pos);
if (pos2 > 0)
displayNumber = display.mid(pos, pos2 - pos);
else
displayNumber = display.mid(pos);
if (debug)
qDebug() << "host=" << host << "displayNumber" << displayNumber;
return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
QLatin1String("/ibus/bus/") +
QLatin1String(QDBusConnection::localMachineId()) +
QLatin1Char('-') + QString::fromLocal8Bit(host) + QLatin1Char('-') + QString::fromLocal8Bit(displayNumber);
}
QDBusConnection *QIBusPlatformInputContextPrivate::createConnection()
{
if (usePortal)
return new QDBusConnection(QDBusConnection::connectToBus(QDBusConnection::SessionBus, QLatin1String("QIBusProxy")));
QFile file(getSocketPath());
if (!file.open(QFile::ReadOnly))
return 0;
QByteArray address;
int pid = -1;
while (!file.atEnd()) {
QByteArray line = file.readLine().trimmed();
if (line.startsWith('#'))
continue;
if (line.startsWith("IBUS_ADDRESS="))
address = line.mid(sizeof("IBUS_ADDRESS=") - 1);
if (line.startsWith("IBUS_DAEMON_PID="))
pid = line.mid(sizeof("IBUS_DAEMON_PID=") - 1).toInt();
}
if (debug)
qDebug() << "IBUS_ADDRESS=" << address << "PID=" << pid;
if (address.isEmpty() || pid < 0 || kill(pid, 0) != 0)
return 0;
return new QDBusConnection(QDBusConnection::connectToBus(QString::fromLatin1(address), QLatin1String("QIBusProxy")));
}
QT_END_NAMESPACE