diff --git a/cli/kdeconnect-cli.cpp b/cli/kdeconnect-cli.cpp index 1acb72314..8281b1239 100644 --- a/cli/kdeconnect-cli.cpp +++ b/cli/kdeconnect-cli.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -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); diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt index 19a9cdc22..806ff36e7 100644 --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -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}) diff --git a/interfaces/dbusinterfaces.cpp b/interfaces/dbusinterfaces.cpp index aab3e0530..1f9903674 100644 --- a/interfaces/dbusinterfaces.cpp +++ b/interfaces/dbusinterfaces.cpp @@ -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" diff --git a/interfaces/dbusinterfaces.h b/interfaces/dbusinterfaces.h index 9848d0cfe..ade1543fa 100644 --- a/interfaces/dbusinterfaces.h +++ b/interfaces/dbusinterfaces.h @@ -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 diff --git a/plasmoid/declarativeplugin/kdeconnectdeclarativeplugin.cpp b/plasmoid/declarativeplugin/kdeconnectdeclarativeplugin.cpp index 282f2a97f..38c5960d5 100644 --- a/plasmoid/declarativeplugin/kdeconnectdeclarativeplugin.cpp +++ b/plasmoid/declarativeplugin/kdeconnectdeclarativeplugin.cpp @@ -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(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); qmlRegisterUncreatableType(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); qmlRegisterUncreatableType(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); + qmlRegisterUncreatableType(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); qmlRegisterUncreatableType(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); qmlRegisterSingletonType(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)); diff --git a/plasmoid/package/contents/ui/DeviceDelegate.qml b/plasmoid/package/contents/ui/DeviceDelegate.qml index e90e021a6..3618ef55c 100644 --- a/plasmoid/package/contents/ui/DeviceDelegate.qml +++ b/plasmoid/package/contents/ui/DeviceDelegate.qml @@ -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 { diff --git a/plasmoid/package/contents/ui/RemoteKeyboard.qml b/plasmoid/package/contents/ui/RemoteKeyboard.qml new file mode 100644 index 000000000..80c8b5ab5 --- /dev/null +++ b/plasmoid/package/contents/ui/RemoteKeyboard.qml @@ -0,0 +1,71 @@ +/** + * Copyright 2016 Holger Kaelberer + * + * 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 . + */ + +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 + } + } +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9d230ac32..f0d5bb5c9 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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) diff --git a/plugins/mousepad/kdeconnect_mousepad.json b/plugins/mousepad/kdeconnect_mousepad.json index 850e855af..962cf61b2 100644 --- a/plugins/mousepad/kdeconnect_mousepad.json +++ b/plugins/mousepad/kdeconnect_mousepad.json @@ -78,4 +78,4 @@ "X-KdeConnect-SupportedPackageType": [ "kdeconnect.mousepad.request" ] -} +} \ No newline at end of file diff --git a/plugins/remotekeyboard/CMakeLists.txt b/plugins/remotekeyboard/CMakeLists.txt new file mode 100644 index 000000000..95ad9833b --- /dev/null +++ b/plugins/remotekeyboard/CMakeLists.txt @@ -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 +) diff --git a/plugins/remotekeyboard/README b/plugins/remotekeyboard/README new file mode 100644 index 000000000..b3016acac --- /dev/null +++ b/plugins/remotekeyboard/README @@ -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. diff --git a/plugins/remotekeyboard/kdeconnect_remotekeyboard.json b/plugins/remotekeyboard/kdeconnect_remotekeyboard.json new file mode 100644 index 000000000..a0fbc75e9 --- /dev/null +++ b/plugins/remotekeyboard/kdeconnect_remotekeyboard.json @@ -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" + ] +} diff --git a/plugins/remotekeyboard/remotekeyboardplugin.cpp b/plugins/remotekeyboard/remotekeyboardplugin.cpp new file mode 100644 index 000000000..66565af4b --- /dev/null +++ b/plugins/remotekeyboard/remotekeyboardplugin.cpp @@ -0,0 +1,144 @@ +/** + * Copyright 2016 Holger Kaelberer + * + * 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 . + */ + +#include "remotekeyboardplugin.h" +#include +#include +#include +#include +#include + +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 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("key"), + np.get("specialKey", 0), + np.get("shift", false), + np.get("ctrl", false), + np.get("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" diff --git a/plugins/remotekeyboard/remotekeyboardplugin.h b/plugins/remotekeyboard/remotekeyboardplugin.h new file mode 100644 index 000000000..f0da7b1f3 --- /dev/null +++ b/plugins/remotekeyboard/remotekeyboardplugin.h @@ -0,0 +1,65 @@ +/** + * Copyright 2016 Holger Kaelberer + * + * 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 . + */ + +#ifndef REMOTEKEYBOARDPLUGIN_H +#define REMOTEKEYBOARDPLUGIN_H + +#include +#include +#include +#include + +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