kdeconnect-kde: Add remotekeyboard plugin

Allow to inject keypress events to remote peers (most notably Android devices)

Notes / open issues / possible improvements:

- For the json-payload I used the syntax of the key-events as sent by mousepad-plugin with the addition of a "sendAck"-flag. If "sendAck" is set to true the remote peer should echo a key-event if it could be handled, thus allowing the local client to find out whether the key was accepted. For performance reasons, it's allowed to send multi-char strings in the "key" property (performs much better if you send a whole string via "echo '...' |  kdeconnect-cli ..." e.g.)

- kdeconnect-cli: For now takes a string and transforms it into single key-events for visible characters only. In a first implementation I used a kbhit() helper that used termios.h to catch and relay keypresses interactively (including some special-events), which was not optimal. A better approch might be to use linux input-api directly. Would this be an option regarding cross-platform compatibility or can I assume to develop for Linux only? Being a command-line guy, I'd really like to have a fully featured kdeconnect-cli interface ;-)

- Factor out the Qt::Key-to-internal keymap to some core-helper because it corresponds to the mapping in the mousepad-plugin?

- The plasmoid is not perfect as it is: A single line containing a non-echoing TextField (i.e. it eats all the KeyPress events), and only ack-ed keypress-packets from the peer device are injected if they contain visible keys. Advantage: the user sees whether his key-presses are accepted by the peer device. Disadvantage: The echoed text does not correspond 1:1 to what is shown on the peer's display, user might be confused when typing without success. I played around with different variations each of which with its proper shortcomings:
1. An echoing Textfield for typing: Has the advantage that the user can directly see what he is typing, which makes interaction in the typing field easier, BUT messes up interaction if the Editor on the peer is changed silently and does not notify the user if his keypresses are not handled by the peer.
2. A non-echoing TextField for typing PLUS a readonly one for printing visible echoed keys. Disadvantage: same as for the previous one and uses more space on the plasmoid.
Comments? Ideas?

REVIEW: 129727
BUG: 370919
This commit is contained in:
Holger Kaelberer 2017-01-10 21:12:42 +01:00 committed by Aleix Pol
parent 3734d6ce4d
commit 040ad7357b
14 changed files with 448 additions and 1 deletions

View file

