/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2016 Martin Gräßlin SPDX-License-Identifier: GPL-2.0-or-later */ #include "inputmethod.h" #include "config-kwin.h" #include "input.h" #include "input_event.h" #include "inputpanelv1window.h" #include "keyboard_input.h" #include "utils/common.h" #include "virtualkeyboard_dbus.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #if KWIN_BUILD_SCREENLOCKER #include "screenlockerwatcher.h" #endif #include "pointer_input.h" #include "tablet_input.h" #include "touch_input.h" #include "wayland/display.h" #include "wayland/inputmethod_v1.h" #include "wayland/keyboard.h" #include "wayland/seat.h" #include "wayland/surface.h" #include "wayland/textinput_v1.h" #include "wayland/textinput_v3.h" #include "xkb.h" #include #include #include #include #include #include #include #include #include #include #include namespace KWin { static std::vector textToKey(const QString &text) { if (text.isEmpty()) { return {}; } auto sequence = QKeySequence::fromString(text); if (sequence.isEmpty()) { return {}; } const QList syms = KWin::Xkb::keysymsFromQtKey(sequence[0]); if (syms.empty()) { return {}; } std::optional keyCode; for (xkb_keysym_t sym : syms) { auto code = input()->keyboard()->xkb()->keycodeFromKeysym(sym); if (code) { keyCode = code->first; break; } } if (!keyCode) { return {}; } if (text.isUpper()) { return {KEY_LEFTSHIFT, quint32(keyCode.value())}; } return {quint32(keyCode.value())}; } InputMethod::InputMethod() { m_enabled = kwinApp()->config()->group(QStringLiteral("Wayland")).readEntry("VirtualKeyboardEnabled", true); // this is actually too late. Other processes are started before init, // so might miss the availability of text input // but without Workspace we don't have the window listed at all if (workspace()) { init(); } else { connect(kwinApp(), &Application::workspaceCreated, this, &InputMethod::init); } } InputMethod::~InputMethod() { stopInputMethod(); } void InputMethod::init() { // Stop restarting the input method if it starts crashing very frequently m_inputMethodCrashTimer.setInterval(20000); m_inputMethodCrashTimer.setSingleShot(true); connect(&m_inputMethodCrashTimer, &QTimer::timeout, this, [this] { m_inputMethodCrashes = 0; }); #if KWIN_BUILD_SCREENLOCKER connect(kwinApp()->screenLockerWatcher(), &ScreenLockerWatcher::aboutToLock, this, &InputMethod::hide); #endif new VirtualKeyboardDBus(this); qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the DBus interface"; if (waylandServer()) { new TextInputManagerV1Interface(waylandServer()->display(), this); new TextInputManagerV2Interface(waylandServer()->display(), this); new TextInputManagerV3Interface(waylandServer()->display(), this); connect(waylandServer()->seat(), &SeatInterface::focusedKeyboardSurfaceAboutToChange, this, &InputMethod::commitPendingText); connect(waylandServer()->seat(), &SeatInterface::focusedTextInputSurfaceChanged, this, &InputMethod::handleFocusedSurfaceChanged); TextInputV1Interface *textInputV1 = waylandServer()->seat()->textInputV1(); connect(textInputV1, &TextInputV1Interface::requestShowInputPanel, this, &InputMethod::show); connect(textInputV1, &TextInputV1Interface::requestHideInputPanel, this, &InputMethod::hide); connect(textInputV1, &TextInputV1Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); connect(textInputV1, &TextInputV1Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); connect(textInputV1, &TextInputV1Interface::stateUpdated, this, &InputMethod::textInputInterfaceV1StateUpdated); connect(textInputV1, &TextInputV1Interface::reset, this, &InputMethod::textInputInterfaceV1Reset); connect(textInputV1, &TextInputV1Interface::invokeAction, this, &InputMethod::invokeAction); connect(textInputV1, &TextInputV1Interface::enabledChanged, this, &InputMethod::textInputInterfaceV1EnabledChanged); TextInputV2Interface *textInputV2 = waylandServer()->seat()->textInputV2(); connect(textInputV2, &TextInputV2Interface::requestShowInputPanel, this, &InputMethod::show); connect(textInputV2, &TextInputV2Interface::requestHideInputPanel, this, &InputMethod::hide); connect(textInputV2, &TextInputV2Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); connect(textInputV2, &TextInputV2Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); connect(textInputV2, &TextInputV2Interface::stateUpdated, this, &InputMethod::textInputInterfaceV2StateUpdated); connect(textInputV2, &TextInputV2Interface::enabledChanged, this, &InputMethod::textInputInterfaceV2EnabledChanged); TextInputV3Interface *textInputV3 = waylandServer()->seat()->textInputV3(); connect(textInputV3, &TextInputV3Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); connect(textInputV3, &TextInputV3Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); connect(textInputV3, &TextInputV3Interface::stateCommitted, this, &InputMethod::stateCommitted); connect(textInputV3, &TextInputV3Interface::enabledChanged, this, &InputMethod::textInputInterfaceV3EnabledChanged); connect(textInputV3, &TextInputV3Interface::enableRequested, this, &InputMethod::textInputInterfaceV3EnableRequested); connect(input()->keyboard()->xkb(), &Xkb::modifierStateChanged, this, [this]() { m_hasPendingModifiers = true; }); } } void InputMethod::show() { m_shouldShowPanel = true; if (m_panel) { m_panel->show(); updateInputPanelState(); } else { if (!isActive()) { refreshActive(); } // refreshActive affects the result of isActive if (isActive()) { adoptInputMethodContext(); } } } void InputMethod::hide() { m_shouldShowPanel = false; if (m_panel) { m_panel->hide(); updateInputPanelState(); } } bool InputMethod::shouldShowOnActive() const { static bool alwaysShowIm = qEnvironmentVariableIntValue("KWIN_IM_SHOW_ALWAYS") != 0; return alwaysShowIm || input()->touch() == input()->lastInputHandler() || input()->tablet() == input()->lastInputHandler(); } void InputMethod::refreshActive() { auto seat = waylandServer()->seat(); auto t1 = seat->textInputV1(); auto t2 = seat->textInputV2(); auto t3 = seat->textInputV3(); bool active = false; if (auto focusedSurface = seat->focusedTextInputSurface()) { auto client = focusedSurface->client(); if ((t1->clientSupportsTextInput(client) && t1->isEnabled()) || (t2->clientSupportsTextInput(client) && t2->isEnabled()) || (t3->clientSupportsTextInput(client) && t3->isEnabled())) { active = true; } } setActive(active); } void InputMethod::commitPendingText() { if (!m_pendingText.isEmpty()) { commitString(m_serial++, m_pendingText); m_pendingText = QString(); auto imContext = waylandServer()->inputMethod()->context(); if (imContext) { imContext->sendReset(); } } } void InputMethod::setActive(bool active) { const bool wasActive = waylandServer()->inputMethod()->context(); if (wasActive && !active) { waylandServer()->inputMethod()->sendDeactivate(); } if (active) { if (!m_enabled) { return; } if (!wasActive) { waylandServer()->inputMethod()->sendActivate(); } adoptInputMethodContext(); } else { updateInputPanelState(); } if (wasActive != isActive()) { Q_EMIT activeChanged(active); } } InputPanelV1Window *InputMethod::panel() const { return m_panel; } void InputMethod::setPanel(InputPanelV1Window *panel) { Q_ASSERT(panel->isInputMethod()); if (m_panel) { qCWarning(KWIN_VIRTUALKEYBOARD) << "Replacing input panel" << m_panel << "with" << panel; disconnect(m_panel, nullptr, this, nullptr); } m_panel = panel; connect(panel, &Window::closed, this, [this]() { if (m_trackedWindow) { m_trackedWindow->setVirtualKeyboardGeometry({}); } }); connect(m_panel, &Window::frameGeometryChanged, this, &InputMethod::updateInputPanelState); connect(m_panel, &Window::hiddenChanged, this, &InputMethod::updateInputPanelState); connect(m_panel, &Window::closed, this, &InputMethod::updateInputPanelState); connect(m_panel, &Window::hiddenChanged, this, &InputMethod::visibleChanged); connect(m_panel, &Window::closed, this, &InputMethod::visibleChanged); Q_EMIT visibleChanged(); updateInputPanelState(); Q_EMIT panelChanged(); if (m_shouldShowPanel) { show(); } } void InputMethod::setTrackedWindow(Window *trackedWindow) { // Reset the old window virtual keybaord geom if necessary // Old and new windows could be the same if focus moves between subsurfaces if (m_trackedWindow == trackedWindow) { return; } if (m_trackedWindow) { m_trackedWindow->setVirtualKeyboardGeometry(QRect()); disconnect(m_trackedWindow, &Window::frameGeometryChanged, this, &InputMethod::updateInputPanelState); } m_trackedWindow = trackedWindow; m_shouldShowPanel = false; if (m_trackedWindow) { connect(m_trackedWindow, &Window::frameGeometryChanged, this, &InputMethod::updateInputPanelState, Qt::QueuedConnection); } updateInputPanelState(); } void InputMethod::handleFocusedSurfaceChanged() { resetPendingPreedit(); m_pendingText = QString(); auto seat = waylandServer()->seat(); SurfaceInterface *focusedSurface = seat->focusedTextInputSurface(); setTrackedWindow(waylandServer()->findWindow(focusedSurface)); const auto client = focusedSurface ? focusedSurface->client() : nullptr; bool ret = seat->textInputV2()->clientSupportsTextInput(client) || seat->textInputV3()->clientSupportsTextInput(client); if (ret != m_activeClientSupportsTextInput) { m_activeClientSupportsTextInput = ret; Q_EMIT activeClientSupportsTextInputChanged(); } } void InputMethod::surroundingTextChanged() { auto t2 = waylandServer()->seat()->textInputV2(); auto t3 = waylandServer()->seat()->textInputV3(); auto inputContext = waylandServer()->inputMethod()->context(); if (!inputContext) { return; } if (t2 && t2->isEnabled()) { inputContext->sendSurroundingText(t2->surroundingText(), t2->surroundingTextCursorPosition(), t2->surroundingTextSelectionAnchor()); return; } if (t3 && t3->isEnabled()) { inputContext->sendSurroundingText(t3->surroundingText(), t3->surroundingTextCursorPosition(), t3->surroundingTextSelectionAnchor()); return; } } void InputMethod::contentTypeChanged() { auto t1 = waylandServer()->seat()->textInputV1(); auto t2 = waylandServer()->seat()->textInputV2(); auto t3 = waylandServer()->seat()->textInputV3(); auto inputContext = waylandServer()->inputMethod()->context(); if (!inputContext) { return; } if (t1 && t1->isEnabled()) { inputContext->sendContentType(t1->contentHints(), t1->contentPurpose()); } if (t2 && t2->isEnabled()) { inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); } if (t3 && t3->isEnabled()) { inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); } } void InputMethod::textInputInterfaceV1Reset() { if (!m_enabled) { return; } auto t1 = waylandServer()->seat()->textInputV1(); auto inputContext = waylandServer()->inputMethod()->context(); if (!inputContext) { return; } if (!t1 || !t1->isEnabled()) { return; } inputContext->sendReset(); } void InputMethod::invokeAction(quint32 button, quint32 index) { if (!m_enabled) { return; } auto t1 = waylandServer()->seat()->textInputV1(); auto inputContext = waylandServer()->inputMethod()->context(); if (!inputContext) { return; } if (!t1 || !t1->isEnabled()) { return; } inputContext->sendInvokeAction(button, index); } void InputMethod::textInputInterfaceV1StateUpdated(quint32 serial) { if (!m_enabled) { return; } auto t1 = waylandServer()->seat()->textInputV1(); auto inputContext = waylandServer()->inputMethod()->context(); if (!inputContext) { return; } if (!t1 || !t1->isEnabled()) { return; } inputContext->sendCommitState(serial); } void InputMethod::textInputInterfaceV2StateUpdated(quint32 serial, TextInputV2Interface::UpdateReason reason) { if (!m_enabled) { return; } auto t2 = waylandServer()->seat()->textInputV2(); auto inputContext = waylandServer()->inputMethod()->context(); if (!inputContext) { return; } if (!t2 || !t2->isEnabled()) { return; } if (m_panel && shouldShowOnActive()) { m_panel->allow(); } switch (reason) { case TextInputV2Interface::UpdateReason::StateChange: break; case TextInputV2Interface::UpdateReason::StateEnter: case TextInputV2Interface::UpdateReason::StateFull: adoptInputMethodContext(); break; case TextInputV2Interface::UpdateReason::StateReset: inputContext->sendReset(); break; } } void InputMethod::textInputInterfaceV1EnabledChanged() { if (!m_enabled) { return; } refreshActive(); } void InputMethod::textInputInterfaceV2EnabledChanged() { if (!m_enabled) { return; } refreshActive(); } void InputMethod::textInputInterfaceV3EnabledChanged() { if (!m_enabled) { return; } auto t3 = waylandServer()->seat()->textInputV3(); refreshActive(); if (t3->isEnabled()) { show(); } else { // reset value of preedit when textinput is disabled resetPendingPreedit(); m_pendingText = QString(); } auto context = waylandServer()->inputMethod()->context(); if (context) { context->sendReset(); adoptInputMethodContext(); } } void InputMethod::stateCommitted(uint32_t serial) { if (!isEnabled()) { return; } TextInputV3Interface *textInputV3 = waylandServer()->seat()->textInputV3(); if (!textInputV3) { return; } if (auto inputContext = waylandServer()->inputMethod()->context()) { inputContext->sendCommitState(serial); } } void InputMethod::setEnabled(bool enabled) { if (m_enabled == enabled) { return; } m_enabled = enabled; Q_EMIT enabledChanged(m_enabled); // send OSD message QDBusMessage msg = QDBusMessage::createMethodCall( QStringLiteral("org.kde.plasmashell"), QStringLiteral("/org/kde/osdService"), QStringLiteral("org.kde.osdService"), QStringLiteral("virtualKeyboardEnabledChanged")); msg.setArguments({enabled}); QDBusConnection::sessionBus().asyncCall(msg); if (!m_enabled) { hide(); stopInputMethod(); } else { startInputMethod(); } // save value into config kwinApp()->config()->group(QStringLiteral("Wayland")).writeEntry("VirtualKeyboardEnabled", m_enabled); kwinApp()->config()->sync(); } static quint32 keysymToKeycode(quint32 sym) { switch (sym) { case XKB_KEY_BackSpace: return KEY_BACKSPACE; case XKB_KEY_Return: return KEY_ENTER; case XKB_KEY_Left: return KEY_LEFT; case XKB_KEY_Right: return KEY_RIGHT; case XKB_KEY_Up: return KEY_UP; case XKB_KEY_Down: return KEY_DOWN; default: return KEY_UNKNOWN; } } void InputMethod::keysymReceived(quint32 serial, quint32 time, quint32 sym, bool pressed, quint32 modifiers) { if (auto t1 = waylandServer()->seat()->textInputV1(); t1 && t1->isEnabled()) { if (pressed) { t1->keysymPressed(time, sym, modifiers); } else { t1->keysymReleased(time, sym, modifiers); } return; } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { if (pressed) { t2->keysymPressed(sym, modifiers); } else { t2->keysymReleased(sym, modifiers); } return; } KeyboardKeyState state; if (pressed) { state = KeyboardKeyState::Pressed; } else { state = KeyboardKeyState::Released; } waylandServer()->seat()->notifyKeyboardKey(keysymToKeycode(sym), state); } void InputMethod::commitString(qint32 serial, const QString &text) { if (auto t1 = waylandServer()->seat()->textInputV1(); t1 && t1->isEnabled()) { t1->commitString(text); t1->setPreEditCursor(0); t1->preEdit({}, {}); return; } if (auto t2 = waylandServer()->seat()->textInputV2(); t2 && t2->isEnabled()) { t2->commitString(text); t2->setPreEditCursor(0); t2->preEdit({}, {}); return; } else if (auto t3 = waylandServer()->seat()->textInputV3(); t3 && t3->isEnabled()) { t3->sendPreEditString(QString(), 0, 0); t3->commitString(text); t3->done(); return; } else { // The application has no way of communicating with the input method. // So instead, try to convert what we get from the input method into // keycodes and send those as fake input to the client. auto keys = textToKey(text); if (keys.empty()) { return; } // First, send all the extracted keys as pressed keys to the client. for (const auto &key : keys) { waylandServer()->seat()->notifyKeyboardKey(key, KeyboardKeyState::Pressed); } // Then, send key release for those keys in reverse. for (auto itr = keys.rbegin(); itr != keys.rend(); ++itr) { // Since we are faking key events, we do not have distinct press/release // events. So instead, just queue the button release so it gets sent // a few moments after the press. auto key = *itr; QMetaObject::invokeMethod( this, [key]() { waylandServer()->seat()->notifyKeyboardKey(key, KeyboardKeyState::Released); }, Qt::QueuedConnection); } } } void InputMethod::deleteSurroundingText(int32_t index, uint32_t length) { // zwp_input_method_v1 Delete surrounding text interface is designed for text-input-v1. // The parameter has different meaning in text-input-v{2,3}. // Current cursor is at index 0. // The actualy deleted text range is [index, index + length]. // In v{2,3}'s before/after style, text to be deleted with v{2,3} interface is [-before, after]. // And before/after are all unsigned, which make it impossible to do certain things. // Those request will be ignored. // Verify we can handle such request. if (index > 0 || index + static_cast(length) < 0) { return; } const quint32 before = -index; const quint32 after = index + length; auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->deleteSurroundingText(before, after); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->deleteSurroundingText(before, after); } auto t3 = waylandServer()->seat()->textInputV3(); if (t3 && t3->isEnabled()) { t3->deleteSurroundingText(before, after); t3->done(); } } void InputMethod::setCursorPosition(qint32 index, qint32 anchor) { auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->setCursorPosition(index, anchor); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->setCursorPosition(index, anchor); } } void InputMethod::setLanguage(uint32_t serial, const QString &language) { auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->setLanguage(language); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->setLanguage(language); } } void InputMethod::setTextDirection(uint32_t serial, Qt::LayoutDirection direction) { auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->setTextDirection(direction); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->setTextDirection(direction); } } void InputMethod::setPreeditCursor(qint32 index) { auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->setPreEditCursor(index); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->setPreEditCursor(index); } auto t3 = waylandServer()->seat()->textInputV3(); if (t3 && t3->isEnabled()) { preedit.cursor = index; } } void InputMethod::setPreeditStyling(quint32 index, quint32 length, quint32 style) { auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->preEditStyling(index, length, style); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->preEditStyling(index, length, style); } auto t3 = waylandServer()->seat()->textInputV3(); if (t3 && t3->isEnabled()) { // preedit style: highlight(4) or selection(6) if (style == 4 || style == 6) { preedit.highlightRanges.emplace_back(index, index + length); } } } void InputMethod::setPreeditString(uint32_t serial, const QString &text, const QString &commit) { auto t1 = waylandServer()->seat()->textInputV1(); if (t1 && t1->isEnabled()) { t1->preEdit(text, commit); } auto t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->preEdit(text, commit); } auto t3 = waylandServer()->seat()->textInputV3(); if (t3 && t3->isEnabled()) { m_pendingText = commit; if (!text.isEmpty()) { quint32 cursor = 0, cursorEnd = 0; if (preedit.cursor > 0) { cursor = cursorEnd = preedit.cursor; } // Check if we can convert highlight style to a range of selection. if (!preedit.highlightRanges.empty()) { std::sort(preedit.highlightRanges.begin(), preedit.highlightRanges.end()); // Check if starting point matches. if (preedit.highlightRanges.front().first == cursor) { quint32 end = preedit.highlightRanges.front().second; bool nonContinousHighlight = false; for (size_t i = 1; i < preedit.highlightRanges.size(); i++) { if (end >= preedit.highlightRanges[i].first) { end = std::max(end, preedit.highlightRanges[i].second); } else { nonContinousHighlight = true; break; } } if (!nonContinousHighlight) { cursorEnd = end; } } } t3->sendPreEditString(text, cursor, cursorEnd); } t3->done(); } resetPendingPreedit(); } void InputMethod::key(quint32 /*serial*/, quint32 /*time*/, quint32 keyCode, bool pressed) { waylandServer()->seat()->notifyKeyboardKey(keyCode, pressed ? KeyboardKeyState::Pressed : KeyboardKeyState::Released); } void InputMethod::modifiers(quint32 serial, quint32 mods_depressed, quint32 mods_latched, quint32 mods_locked, quint32 group) { auto xkb = input()->keyboard()->xkb(); xkb->updateModifiers(mods_depressed, mods_latched, mods_locked, group); } void InputMethod::forwardModifiers(ForwardModifiersForce force) { const bool sendModifiers = m_hasPendingModifiers || force == Force; m_hasPendingModifiers = false; if (!sendModifiers) { return; } auto xkb = input()->keyboard()->xkb(); if (m_keyboardGrab) { m_keyboardGrab->sendModifiers(waylandServer()->display()->nextSerial(), xkb->modifierState().depressed, xkb->modifierState().latched, xkb->modifierState().locked, xkb->currentLayout()); } } void InputMethod::adoptInputMethodContext() { auto inputContext = waylandServer()->inputMethod()->context(); TextInputV1Interface *t1 = waylandServer()->seat()->textInputV1(); TextInputV2Interface *t2 = waylandServer()->seat()->textInputV2(); TextInputV3Interface *t3 = waylandServer()->seat()->textInputV3(); if (t1 && t1->isEnabled()) { inputContext->sendSurroundingText(t1->surroundingText(), t1->surroundingTextCursorPosition(), t1->surroundingTextSelectionAnchor()); inputContext->sendPreferredLanguage(t1->preferredLanguage()); inputContext->sendContentType(t1->contentHints(), t2->contentPurpose()); connect(inputContext, &InputMethodContextV1Interface::language, this, &InputMethod::setLanguage); connect(inputContext, &InputMethodContextV1Interface::textDirection, this, &InputMethod::setTextDirection); } else if (t2 && t2->isEnabled()) { inputContext->sendSurroundingText(t2->surroundingText(), t2->surroundingTextCursorPosition(), t2->surroundingTextSelectionAnchor()); inputContext->sendPreferredLanguage(t2->preferredLanguage()); inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); connect(inputContext, &InputMethodContextV1Interface::language, this, &InputMethod::setLanguage); connect(inputContext, &InputMethodContextV1Interface::textDirection, this, &InputMethod::setTextDirection); } else if (t3 && t3->isEnabled()) { inputContext->sendSurroundingText(t3->surroundingText(), t3->surroundingTextCursorPosition(), t3->surroundingTextSelectionAnchor()); inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); } else { // When we have neither text-input-v2 nor text-input-v3 we can only send // fake key events, not more complex text. So ask the input method to // only send basic characters without any pre-editing. inputContext->sendContentType(TextInputContentHint::Latin, TextInputContentPurpose::Normal); } inputContext->sendCommitState(m_serial++); connect(inputContext, &InputMethodContextV1Interface::keysym, this, &InputMethod::keysymReceived, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::key, this, &InputMethod::key, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::modifiers, this, &InputMethod::modifiers, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::commitString, this, &InputMethod::commitString, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::deleteSurroundingText, this, &InputMethod::deleteSurroundingText, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::cursorPosition, this, &InputMethod::setCursorPosition, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::preeditStyling, this, &InputMethod::setPreeditStyling, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::preeditString, this, &InputMethod::setPreeditString, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::preeditCursor, this, &InputMethod::setPreeditCursor, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::keyboardGrabRequested, this, &InputMethod::installKeyboardGrab, Qt::UniqueConnection); connect(inputContext, &InputMethodContextV1Interface::modifiersMap, this, &InputMethod::updateModifiersMap, Qt::UniqueConnection); } void InputMethod::updateInputPanelState() { if (!waylandServer()) { return; } auto t = waylandServer()->seat()->textInputV2(); if (!t) { return; } if (m_panel && shouldShowOnActive()) { m_panel->allow(); } QRectF overlap = QRectF(0, 0, 0, 0); if (m_trackedWindow) { const bool bottomKeyboard = m_panel && m_panel->mode() != InputPanelV1Window::Mode::Overlay && m_panel->isShown(); m_trackedWindow->setVirtualKeyboardGeometry(bottomKeyboard ? m_panel->frameGeometry() : QRectF()); if (m_panel && m_panel->mode() != InputPanelV1Window::Mode::Overlay) { overlap = m_trackedWindow->frameGeometry() & m_panel->frameGeometry(); overlap.moveTo(m_trackedWindow->mapToLocal(overlap.topLeft())); } } t->setInputPanelState(m_panel && m_panel->isShown(), overlap.toRect()); } void InputMethod::setInputMethodCommand(const QString &command) { if (m_inputMethodCommand == command) { return; } m_inputMethodCommand = command; if (m_enabled) { startInputMethod(); } Q_EMIT availableChanged(); } void InputMethod::stopInputMethod() { if (!m_inputMethodProcess) { return; } disconnect(m_inputMethodProcess, nullptr, this, nullptr); m_inputMethodProcess->terminate(); if (!m_inputMethodProcess->waitForFinished()) { m_inputMethodProcess->kill(); m_inputMethodProcess->waitForFinished(); } m_inputMethodProcess->deleteLater(); m_inputMethodProcess = nullptr; waylandServer()->destroyInputMethodConnection(); } void InputMethod::startInputMethod() { stopInputMethod(); if (m_inputMethodCommand.isEmpty() || kwinApp()->isTerminating()) { return; } QStringList arguments = KShell::splitArgs(m_inputMethodCommand); if (arguments.isEmpty()) { qWarning("Failed to launch the input method server: %s is an invalid command", qPrintable(m_inputMethodCommand)); return; } const QString program = arguments.takeFirst(); int socket = waylandServer()->createInputMethodConnection(); if (socket < 0) { qWarning("Failed to create the input method connection"); return; } socket = dup(socket); QProcessEnvironment environment = kwinApp()->processStartupEnvironment(); environment.insert(QStringLiteral("WAYLAND_SOCKET"), QString::number(socket)); environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland")); // When we use Maliit as virtual keyboard, we want KWin to handle the animation // since that works a lot better. So we need to tell Maliit to not do client side // animation. environment.insert(QStringLiteral("MALIIT_ENABLE_ANIMATIONS"), QStringLiteral("0")); if (qEnvironmentVariableIntValue("KWIN_IM_WAYLAND_DEBUG") == 1) { environment.insert("WAYLAND_DEBUG", QByteArrayLiteral("1")); } m_inputMethodProcess = new QProcess(this); m_inputMethodProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); m_inputMethodProcess->setProcessEnvironment(environment); m_inputMethodProcess->setProgram(program); m_inputMethodProcess->setArguments(arguments); m_inputMethodProcess->start(); close(socket); connect(m_inputMethodProcess, QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus == QProcess::CrashExit) { m_inputMethodCrashes++; m_inputMethodCrashTimer.start(); qWarning() << "Input Method crashed" << m_inputMethodProcess->program() << m_inputMethodProcess->arguments() << exitCode << exitStatus; if (m_inputMethodCrashes < 5) { startInputMethod(); } else { qWarning() << "Input Method keeps crashing, please fix" << m_inputMethodProcess->program() << m_inputMethodProcess->arguments(); stopInputMethod(); } } }); } bool InputMethod::isActive() const { return waylandServer()->inputMethod()->context(); } InputMethodGrabV1 *InputMethod::keyboardGrab() { return isActive() ? m_keyboardGrab : nullptr; } void InputMethod::installKeyboardGrab(InputMethodGrabV1 *keyboardGrab) { auto xkb = input()->keyboard()->xkb(); m_keyboardGrab = keyboardGrab; keyboardGrab->sendKeymap(xkb->keymapContents()); forwardModifiers(Force); } void InputMethod::updateModifiersMap(const QByteArray &modifiers) { TextInputV2Interface *t2 = waylandServer()->seat()->textInputV2(); if (t2 && t2->isEnabled()) { t2->setModifiersMap(modifiers); } } bool InputMethod::isVisible() const { return m_panel && m_panel->isShown() && m_panel->readyForPainting(); } bool InputMethod::isAvailable() const { return !m_inputMethodCommand.isEmpty(); } Window *InputMethod::activeWindow() const { return m_trackedWindow; } void InputMethod::resetPendingPreedit() { preedit.cursor = 0; preedit.highlightRanges.clear(); } bool InputMethod::activeClientSupportsTextInput() const { return m_activeClientSupportsTextInput; } void InputMethod::forceActivate() { setActive(true); show(); } void InputMethod::textInputInterfaceV3EnableRequested() { refreshActive(); show(); } } #include "moc_inputmethod.cpp"