You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
477 lines
16 KiB
477 lines
16 KiB
/* |
|
KWin - the KDE window manager |
|
This file is part of the KDE project. |
|
|
|
SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
#include "inputmethod.h" |
|
#include "virtualkeyboard_dbus.h" |
|
#include "input.h" |
|
#include "keyboard_input.h" |
|
#include "utils.h" |
|
#include "screens.h" |
|
#include "wayland_server.h" |
|
#include "workspace.h" |
|
#include "screenlockerwatcher.h" |
|
|
|
#include <KWaylandServer/display.h> |
|
#include <KWaylandServer/seat_interface.h> |
|
#include <KWaylandServer/textinput_v3_interface.h> |
|
#include <KWaylandServer/surface_interface.h> |
|
#include <KWaylandServer/inputmethod_v1_interface.h> |
|
|
|
#include <KStatusNotifierItem> |
|
#include <KLocalizedString> |
|
|
|
#include <QDBusConnection> |
|
#include <QDBusPendingCall> |
|
#include <QDBusMessage> |
|
|
|
#include <linux/input-event-codes.h> |
|
#include <xkbcommon/xkbcommon-keysyms.h> |
|
|
|
using namespace KWaylandServer; |
|
|
|
namespace KWin |
|
{ |
|
|
|
KWIN_SINGLETON_FACTORY(InputMethod) |
|
|
|
InputMethod::InputMethod(QObject *parent) |
|
: QObject(parent) |
|
{ |
|
// 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 |
|
connect(kwinApp(), &Application::workspaceCreated, this, &InputMethod::init); |
|
} |
|
|
|
InputMethod::~InputMethod() = default; |
|
|
|
void InputMethod::init() |
|
{ |
|
connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::aboutToLock, this, &InputMethod::hide); |
|
|
|
if (waylandServer()) { |
|
m_enabled = !input()->hasAlphaNumericKeyboard(); |
|
qCDebug(KWIN_VIRTUALKEYBOARD) << "enabled by default: " << m_enabled; |
|
connect(input(), &InputRedirection::hasAlphaNumericKeyboardChanged, this, |
|
[this] (bool set) { |
|
qCDebug(KWIN_VIRTUALKEYBOARD) << "AlphaNumeric Keyboard changed:" << set << "toggling virtual keyboard."; |
|
setEnabled(!set); |
|
} |
|
); |
|
} |
|
|
|
qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the SNI"; |
|
m_sni = new KStatusNotifierItem(QStringLiteral("kwin-virtual-keyboard"), this); |
|
m_sni->setStandardActionsEnabled(false); |
|
m_sni->setCategory(KStatusNotifierItem::Hardware); |
|
m_sni->setStatus(KStatusNotifierItem::Passive); |
|
m_sni->setTitle(i18n("Virtual Keyboard")); |
|
updateSni(); |
|
connect(m_sni, &KStatusNotifierItem::activateRequested, this, |
|
[this] { |
|
setEnabled(!m_enabled); |
|
} |
|
); |
|
connect(this, &InputMethod::enabledChanged, this, &InputMethod::updateSni); |
|
|
|
auto dbus = new VirtualKeyboardDBus(this); |
|
qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the DBus interface"; |
|
dbus->setEnabled(m_enabled); |
|
connect(dbus, &VirtualKeyboardDBus::activateRequested, this, &InputMethod::setEnabled); |
|
connect(this, &InputMethod::enabledChanged, dbus, &VirtualKeyboardDBus::setEnabled); |
|
connect(input(), &InputRedirection::keyStateChanged, this, &InputMethod::hide); |
|
|
|
if (waylandServer()) { |
|
new TextInputManagerV2Interface(waylandServer()->display()); |
|
new TextInputManagerV3Interface(waylandServer()->display()); |
|
|
|
connect(workspace(), &Workspace::clientAdded, this, &InputMethod::clientAdded); |
|
connect(waylandServer()->seat(), &SeatInterface::focusedTextInputSurfaceChanged, this, &InputMethod::handleFocusedSurfaceChanged); |
|
|
|
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::enabledChanged, this, &InputMethod::textInputInterfaceV2EnabledChanged); |
|
connect(textInputV2, &TextInputV2Interface::stateUpdated, this, &InputMethod::textInputInterfaceV2StateUpdated); |
|
|
|
TextInputV3Interface *textInputV3 = waylandServer()->seat()->textInputV3(); |
|
connect(textInputV3, &TextInputV3Interface::enabledChanged, this, &InputMethod::textInputInterfaceV3EnabledChanged); |
|
connect(textInputV3, &TextInputV3Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); |
|
connect(textInputV3, &TextInputV3Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); |
|
connect(textInputV3, &TextInputV3Interface::stateCommitted, this, &InputMethod::stateCommitted); |
|
} |
|
} |
|
|
|
void InputMethod::show() |
|
{ |
|
waylandServer()->inputMethod()->sendActivate(); |
|
} |
|
|
|
void InputMethod::hide() |
|
{ |
|
waylandServer()->inputMethod()->sendDeactivate(); |
|
updateInputPanelState(); |
|
} |
|
|
|
void InputMethod::clientAdded(AbstractClient* client) |
|
{ |
|
if (!client->isInputMethod()) { |
|
return; |
|
} |
|
m_inputClient = client; |
|
auto refreshFrame = [this] { |
|
if (!m_trackedClient) { |
|
return; |
|
} |
|
|
|
if (m_inputClient && !m_inputClient->inputGeometry().isEmpty()) { |
|
m_trackedClient->setVirtualKeyboardGeometry(m_inputClient->inputGeometry()); |
|
} |
|
}; |
|
connect(client->surface(), &SurfaceInterface::inputChanged, this, refreshFrame); |
|
connect(client, &QObject::destroyed, this, [this] { |
|
if (m_trackedClient) { |
|
m_trackedClient->setVirtualKeyboardGeometry({}); |
|
} |
|
}); |
|
connect(m_inputClient, &AbstractClient::frameGeometryChanged, this, refreshFrame); |
|
} |
|
|
|
void InputMethod::handleFocusedSurfaceChanged() |
|
{ |
|
SurfaceInterface *focusedSurface = waylandServer()->seat()->focusedTextInputSurface(); |
|
if (focusedSurface) { |
|
AbstractClient *focusedClient = waylandServer()->findClient(focusedSurface); |
|
// Reset the old client virtual keybaord geom if necessary |
|
// Old and new clients could be the same if focus moves between subsurfaces |
|
if (m_trackedClient != focusedClient) { |
|
if (m_trackedClient) { |
|
m_trackedClient->setVirtualKeyboardGeometry(QRect()); |
|
} |
|
m_trackedClient = focusedClient; |
|
} |
|
} else { |
|
waylandServer()->inputMethod()->sendDeactivate(); |
|
} |
|
updateInputPanelState(); |
|
} |
|
|
|
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 t2 = waylandServer()->seat()->textInputV2(); |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
auto inputContext = waylandServer()->inputMethod()->context(); |
|
if (!inputContext) { |
|
return; |
|
} |
|
if (t2 && t2->isEnabled()) { |
|
inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); |
|
} |
|
if (t3 && t3->isEnabled()) { |
|
inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); |
|
} |
|
} |
|
|
|
void InputMethod::textInputInterfaceV2StateUpdated(quint32 serial, KWaylandServer::TextInputV2Interface::UpdateReason reason) |
|
{ |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
auto inputContext = waylandServer()->inputMethod()->context(); |
|
if (!inputContext) { |
|
return; |
|
} |
|
if (!t2 || !t2->isEnabled()) { |
|
return; |
|
} |
|
switch (reason) { |
|
case KWaylandServer::TextInputV2Interface::UpdateReason::StateChange: |
|
inputContext->sendCommitState(serial); |
|
break; |
|
case KWaylandServer::TextInputV2Interface::UpdateReason::StateEnter: |
|
waylandServer()->inputMethod()->sendActivate(); |
|
inputContext->sendCommitState(serial); |
|
break; |
|
case KWaylandServer::TextInputV2Interface::UpdateReason::StateFull: |
|
adoptInputMethodContext(); |
|
inputContext->sendCommitState(serial); |
|
break; |
|
case KWaylandServer::TextInputV2Interface::UpdateReason::StateReset: |
|
inputContext->sendReset(); |
|
inputContext->sendCommitState(serial); |
|
break; |
|
} |
|
} |
|
|
|
void InputMethod::textInputInterfaceV2EnabledChanged() |
|
{ |
|
auto t = waylandServer()->seat()->textInputV2(); |
|
if (t->isEnabled()) { |
|
waylandServer()->inputMethod()->sendActivate(); |
|
adoptInputMethodContext(); |
|
} else { |
|
waylandServer()->inputMethod()->sendDeactivate(); |
|
hide(); |
|
} |
|
} |
|
|
|
void InputMethod::textInputInterfaceV3EnabledChanged() |
|
{ |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
if (t3->isEnabled()) { |
|
waylandServer()->inputMethod()->sendActivate(); |
|
} else { |
|
waylandServer()->inputMethod()->sendDeactivate(); |
|
// reset value of preedit when textinput is disabled |
|
preedit.text = QString(); |
|
preedit.begin = 0; |
|
preedit.end = 0; |
|
} |
|
auto context = waylandServer()->inputMethod()->context(); |
|
if (context) { |
|
context->sendReset(); |
|
adoptInputMethodContext(); |
|
} |
|
} |
|
|
|
void InputMethod::stateCommitted(uint32_t serial) |
|
{ |
|
auto inputContext = waylandServer()->inputMethod()->context(); |
|
if (!inputContext) { |
|
return; |
|
} |
|
inputContext->sendCommitState(serial); |
|
} |
|
|
|
void InputMethod::setEnabled(bool enabled) |
|
{ |
|
if (m_enabled == enabled) { |
|
return; |
|
} |
|
m_enabled = enabled; |
|
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); |
|
} |
|
|
|
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; |
|
} |
|
} |
|
|
|
static void keysymReceived(quint32 serial, quint32 time, quint32 sym, bool pressed, Qt::KeyboardModifiers modifiers) |
|
{ |
|
Q_UNUSED(serial) |
|
Q_UNUSED(time) |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
if (pressed) { |
|
t2->keysymPressed(sym, modifiers); |
|
} else { |
|
t2->keysymReleased(sym, modifiers); |
|
} |
|
return; |
|
} |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
if (t3 && t3->isEnabled()) { |
|
if (pressed) { |
|
waylandServer()->seat()->keyboard()->keyPressed(keysymToKeycode(sym)); |
|
} else { |
|
waylandServer()->seat()->keyboard()->keyReleased(keysymToKeycode(sym)); |
|
} |
|
return; |
|
} |
|
} |
|
|
|
static void commitString(qint32 serial, const QString &text) |
|
{ |
|
Q_UNUSED(serial) |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->commitString(text.toUtf8()); |
|
t2->preEdit({}, {}); |
|
return; |
|
} |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
if (t3 && t3->isEnabled()) { |
|
t3->commitString(text.toUtf8()); |
|
t3->done(); |
|
return; |
|
} |
|
} |
|
|
|
static void deleteSurroundingText(int32_t index, uint32_t length) |
|
{ |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->deleteSurroundingText(index, length); |
|
} |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
if (t3 && t3->isEnabled()) { |
|
t3->deleteSurroundingText(index, length); |
|
} |
|
} |
|
|
|
static void setCursorPosition(qint32 index, qint32 anchor) |
|
{ |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->setCursorPosition(index, anchor); |
|
} |
|
} |
|
|
|
static void setLanguage(uint32_t serial, const QString &language) |
|
{ |
|
Q_UNUSED(serial) |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->setLanguage(language.toUtf8()); |
|
} |
|
} |
|
|
|
static void setTextDirection(uint32_t serial, Qt::LayoutDirection direction) |
|
{ |
|
Q_UNUSED(serial) |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->setTextDirection(direction); |
|
} |
|
} |
|
|
|
void InputMethod::setPreeditCursor(qint32 index) |
|
{ |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->setPreEditCursor(index); |
|
} |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
if (t3 && t3->isEnabled()) { |
|
preedit.begin = index; |
|
preedit.end = index; |
|
t3->sendPreEditString(preedit.text, preedit.begin, preedit.end); |
|
} |
|
} |
|
|
|
|
|
void InputMethod::setPreeditString(uint32_t serial, const QString &text, const QString &commit) |
|
{ |
|
Q_UNUSED(serial) |
|
auto t2 = waylandServer()->seat()->textInputV2(); |
|
if (t2 && t2->isEnabled()) { |
|
t2->preEdit(text.toUtf8(), commit.toUtf8()); |
|
} |
|
auto t3 = waylandServer()->seat()->textInputV3(); |
|
if (t3 && t3->isEnabled()) { |
|
preedit.text = text; |
|
t3->sendPreEditString(preedit.text, preedit.begin, preedit.end); |
|
} |
|
} |
|
|
|
void InputMethod::adoptInputMethodContext() |
|
{ |
|
auto inputContext = waylandServer()->inputMethod()->context(); |
|
|
|
TextInputV2Interface *t2 = waylandServer()->seat()->textInputV2(); |
|
TextInputV3Interface *t3 = waylandServer()->seat()->textInputV3(); |
|
|
|
if (t2 && t2->isEnabled()) { |
|
inputContext->sendSurroundingText(t2->surroundingText(), t2->surroundingTextCursorPosition(), t2->surroundingTextSelectionAnchor()); |
|
inputContext->sendPreferredLanguage(t2->preferredLanguage()); |
|
inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::language, waylandServer(), &setLanguage); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::textDirection, waylandServer(), &setTextDirection); |
|
} |
|
|
|
if (t3 && t3->isEnabled()) { |
|
inputContext->sendSurroundingText(t3->surroundingText(), t3->surroundingTextCursorPosition(), t3->surroundingTextSelectionAnchor()); |
|
inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); |
|
} |
|
|
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::keysym, waylandServer(), &keysymReceived); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::commitString, waylandServer(), &commitString); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::deleteSurroundingText, waylandServer(), &deleteSurroundingText); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::cursorPosition, waylandServer(), &setCursorPosition); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::preeditString, this, &InputMethod::setPreeditString); |
|
connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::preeditCursor, this, &InputMethod::setPreeditCursor); |
|
} |
|
|
|
void InputMethod::updateSni() |
|
{ |
|
if (!m_sni) { |
|
return; |
|
} |
|
if (m_enabled) { |
|
m_sni->setIconByName(QStringLiteral("input-keyboard-virtual-on")); |
|
m_sni->setTitle(i18n("Virtual Keyboard: enabled")); |
|
} else { |
|
m_sni->setIconByName(QStringLiteral("input-keyboard-virtual-off")); |
|
m_sni->setTitle(i18n("Virtual Keyboard: disabled")); |
|
} |
|
m_sni->setToolTipTitle(i18n("Whether to show the virtual keyboard on demand.")); |
|
} |
|
|
|
void InputMethod::updateInputPanelState() |
|
{ |
|
if (!waylandServer()) { |
|
return; |
|
} |
|
|
|
auto t = waylandServer()->seat()->textInputV2(); |
|
|
|
if (!t) { |
|
return; |
|
} |
|
|
|
if (m_trackedClient) { |
|
m_trackedClient->setVirtualKeyboardGeometry(m_inputClient ? m_inputClient->inputGeometry() : QRect()); |
|
} |
|
t->setInputPanelState(m_inputClient && m_inputClient->isShown(false), QRect(0, 0, 0, 0)); |
|
} |
|
|
|
}
|
|
|