@ -24,6 +24,7 @@
#include <QDBusConnection>
#include <QCoreApplication>
#include <QTextStream>
#include <QFile>
#include <KAboutData>
#include <KLocalizedString>
@ -66,6 +67,7 @@ int main(int argc, char** argv)
parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device")));
parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids")));
parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id")));
parser.addOption(QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device")));
about.setupCommandLine(&parser);
parser.addHelpOption();
@ -199,6 +201,24 @@ int main(int argc, char** argv)
} else if(parser.isSet(QStringLiteral("ring"))) {
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/findmyphone", QStringLiteral("org.kde.kdeconnect.device.findmyphone"), QStringLiteral("ring"));
QDBusConnection::sessionBus().call(msg);
} else if(parser.isSet("send-keys")) {
QString seq = parser.value("send-keys");
QDBusMessage msg = QDBusMessage::createMethodCall("org.kde.kdeconnect", "/modules/kdeconnect/devices/"+device+"/remotekeyboard", "org.kde.kdeconnect.device.remotekeyboard", "sendKeyPress");
if (seq.trimmed() == QLatin1String("-")) {
// from file
QFile in;
if(in.open(stdin,QIODevice::ReadOnly | QIODevice::Unbuffered)) {
while (!in.atEnd()) {
QByteArray line = in.readLine(); // sanitize to ASCII-codes > 31?
msg.setArguments({QString(line), -1, false, false, false});
QDBusConnection::sessionBus().call(msg);
}
in.close();
}
} else {
msg.setArguments({seq, -1, false, false, false});
QDBusConnection::sessionBus().call(msg);
}
} else if(parser.isSet(QStringLiteral("list-notifications"))) {
NotificationsModel notifications;
notifications.setDeviceId(device);

View file

@ -43,6 +43,7 @@ geninterface(${CMAKE_SOURCE_DIR}/plugins/mprisremote/mprisremoteplugin.h mprisre
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotecontrol/remotecontrolplugin.h remotecontrolinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/lockdevice/lockdeviceplugin.h lockdeviceinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotecommands/remotecommandsplugin.h remotecommandsinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotekeyboard/remotekeyboardplugin.h remotekeyboardinterface)
add_library(kdeconnectinterfaces SHARED ${libkdeconnect_SRC})

View file

@ -156,4 +156,11 @@ RemoteCommandsDbusInterface::RemoteCommandsDbusInterface(const QString& deviceId
RemoteCommandsDbusInterface::~RemoteCommandsDbusInterface() = default;
RemoteKeyboardDbusInterface::RemoteKeyboardDbusInterface(const QString& deviceId, QObject* parent):
OrgKdeKdeconnectDeviceRemotekeyboardInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/remotekeyboard", QDBusConnection::sessionBus(), parent)
{
}
RemoteKeyboardDbusInterface::~RemoteKeyboardDbusInterface() = default;
#include "dbusinterfaces.moc"

View file

@ -34,6 +34,7 @@
#include "interfaces/remotecontrolinterface.h"
#include "interfaces/lockdeviceinterface.h"
#include "interfaces/remotecommandsinterface.h"
#include "interfaces/remotekeyboardinterface.h"
/**
* Using these "proxy" classes just in case we need to rename the
@ -180,4 +181,13 @@ public:
~RemoteCommandsDbusInterface() override;
};
class KDECONNECTINTERFACES_EXPORT RemoteKeyboardDbusInterface
: public OrgKdeKdeconnectDeviceRemotekeyboardInterface
{
Q_OBJECT
public:
explicit RemoteKeyboardDbusInterface(const QString& deviceId, QObject* parent = nullptr);
~RemoteKeyboardDbusInterface() override;
};
#endif

View file

@ -49,6 +49,11 @@ QObject* createFindMyPhoneInterface(const QVariant &deviceId)
return new FindMyPhoneDeviceDbusInterface(deviceId.toString());
}
QObject* createRemoteKeyboardInterface(const QVariant &deviceId)
{
return new RemoteKeyboardDbusInterface(deviceId.toString());
}
QObject* createSftpInterface(const QVariant &deviceId)
{
return new SftpDbusInterface(deviceId.toString());
@ -85,6 +90,7 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri)
qmlRegisterUncreatableType<MprisDbusInterface>(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<LockDeviceDbusInterface>(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<FindMyPhoneDeviceDbusInterface>(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<RemoteKeyboardDbusInterface>(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<DeviceDbusInterface>(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterSingletonType<DaemonDbusInterface>(uri, 1, 0, "DaemonDbusInterface",
[](QQmlEngine*, QJSEngine*) -> QObject* {
@ -109,6 +115,9 @@ void KdeConnectDeclarativePlugin::initializeEngine(QQmlEngine* engine, const cha
engine->rootContext()->setContextProperty(QStringLiteral("SftpDbusInterfaceFactory")
, new ObjectFactory(engine, createSftpInterface));
engine->rootContext()->setContextProperty(QStringLiteral("RemoteKeyboardDbusInterfaceFactory")
, new ObjectFactory(engine, createRemoteKeyboardInterface));
engine->rootContext()->setContextProperty(QStringLiteral("MprisDbusInterfaceFactory")
, new ObjectFactory(engine, createMprisInterface));

View file

@ -23,12 +23,31 @@ import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kdeconnect 1.0
import QtQuick.Controls.Styles 1.4
PlasmaComponents.ListItem
{
id: root
readonly property QtObject device: DeviceDbusInterfaceFactory.create(model.deviceId)
RemoteKeyboard {
id: remoteKeyboard
device: root.device
onKeyPressReceived: {
if (specialKey == 12) // Return -> clear
remoteKeyboardInput.text = "";
else {
var sanitized = "";
for (var i = 0; i < key.length; i++) {
if (key.charCodeAt(i) > 31)
sanitized += key.charAt(i);
}
if (sanitized.length > 0 && !ctrl && !alt)
remoteKeyboardInput.text += sanitized;
}
}
}
Column {
width: parent.width
@ -86,6 +105,47 @@ PlasmaComponents.ListItem
width: parent.width
}
//RemoteKeyboard
PlasmaComponents.ListItem {
sectionDelegate: true
visible: remoteKeyboard.available
width: parent.width
Row {
width: parent.width
spacing: 5
PlasmaComponents.Label {
id: remoteKeyboardLabel
//font.bold: true
text: i18n("Remote Keyboard")
}
PlasmaComponents.TextField {
id: remoteKeyboardInput
textColor: "black"
height: parent.height
width: parent.width - 5 - remoteKeyboardLabel.width
verticalAlignment: TextInput.AlignVCenter
style: TextFieldStyle {
textColor: "black"
background: Rectangle {
radius: 2
border.color: "gray"
border.width: 1
color: "white"
}
}
Keys.onPressed: {
if (remoteKeyboard.available)
remoteKeyboard.sendEvent(event);
event.accepted = true;
}
}
}
}
//Battery
PlasmaComponents.ListItem {

View file

@ -0,0 +1,71 @@
/**
* Copyright 2016 Holger Kaelberer <holger.k@elberer.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kdeconnect 1.0
QtObject {
id: root
property alias device: checker.device
readonly property alias available: checker.available
readonly property PluginChecker pluginChecker: PluginChecker {
id: checker
pluginName: "remotekeyboard"
}
property variant remoteKeyboard: null
signal keyPressReceived(string key, int specialKey, bool shift, bool ctrl, bool alt)
function sendEvent(event) {
if (remoteKeyboard) {
var transEvent = JSON.parse(JSON.stringify(event)); // transform to anonymous object
if (transEvent.modifiers & Qt.ControlModifier) {
// special handling for ctrl+c/v/x/a, for which only 'key' gets
// set, but no visbile 'text', which is expected by the remoteKeyboard
// wire-format:
if (transEvent.key === Qt.Key_C)
transEvent.text = 'c';
if (transEvent.key === Qt.Key_V)
transEvent.text = 'v';
if (transEvent.key === Qt.Key_A)
transEvent.text = 'a';
if (transEvent.key === Qt.Key_X)
transEvent.text = 'x';
}
remoteKeyboard.sendQKeyEvent(transEvent);
}
}
onAvailableChanged: {
if (available) {
remoteKeyboard = RemoteKeyboardDbusInterfaceFactory.create(device.id());
remoteKeyboard.keyPressReceived.connect(keyPressReceived);
} else {
remoteKeyboard = null
}
}
}

View file

@ -10,6 +10,7 @@ add_subdirectory(notifications)
add_subdirectory(battery)
add_subdirectory(remotecommands)
add_subdirectory(findmyphone)
add_subdirectory(remotekeyboard)
if(NOT WIN32)
add_subdirectory(runcommand)
add_subdirectory(sendnotifications)

View file

@ -0,0 +1,8 @@
kdeconnect_add_plugin(kdeconnect_remotekeyboard JSON kdeconnect_remotekeyboard.json
SOURCES remotekeyboardplugin.cpp)
target_link_libraries(kdeconnect_remotekeyboard
kdeconnectcore
KF5::I18n
Qt5::DBus
)

View file

@ -0,0 +1,23 @@
Sends key-events to remote devices. The payload structure corresponds basically
to that of remote key-presses in the mousepad-plugin (with the exception of the
"sendAck"-flag) , e.g.:
{
"key": "a",
"specialKey": 12,
"shift": false,
"ctrl": false,
"alt": false,
"sendAck": true
}
If "specialKey" is a valid keycode according to the internal map (1 <= x <= 32),
the event is interpreted as a special event and the contents of "key" are not
considered.
"key" may contain multi-char strings for performance reasons. In that case,
the peer is expected to print the whole string.
If "sendAck" is set to true, the device expects the remote peer to echo the
event in case it could be handled. This can be used to determine whether the
remote device is ready to accept remote keypresses.

View file

@ -0,0 +1,28 @@
{
"Encoding": "UTF-8",
"KPlugin": {
"Authors": [
{
"Email": "holger.k@elberer.de",
"Name": "Holger Kaelberer"
}
],
"Description": "Use your keyboard to send key-events to your paired device",
"Description[x-test]": "xxUse your keyboard to send key-events to your paired devicexx",
"EnabledByDefault": true,
"Icon": "edit-select",
"Id": "kdeconnect_remotekeyboard",
"License": "GPL",
"Name": "Remote keyboard from the desktop",
"ServiceTypes": [
"KdeConnect/Plugin"
],
"Version": "0.1"
},
"X-KdeConnect-OutgoingPackageType": [
"kdeconnect.remotekeyboard.request"
],
"X-KdeConnect-SupportedPackageType": [
"kdeconnect.remotekeyboard"
]
}

View file

@ -0,0 +1,144 @@
/**
* Copyright 2016 Holger Kaelberer <holger.k@elberer.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "remotekeyboardplugin.h"
#include <KPluginFactory>
#include <KLocalizedString>
#include <QDebug>
#include <QString>
#include <QVariantMap>
K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_remotekeyboard.json", registerPlugin< RemoteKeyboardPlugin >(); )
Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_REMOTEKEYBOARD, "kdeconnect.plugin.remotekeyboard");
// Mapping of Qt::Key to internal codes, corresponds to the mapping in mousepadplugin
QMap<int, int> specialKeysMap = {
//0, // Invalid
{Qt::Key_Backspace, 1},
{Qt::Key_Tab, 2},
//XK_Linefeed, // 3
{Qt::Key_Left, 4},
{Qt::Key_Up, 5},
{Qt::Key_Right, 6},
{Qt::Key_Down, 7},
{Qt::Key_PageUp, 8},
{Qt::Key_PageDown, 9},
{Qt::Key_Home, 10},
{Qt::Key_End, 11},
{Qt::Key_Return, 12},
{Qt::Key_Enter, 12},
{Qt::Key_Delete, 13},
{Qt::Key_Escape, 14},
{Qt::Key_SysReq, 15},
{Qt::Key_ScrollLock, 16},
//0, // 17
//0, // 18
//0, // 19
//0, // 20
{Qt::Key_F1, 21},
{Qt::Key_F2, 22},
{Qt::Key_F3, 23},
{Qt::Key_F4, 24},
{Qt::Key_F5, 25},
{Qt::Key_F6, 26},
{Qt::Key_F7, 27},
{Qt::Key_F8, 28},
{Qt::Key_F9, 29},
{Qt::Key_F10, 30},
{Qt::Key_F11, 31},
{Qt::Key_F12, 32},
};
RemoteKeyboardPlugin::RemoteKeyboardPlugin(QObject* parent, const QVariantList& args)
: KdeConnectPlugin(parent, args)
{
}
RemoteKeyboardPlugin::~RemoteKeyboardPlugin()
{
}
bool RemoteKeyboardPlugin::receivePackage(const NetworkPackage& np)
{
if (np.type() == PACKAGE_TYPE_REMOTEKEYBOARD) {
if (!np.has("isAck") || !np.has("key")) {
qCWarning(KDECONNECT_PLUGIN_REMOTEKEYBOARD) << "Invalid packet of type"
<< PACKAGE_TYPE_REMOTEKEYBOARD;
return false;
}
// qCWarning(KDECONNECT_PLUGIN_REMOTEKEYBOARD) << "Received keypress" << np;
Q_EMIT keyPressReceived(np.get<QString>("key"),
np.get<int>("specialKey", 0),
np.get<int>("shift", false),
np.get<int>("ctrl", false),
np.get<int>("alt", false));
return true;
}
return true;
}
void RemoteKeyboardPlugin::sendKeyPress(const QString& key, int specialKey,
bool shift, bool ctrl,
bool alt, bool sendAck) const
{
NetworkPackage np(PACKAGE_TYPE_REMOTEKEYBOARD_REQUEST, {
{"key", key},
{"specialKey", specialKey},
{"shift", shift},
{"ctrl", ctrl},
{"alt", alt},
{"sendAck", sendAck}
});
sendPackage(np);
}
void RemoteKeyboardPlugin::sendQKeyEvent(const QVariantMap& keyEvent, bool sendAck) const
{
if (!keyEvent.contains("key"))
return;
int k = translateQtKey(keyEvent.value("key").toInt());
int modifiers = keyEvent.value("modifiers").toInt();
sendKeyPress(keyEvent.value("text").toString(), k,
modifiers & Qt::ShiftModifier,
modifiers & Qt::ControlModifier,
modifiers & Qt::AltModifier,
sendAck);
}
int RemoteKeyboardPlugin::translateQtKey(int qtKey) const
{
return specialKeysMap.value(qtKey, 0);
}
void RemoteKeyboardPlugin::connected()
{
QDBusConnection::sessionBus().registerObject(dbusPath(), this,
QDBusConnection::ExportScriptableInvokables
| QDBusConnection::ExportScriptableSignals);
}
QString RemoteKeyboardPlugin::dbusPath() const
{
return "/modules/kdeconnect/devices/" + device()->id() + "/remotekeyboard";
}
#include "remotekeyboardplugin.moc"

View file

@ -0,0 +1,65 @@
/**
* Copyright 2016 Holger Kaelberer <holger.k@elberer.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef REMOTEKEYBOARDPLUGIN_H
#define REMOTEKEYBOARDPLUGIN_H
#include <core/kdeconnectplugin.h>
#include <QDBusInterface>
#include <QLoggingCategory>
#include <QVariantMap>
struct FakeKey;
Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_REMOTEKEYBOARD);
#define PACKAGE_TYPE_REMOTEKEYBOARD_REQUEST QLatin1String("kdeconnect.remotekeyboard.request")
#define PACKAGE_TYPE_REMOTEKEYBOARD QLatin1String("kdeconnect.remotekeyboard")
class RemoteKeyboardPlugin
: public KdeConnectPlugin
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.remotekeyboard")
public:
explicit RemoteKeyboardPlugin(QObject *parent, const QVariantList &args);
~RemoteKeyboardPlugin() override;
bool receivePackage(const NetworkPackage& np) override;
void connected() override;
Q_SCRIPTABLE void sendKeyPress(const QString& key, int specialKey = 0,
bool shift = false, bool ctrl = false,
bool alt = false, bool sendAck = true) const;
Q_SCRIPTABLE void sendQKeyEvent(const QVariantMap& keyEvent,
bool sendAck = true) const;
Q_SCRIPTABLE int translateQtKey(int qtKey) const;
Q_SIGNALS:
Q_SCRIPTABLE void keyPressReceived(const QString& key, int specialKey = 0,
bool shift = false, bool ctrl = false,
bool alt = false) const;
private:
QString dbusPath() const;
};
#endif