// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qibusplatforminputcontext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "qibusproxy.h" #include "qibusproxyportal.h" #include "qibusinputcontextproxy.h" #include "qibustypes.h" #include #include #include #include #include #include #include #include #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 using namespace Qt::StringLiterals; enum { debug = 0 }; class QIBusPlatformInputContextPrivate { Q_DISABLE_COPY_MOVE(QIBusPlatformInputContextPrivate) public: // This enum might be synced with IBusPreeditFocusMode // in ibustypes.h of IBUS project enum PreeditFocusMode { PREEDIT_CLEAR = 0, PREEDIT_COMMIT = 1, }; QIBusPlatformInputContextPrivate(); ~QIBusPlatformInputContextPrivate() { // dereference QDBusConnection to actually disconnect serviceWatcher.setConnection(QDBusConnection(QString())); context = nullptr; portalBus = nullptr; bus = nullptr; QDBusConnection::disconnectFromBus("QIBusProxy"_L1); } static QString getSocketPath(); void createConnection(); void initBus(); void createBusProxy(); std::unique_ptr bus; std::unique_ptr portalBus; // bus and portalBus are alternative. std::unique_ptr context; QDBusServiceWatcher serviceWatcher; bool usePortal; // return value of shouldConnectIbusPortal bool valid; bool busConnected; QString predit; QList attributes; bool needsSurroundingText; QLocale locale; PreeditFocusMode preeditFocusMode = PREEDIT_COMMIT; // for backward compatibility }; 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() { if (!d->busConnected) return; d->context->Reset(); d->predit = QString(); d->attributes.clear(); } void QIBusPlatformInputContext::commit() { if (!d->busConnected) return; QObject *input = qApp->focusObject(); if (!input) { d->predit = QString(); d->attributes.clear(); return; } if (d->preeditFocusMode == QIBusPlatformInputContextPrivate::PREEDIT_COMMIT) { if (!d->predit.isEmpty()) { QInputMethodEvent event; event.setCommitString(d->predit); QCoreApplication::sendEvent(input, &event); } } else { if (!d->predit.isEmpty()) { // Clear the existing preedit QInputMethodEvent event; 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); } } void QIBusPlatformInputContext::cursorRectChanged() { if (!d->busConnected) return; QRect r = qApp->inputMethod()->cursorRectangle().toRect(); if (!r.isValid()) return; QWindow *inputWindow = qApp->focusWindow(); if (!inputWindow) return; if (!inputWindow->screen()) return; if (QGuiApplication::platformName().startsWith("wayland"_L1)) { auto margins = inputWindow->frameMargins(); r.translate(margins.left(), margins.top()); qreal scale = inputWindow->devicePixelRatio(); QRect newRect = QRect(r.x() * scale, r.y() * scale, r.width() * scale, r.height() * scale); if (debug) qDebug() << "microFocus" << newRect; d->context->SetCursorLocationRelative(newRect.x(), newRect.y(), newRect.width(), newRect.height()); return; } // x11/xcb auto screenGeometry = inputWindow->screen()->geometry(); auto point = inputWindow->mapToGlobal(r.topLeft()); qreal scale = inputWindow->devicePixelRatio(); auto native = (point - screenGeometry.topLeft()) * scale + screenGeometry.topLeft(); QRect newRect(native, r.size() * scale); if (debug) qDebug() << "microFocus" << newRect; d->context->SetCursorLocation(newRect.x(), newRect.y(), newRect.width(), newRect.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 (object && !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 = qvariant_cast(text.variant()); 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 = qvariant_cast(text.variant()); 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::updatePreeditTextWithMode(const QDBusVariant &text, uint cursorPos, bool visible, uint mode) { updatePreeditText(text, cursorPos, visible); if (mode > 0) d->preeditFocusMode = QIBusPlatformInputContextPrivate::PreeditFocusMode::PREEDIT_COMMIT; else d->preeditFocusMode = QIBusPlatformInputContextPrivate::PreeditFocusMode::PREEDIT_CLEAR; } 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 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(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 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(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 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(args.at(0).toUInt()); const QEvent::Type type = static_cast(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 != nullptr) { 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(); // dereference QDBusConnection to actually disconnect d->serviceWatcher.setConnection(QDBusConnection(QString())); d->context = nullptr; d->bus = nullptr; d->busConnected = false; QDBusConnection::disconnectFromBus("QIBusProxy"_L1); 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.get(), SIGNAL(GlobalEngineChanged(QString)), this, SLOT(globalEngineChanged(QString))); } if (d->context) { connect(d->context.get(), SIGNAL(CommitText(QDBusVariant)), SLOT(commitText(QDBusVariant))); connect(d->context.get(), SIGNAL(UpdatePreeditText(QDBusVariant,uint,bool)), this, SLOT(updatePreeditText(QDBusVariant,uint,bool))); connect(d->context.get(), SIGNAL(UpdatePreeditTextWithMode(QDBusVariant,uint,bool,uint)), this, SLOT(updatePreeditTextWithMode(QDBusVariant,uint,bool,uint))); connect(d->context.get(), SIGNAL(ForwardKeyEvent(uint,uint,uint)), this, SLOT(forwardKeyEvent(uint,uint,uint))); connect(d->context.get(), SIGNAL(DeleteSurroundingText(int,uint)), this, SLOT(deleteSurroundingText(int,uint))); connect(d->context.get(), SIGNAL(RequireSurroundingText()), this, SLOT(surroundingTextRequired())); connect(d->context.get(), SIGNAL(HidePreeditText()), this, SLOT(hidePreeditText())); connect(d->context.get(), SIGNAL(ShowPreeditText()), this, SLOT(showPreeditText())); } } static inline bool checkNeedPortalSupport() { return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP"); } static bool shouldConnectIbusPortal() { // honor the same env as ibus-gtk return (checkNeedPortalSupport() || qEnvironmentVariableIsSet("IBUS_USE_PORTAL")); } QIBusPlatformInputContextPrivate::QIBusPlatformInputContextPrivate() : 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() { createConnection(); busConnected = false; createBusProxy(); } void QIBusPlatformInputContextPrivate::createBusProxy() { QDBusConnection connection("QIBusProxy"_L1); if (!connection.isConnected()) return; const char* ibusService = usePortal ? "org.freedesktop.portal.IBus" : "org.freedesktop.IBus"; QDBusReply ic; if (usePortal) { portalBus = std::make_unique(QLatin1StringView(ibusService), "/org/freedesktop/IBus"_L1, connection); if (!portalBus->isValid()) { qWarning("QIBusPlatformInputContext: invalid portal bus."); return; } ic = portalBus->CreateInputContext("QIBusInputContext"_L1); } else { bus = std::make_unique(QLatin1StringView(ibusService), "/org/freedesktop/IBus"_L1, connection); if (!bus->isValid()) { qWarning("QIBusPlatformInputContext: invalid bus."); return; } ic = bus->CreateInputContext("QIBusInputContext"_L1); } serviceWatcher.removeWatchedService(ibusService); serviceWatcher.setConnection(connection); serviceWatcher.addWatchedService(ibusService); if (!ic.isValid()) { qWarning("QIBusPlatformInputContext: CreateInputContext failed."); return; } context = std::make_unique(QLatin1StringView(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); context->setClientCommitPreedit(QIBusPropTypeClientCommitPreedit(true)); if (debug) qDebug(">>>> bus connected!"); busConnected = true; } QString QIBusPlatformInputContextPrivate::getSocketPath() { QByteArray display; QByteArray displayNumber = "0"; bool isWayland = false; if (qEnvironmentVariableIsSet("IBUS_ADDRESS_FILE")) { QByteArray path = qgetenv("IBUS_ADDRESS_FILE"); return QString::fromLocal8Bit(path); } else if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { display = qgetenv("WAYLAND_DISPLAY"); isWayland = true; } else { display = qgetenv("DISPLAY"); } QByteArray host = "unix"; if (isWayland) { displayNumber = display; } else { 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) + "/ibus/bus/"_L1 + QLatin1StringView(QDBusConnection::localMachineId()) + u'-' + QString::fromLocal8Bit(host) + u'-' + QString::fromLocal8Bit(displayNumber); } void QIBusPlatformInputContextPrivate::createConnection() { if (usePortal) { QDBusConnection::connectToBus(QDBusConnection::SessionBus, "QIBusProxy"_L1); return; } QFile file(getSocketPath()); if (!file.open(QFile::ReadOnly)) return; 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; QDBusConnection::connectToBus(QString::fromLatin1(address), "QIBusProxy"_L1); } QT_END_NAMESPACE #include "moc_qibusplatforminputcontext.cpp"