222b558b04
VirtualKeyboard class does not implement the relevant VirtualKeyboard protocol but rather implements the InputMethod protcol and can in theory be used by other input method like e.g. ibus. Make class name consistent with what it does to avoid confusion in future. For now only rename of main class is done and dbus service is kept as-is to provide retro-compatibility, when input method protocol is implemented fully, we can think of what to do wrt the dbus interface later when we fully implement zwp_input_method_unstable_v1 protocol.
467 lines
16 KiB
C++
467 lines
16 KiB
C++
/*
|
|
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_v2_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()) {
|
|
waylandServer()->display()->createTextInputManagerV2();
|
|
waylandServer()->display()->createTextInputManagerV3();
|
|
|
|
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::requestReset, this, &InputMethod::requestReset);
|
|
connect(textInputV2, &TextInputV2Interface::enabledChanged, this, &InputMethod::textInputInterfaceV2EnabledChanged);
|
|
connect(textInputV2, &TextInputV2Interface::stateCommitted, this, &InputMethod::stateCommitted);
|
|
|
|
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()
|
|
{
|
|
auto t = waylandServer()->seat()->textInputV2();
|
|
if (t) {
|
|
//FIXME: this shouldn't be necessary and causes double emits?
|
|
Q_EMIT t->enabledChanged();
|
|
}
|
|
}
|
|
|
|
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::requestReset()
|
|
{
|
|
auto t2 = waylandServer()->seat()->textInputV2();
|
|
auto inputContext = waylandServer()->inputMethod()->context();
|
|
if (!inputContext) {
|
|
return;
|
|
}
|
|
inputContext->sendReset();
|
|
if (t2 && t2->isEnabled()) {
|
|
inputContext->sendSurroundingText(t2->surroundingText(), t2->surroundingTextCursorPosition(), t2->surroundingTextSelectionAnchor());
|
|
inputContext->sendPreferredLanguage(t2->preferredLanguage());
|
|
inputContext->sendContentType(t2->contentHints(), t2->contentPurpose());
|
|
}
|
|
}
|
|
|
|
void InputMethod::textInputInterfaceV2EnabledChanged()
|
|
{
|
|
auto t = waylandServer()->seat()->textInputV2();
|
|
if (t->isEnabled()) {
|
|
//FIXME This sendDeactivate shouldn't be necessary?
|
|
waylandServer()->inputMethod()->sendDeactivate();
|
|
waylandServer()->inputMethod()->sendActivate();
|
|
adoptInputMethodContext();
|
|
} else {
|
|
waylandServer()->inputMethod()->sendDeactivate();
|
|
hide();
|
|
}
|
|
}
|
|
|
|
void InputMethod::textInputInterfaceV3EnabledChanged()
|
|
{
|
|
auto t3 = waylandServer()->seat()->textInputV3();
|
|
if (t3->isEnabled()) {
|
|
waylandServer()->inputMethod()->sendActivate();
|
|
adoptInputMethodContext();
|
|
} else {
|
|
waylandServer()->inputMethod()->sendDeactivate();
|
|
// reset value of preedit when textinput is disabled
|
|
preedit.text = QString();
|
|
preedit.begin = 0;
|
|
preedit.end = 0;
|
|
}
|
|
}
|
|
|
|
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()->keyPressed(keysymToKeycode(sym));
|
|
} else {
|
|
waylandServer()->seat()->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));
|
|
}
|
|
|
|
}
|