Add shareinptudevices plugin
This plugin provides synergy-like behavior of sharing input devices between machines by moving the cursor seamlessly between them. To this end this uses the InputCapture Portal to be notified when the cursor moves 'out of the screen'. For forwarding input the existing mousepad infrastructure is used. On the other side a tiny hidden plugin listens to mouse events to track when input should pass back to source machine.
This commit is contained in:
parent
8c4c7d7baa
commit
e58c1a1415
20 changed files with 1870 additions and 0 deletions
|
@ -71,6 +71,9 @@ if (UNIX AND NOT APPLE)
|
||||||
geninterface(systeminterfaces/org.mpris.MediaPlayer2.Player.xml generated/systeminterfaces/mprisplayer)
|
geninterface(systeminterfaces/org.mpris.MediaPlayer2.Player.xml generated/systeminterfaces/mprisplayer)
|
||||||
geninterface(systeminterfaces/org.mpris.MediaPlayer2.xml generated/systeminterfaces/mprisroot)
|
geninterface(systeminterfaces/org.mpris.MediaPlayer2.xml generated/systeminterfaces/mprisroot)
|
||||||
geninterface(systeminterfaces/org.freedesktop.portal.RemoteDesktop.xml generated/systeminterfaces/remotedesktop)
|
geninterface(systeminterfaces/org.freedesktop.portal.RemoteDesktop.xml generated/systeminterfaces/remotedesktop)
|
||||||
|
geninterface(systeminterfaces/org.freedesktop.portal.InputCapture.xml generated/systeminterfaces/inputcapture)
|
||||||
|
geninterface(systeminterfaces/org.freedesktop.portal.Request.xml generated/systeminterfaces/request)
|
||||||
|
geninterface(systeminterfaces/org.freedesktop.portal.Session.xml generated/systeminterfaces/session)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_library(kdeconnectinterfaces STATIC)
|
add_library(kdeconnectinterfaces STATIC)
|
||||||
|
|
|
@ -0,0 +1,530 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2022 Red Hat, Inc.
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Author: Matthias Clasen <mclasen@redhat.com>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
|
||||||
|
<!--
|
||||||
|
org.freedesktop.portal.InputCapture:
|
||||||
|
@short_description: Portal for permitting input capture
|
||||||
|
|
||||||
|
The InputCapture portal allows capture input events from connected
|
||||||
|
physical or logical devices. Capturing input has two distinct states:
|
||||||
|
"enabled" where an application has requested that input should be captured
|
||||||
|
once certain conditions are met (but no input events are being delivered
|
||||||
|
yet) and "active", when input events are being delivered to the application.
|
||||||
|
An application can only control the "enabled" state, the compositor decides
|
||||||
|
when to switch into the "active" state - and which devices to capture.
|
||||||
|
|
||||||
|
Once the compositor activates input capturing, events from physical or
|
||||||
|
logical devices are sent directly to the application instead of using
|
||||||
|
those events to update the pointer position on-screen. The compositor
|
||||||
|
is in control of the input capturing and may filter events and/or stop
|
||||||
|
capturing at any time.
|
||||||
|
|
||||||
|
Input capturing is an asynchronous operation using "triggers". An
|
||||||
|
application sets up a number of triggers but it is the compositor who
|
||||||
|
decides when the trigger conditions are met. Currently, the only defined
|
||||||
|
trigger are pointer barriers: horizontal or vertical "lines" on the screen
|
||||||
|
that should trigger when the cursor moves across those lines.
|
||||||
|
See org.freedesktop.portal.InputCapture.SetPointerBarriers().
|
||||||
|
|
||||||
|
There is currently no way for an application to activate immediate input
|
||||||
|
capture.
|
||||||
|
|
||||||
|
The InputCapture portal merely *manages* the logic when input should be
|
||||||
|
captured. The transport of actual input events is delegated to a
|
||||||
|
transport layer, specifically libei. See org.freedesktop.portal.InputCapture.ConnectToEIS().
|
||||||
|
|
||||||
|
This documentation describes version 1 of this interface.
|
||||||
|
-->
|
||||||
|
<interface name="org.freedesktop.portal.InputCapture">
|
||||||
|
<!--
|
||||||
|
CreateSession:
|
||||||
|
@parent_window: Identifier for the application window, see :doc:`window-identifiers`
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
@handle: Object path for the :ref:`org.freedesktop.portal.Request` object representing this call
|
||||||
|
|
||||||
|
Create a capture input session. A successfully created session can at
|
||||||
|
any time be closed using :ref:`org.freedesktop.portal.Session.Close`, or may
|
||||||
|
at any time be closed by the portal implementation, which will be
|
||||||
|
signalled via :ref:`org.freedesktop.portal.Session::Closed`.
|
||||||
|
|
||||||
|
Supported keys in the @options vardict include:
|
||||||
|
|
||||||
|
* ``handle_token`` (``s``)
|
||||||
|
|
||||||
|
A string that will be used as the last element of the @handle. Must be a valid
|
||||||
|
object path element. See the :ref:`org.freedesktop.portal.Request` documentation for
|
||||||
|
more information about the @handle.
|
||||||
|
|
||||||
|
* ``session_handle_token`` (``s``)
|
||||||
|
|
||||||
|
A string that will be used as the last element of the session handle. Must be a valid
|
||||||
|
object path element. See the :ref:`org.freedesktop.portal.Session` documentation for
|
||||||
|
more information about the session handle.
|
||||||
|
|
||||||
|
* ``capabilities`` (``u``)
|
||||||
|
|
||||||
|
Bitmask of requested capabilities, see the SupportedCapabilities property.
|
||||||
|
This value is required and must not be zero.
|
||||||
|
|
||||||
|
The following results get returned via the :ref:`org.freedesktop.portal.Request::Response` signal:
|
||||||
|
|
||||||
|
* ``session_handle`` (``o``)
|
||||||
|
|
||||||
|
The session handle. An object path for the
|
||||||
|
:ref:`org.freedesktop.portal.Session` object representing the created
|
||||||
|
session.
|
||||||
|
|
||||||
|
* ``capabilities`` (``u``)
|
||||||
|
|
||||||
|
The capabilities available to this session. This is always a
|
||||||
|
subset of the requested capabilities.
|
||||||
|
See the SupportedCapabilities property for details. Note that
|
||||||
|
while a capability may be available to a session, there is no
|
||||||
|
guarantee a device with that capability is currently available
|
||||||
|
or if one does become available that it will trigger input capture.
|
||||||
|
|
||||||
|
It is best to view this set as a negative confirmation - a
|
||||||
|
capability that was requested but is missing is an indication that
|
||||||
|
this application may not capture events of that capability.
|
||||||
|
-->
|
||||||
|
<method name="CreateSession">
|
||||||
|
<arg type="s" name="parent_window" direction="in"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="o" name="handle" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
GetZones:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
@handle: Object path for the :ref:`org.freedesktop.portal.Request` object representing this call
|
||||||
|
|
||||||
|
Retrieve the set of currently available input zones for this session.
|
||||||
|
The zones may not be continuous and may be a logical representation
|
||||||
|
of the physical screens (e.g. a 4k screen may be represented as
|
||||||
|
low-resolution screen instead). A set of zones is identified by a unique
|
||||||
|
zone_set ID.
|
||||||
|
|
||||||
|
The name "Zone" was chosen to provide distinction with the libei
|
||||||
|
"Region".
|
||||||
|
|
||||||
|
If the zones change (e.g. a monitor is unplugged), the
|
||||||
|
#org.freedesktop.portal.InputCapture::ZonesChanged signal is emitted
|
||||||
|
and the application should re-request the current zones to update its
|
||||||
|
internal state.
|
||||||
|
|
||||||
|
Note that zones are session-specific, there is no guarantee that two
|
||||||
|
applications see the same screen zones. An empty zone list implies
|
||||||
|
that no pointer barriers can be set.
|
||||||
|
|
||||||
|
Whenever the application calls GetZones, the current
|
||||||
|
zone_set ID is returned that references the current set of zones. To
|
||||||
|
establish a pointer barrier, the application must pass this ID to
|
||||||
|
org.freedesktop.portal.InputCapture.SetPointerBarriers(). A mismatch of
|
||||||
|
zone_set IDs implies the application is not using the current zone set and
|
||||||
|
pointer barriers will fail.
|
||||||
|
|
||||||
|
The zone_set ID increases by an unspecified amount with each change to
|
||||||
|
the set of zones. Applications must be able to handle the zone_set ID
|
||||||
|
wrapping around. Implementations of this portal must to increase
|
||||||
|
the zone_set ID by a sensible amount to allow for
|
||||||
|
wrapping detection.
|
||||||
|
|
||||||
|
The following results get returned via the :ref:`org.freedesktop.portal.Request::Response` signal:
|
||||||
|
|
||||||
|
* ``zones`` (``a(uuii)``)
|
||||||
|
|
||||||
|
An array of regions, each specifying that zone's width, height,
|
||||||
|
x/y offset.
|
||||||
|
|
||||||
|
* ``zone_set`` (``u``)
|
||||||
|
|
||||||
|
A unique ID to be used in the
|
||||||
|
org.freedesktop.portal.InputCapture.SetPointerBarriers() method to refer to
|
||||||
|
this set of zones. This id increases by an unspecified
|
||||||
|
amount whenever the zones change and pointer barriers can only be set up
|
||||||
|
if the zone_set matches the most recent returned zone_set.
|
||||||
|
-->
|
||||||
|
<method name="GetZones">
|
||||||
|
<arg type="o" name="session_handle" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
<arg type="o" name="handle" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
SetPointerBarriers:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
@barriers: An array of vardicts, each specifying one barrier
|
||||||
|
@zone_set: A unique zone_set ID referring to the zone set
|
||||||
|
@handle: Object path for the :ref:`org.freedesktop.portal.Request` object representing this call
|
||||||
|
|
||||||
|
Set up zero or more pointer barriers. Pointer barriers are horizontal
|
||||||
|
or vertical "lines" that should trigger the start of input capture when the cursor moves
|
||||||
|
across the pointer barrier. After a successful
|
||||||
|
org.freedesktop.portal.InputCapture.Enable() call and when the
|
||||||
|
compositor has deemed the pointer barrier to be crossed, input events
|
||||||
|
(from compositor-determined input devices) are sent to the application
|
||||||
|
via the transport layer.
|
||||||
|
|
||||||
|
Pointer barriers are situated on the top (for horizontal barriers) or left
|
||||||
|
(for vertical barriers) edge of their respective pixels and their width or height
|
||||||
|
is inclusive each pixel's width or height. In other words, a barrier spanning
|
||||||
|
x1=0, x2=1 is exactly two pixels wide. A pointer barrier must be situated at the
|
||||||
|
outside boundary of the union of all zones.
|
||||||
|
A pointer barrier must be fully contained within one zone.
|
||||||
|
|
||||||
|
For example, consider two zones of size 1920x1080 with offsets
|
||||||
|
0,0 and 1920,0, respectively. (i.e. a left-right dual-monitor setup).
|
||||||
|
The following pointer barriers are permitted:
|
||||||
|
|
||||||
|
- top edge of left screen: `x1=0, y1=0, x2=1919, y1=0`
|
||||||
|
- bottom edge of left screen: `x1=0, y1=1080, x2=1919, y1=1080`
|
||||||
|
- top edge of right screen: `x1=1920, y1=0, x2=3839, y1=0`
|
||||||
|
- bottom edge of right screen: `x1=1920, y1=1080, x2=3839, y1=1080`
|
||||||
|
- left edge of left screen: `x1=0, y1=0, x2=0, y1=1079`
|
||||||
|
- right edge of right screen: `x1=3840, y1=0, x2=3840, y1=1079`
|
||||||
|
|
||||||
|
A pointer barrier is considered triggered when the pointer would
|
||||||
|
logically move off that zone, even if the actual cusor movement is
|
||||||
|
clipped to the zone.
|
||||||
|
|
||||||
|
A zero-sized array of pointer barriers removes all existing pointer barriers
|
||||||
|
for this session. Setting pointer barriers immediately suspends the
|
||||||
|
current session and the application must call org.freedesktop.portal.InputCapture.Enable()
|
||||||
|
after this method.
|
||||||
|
|
||||||
|
The @zone_set must be equivalent to the last returned zone_set of the
|
||||||
|
org.freedesktop.portal.InputCapture.GetZones() method. A mismatch of ids
|
||||||
|
implies the application is not using the current zone set and
|
||||||
|
pointer barriers will fail.
|
||||||
|
|
||||||
|
Supported keys in the @options vardict include:
|
||||||
|
|
||||||
|
* ``handle_token`` (``s``)
|
||||||
|
|
||||||
|
A string that will be used as the last element of the @handle. Must be a valid
|
||||||
|
object path element. See the :ref:`org.freedesktop.portal.Request` documentation for
|
||||||
|
more information about the @handle.
|
||||||
|
|
||||||
|
Supported keys in the @barriers vardicts include:
|
||||||
|
|
||||||
|
* ``barrier_id`` (``u``)
|
||||||
|
|
||||||
|
The non-zero id of this barrier. This id is used in the
|
||||||
|
#org.freedesktop.portal.InputCapture::Activated signal to inform
|
||||||
|
which barrier triggered input capture.
|
||||||
|
|
||||||
|
* ``position`` (``(iiii)``)
|
||||||
|
|
||||||
|
The x1/y1 x2/y2 position of the pointer barrier. A horizontal
|
||||||
|
pointer barrier must have y1 == y2, a vertical pointer barrier
|
||||||
|
must have x1 == x2. Diagonal pointer barriers are not supported.
|
||||||
|
|
||||||
|
The following results get returned via the :ref:`org.freedesktop.portal.Request::Response` signal:
|
||||||
|
|
||||||
|
* ``failed_barriers`` (``au``)
|
||||||
|
|
||||||
|
An array of barrier_ids of pointer barriers that have been denied. The
|
||||||
|
id matches the barrier_id of the entries in the @barriers argument.
|
||||||
|
-->
|
||||||
|
<method name="SetPointerBarriers">
|
||||||
|
<arg type="o" name="session_handle" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QList<QVariantMap>"/>
|
||||||
|
<arg type="aa{sv}" name="barriers" direction="in"/>
|
||||||
|
<arg type="u" name="zone_set" direction="in"/>
|
||||||
|
<arg type="o" name="handle" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enable:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
Enable input capturing. This does not immediately trigger capture, it
|
||||||
|
merely enables the capturing to be triggered at some future point
|
||||||
|
(e.g. by the cursor moving across a barrier). If and when that happens,
|
||||||
|
the #org.freedesktop.portal.InputCapture::Activated signal is emitted.
|
||||||
|
-->
|
||||||
|
<method name="Enable">
|
||||||
|
<arg type="o" name="session_handle" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Disable:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
Disable input capturing. No
|
||||||
|
#org.freedesktop.portal.InputCapture::Disabled signal will be emitted.
|
||||||
|
|
||||||
|
If input capturing is currently ongoing, no
|
||||||
|
#org.freedesktop.portal.InputCapture::Deactivated signal will be
|
||||||
|
emitted.
|
||||||
|
|
||||||
|
Due to the asynchronous nature of this protocol,
|
||||||
|
#org.freedesktop.portal.InputCapture::Deactivated
|
||||||
|
and/or #org.freedesktop.portal.InputCapture::Deactivated signals may
|
||||||
|
nevertheless be received by the application after a call to
|
||||||
|
org.freedesktop.portal.InputCapture.Enable().
|
||||||
|
|
||||||
|
Input events will not be captured until a subsequent
|
||||||
|
org.freedesktop.portal.InputCapture.Enable() call.
|
||||||
|
-->
|
||||||
|
<method name="Disable">
|
||||||
|
<arg type="o" name="session_handle" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Release:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
Release any ongoing input capture. No
|
||||||
|
#org.freedesktop.portal.InputCapture::Deactivated signal will be emitted.
|
||||||
|
|
||||||
|
Due to the asynchronous nature of this protocol, a
|
||||||
|
#org.freedesktop.portal.InputCapture::Deactivated
|
||||||
|
signal may
|
||||||
|
nevertheless be received by the application after a call to
|
||||||
|
org.freedesktop.portal.InputCapture.Release().
|
||||||
|
|
||||||
|
The activation_id provided in the @options vardict specifies which
|
||||||
|
currently ongoing input capture should be terminated. The asynchronous
|
||||||
|
nature of this portal allows for an input capture to be Deactivated and
|
||||||
|
a new input capture to be Activated before the client requests the
|
||||||
|
Release for the previous input capture.
|
||||||
|
|
||||||
|
A compositor should ignore a
|
||||||
|
org.freedesktop.portal.InputCapture.Release() request for a no longer active
|
||||||
|
activation_id.
|
||||||
|
|
||||||
|
Supported keys in the @options vardict include:
|
||||||
|
|
||||||
|
* ``activation_id`` (``u``)
|
||||||
|
|
||||||
|
The same activation_id number as in the
|
||||||
|
#org.freedesktop.portal.InputCapture::Activated signal.
|
||||||
|
|
||||||
|
* ``cursor_position`` (``(dd)``)
|
||||||
|
|
||||||
|
The suggested cursor position within the Zones available in
|
||||||
|
this session.
|
||||||
|
|
||||||
|
This is a suggestion to the compositor to place the cursor in
|
||||||
|
the correct position to allow for fluent movement between virtual
|
||||||
|
screens. The compositor is not required to honor this suggestion.
|
||||||
|
-->
|
||||||
|
<method name="Release">
|
||||||
|
<arg type="o" name="session_handle" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ConnectToEIS:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
@fd: A file descriptor to an active EIS implementation that can be passed to a passive libei context
|
||||||
|
|
||||||
|
Set up the connection to an active EIS implementation. Once input capturing starts,
|
||||||
|
input events are sent via the EI protocol between the compositor and the application.
|
||||||
|
This call must be invoked before org.freedesktop.portal.InputCapture.Enable().
|
||||||
|
|
||||||
|
A session only needs to set this up once, the EIS implementation is not affected by
|
||||||
|
calls to org.freedesktop.portal.InputCapture.Disable() and org.freedesktop.portal.InputCapture.Enable() -
|
||||||
|
the same connection can be re-used until the session is closed.
|
||||||
|
-->
|
||||||
|
<method name="ConnectToEIS">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="connect_to_eis"/>
|
||||||
|
<annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
|
||||||
|
<arg type="o" name="session_handle" direction="in"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/>
|
||||||
|
<arg type="h" name="fd" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Disabled:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
The Disabled signal is emitted when the application will no longer
|
||||||
|
receive captured input. If input capturing is currently ongoing, the
|
||||||
|
#org.freedesktop.portal.InputCapture::Deactivated signal is emitted
|
||||||
|
before this signal.
|
||||||
|
|
||||||
|
Applications must call org.freedesktop.portal.InputCapture.Enable() to
|
||||||
|
request future input capturing for this session.
|
||||||
|
-->
|
||||||
|
<signal name="Disabled">
|
||||||
|
<arg type="o" name="session_handle" direction="out"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="out"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Activated:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
The Activated signal is emitted when input capture starts and input events
|
||||||
|
are about to be sent to the application.
|
||||||
|
|
||||||
|
This signal is only emitted after a prior call
|
||||||
|
to org.freedesktop.portal.InputCapture.Enable().
|
||||||
|
|
||||||
|
Supported keys in the @options vardict include:
|
||||||
|
|
||||||
|
* ``activation_id`` (``u``)
|
||||||
|
|
||||||
|
A number that can be used to synchronize with the
|
||||||
|
transport-layer. This number has no intrinsic meaning but
|
||||||
|
is guaranteed to increase by an unspecified amount on each call.
|
||||||
|
|
||||||
|
In particular: if the compositor sends a activation_id of N as
|
||||||
|
part of this request it will also set the sequence in EIS'
|
||||||
|
start_emulating event the same value N on the EIS connection
|
||||||
|
before the first event from a device is sent.
|
||||||
|
This allows an application to have a synchronization point and
|
||||||
|
attribute an event sequence to the portal interaction.
|
||||||
|
|
||||||
|
Applications must be able to handle the activation_id number
|
||||||
|
wrapping around. Implementations of this portal must increase
|
||||||
|
the activation_id number by a sensible amount to allow for
|
||||||
|
wrapping detection.
|
||||||
|
|
||||||
|
* ``cursor_position`` (``(dd)``)
|
||||||
|
|
||||||
|
The current cursor position in the same coordinate space as
|
||||||
|
the Zones. Note that this position is usually outside the Zones
|
||||||
|
available to this session as all PointerBarriers are at the edge
|
||||||
|
of their respective Zones.
|
||||||
|
|
||||||
|
For example, a fast movement against a barrier on the right edge
|
||||||
|
of a screen may logically put the cursor dozens of pixels into
|
||||||
|
the (non-existing) screen on the other side of the barrier.
|
||||||
|
It is the application's responsibility to calculate and adjust the
|
||||||
|
cursor position as necessary.
|
||||||
|
|
||||||
|
* ``barrier_id`` (``u``)
|
||||||
|
|
||||||
|
The barrier id of the barrier that triggered. If the value is
|
||||||
|
nonzero, it matches the barrier id as specified in
|
||||||
|
org.freedesktop.portal.InputCapture.SetPointerBarriers().
|
||||||
|
|
||||||
|
If the id is zero, the pointer barrier could not be determined.
|
||||||
|
If the id is missing, the input capture was not triggered by a
|
||||||
|
pointer barrier.
|
||||||
|
|
||||||
|
Where more than one pointer barrier are triggered by the same
|
||||||
|
movement it is up to the compositor to choose one barrier (or use
|
||||||
|
a barrier id of zero).
|
||||||
|
-->
|
||||||
|
<signal name="Activated">
|
||||||
|
<arg type="o" name="session_handle" direction="out"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="out"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Deactivated:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
The Deactivated signal is emitted when input capture stopped and input events
|
||||||
|
are no longer sent to the application. To prevent future input
|
||||||
|
capture, an application must call org.freedesktop.portal.InputCapture.Disable().
|
||||||
|
|
||||||
|
Supported keys in the @options vardict include:
|
||||||
|
|
||||||
|
* ``activation_id`` (``u``)
|
||||||
|
|
||||||
|
The same activation_id number as in the corresponding
|
||||||
|
#org.freedesktop.portal.InputCapture::Activated signal.
|
||||||
|
-->
|
||||||
|
<signal name="Deactivated">
|
||||||
|
<arg type="o" name="session_handle" direction="out"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="out"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ZonesChanged:
|
||||||
|
@session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
|
||||||
|
@options: Vardict with optional further information
|
||||||
|
|
||||||
|
The ZonesChanged signal is emitted when the set of zones
|
||||||
|
available **to this session** change. An application should immediately
|
||||||
|
call org.freedesktop.portal.InputCapture.GetZones() to update its state
|
||||||
|
of the zones followed by
|
||||||
|
org.freedesktop.portal.InputCapture.SetPointerBarriers() to re-establish
|
||||||
|
the pointer barriers.
|
||||||
|
|
||||||
|
The ZonesChanged signal includes the zone_set ID of the set of zones
|
||||||
|
invalidated, see #org.freedesktop.portal.InputCapture.GetZones().
|
||||||
|
Due to the asynchronous nature of this protocol, the zone_set ID may
|
||||||
|
identify a set of zones that the application has never (or not yet) seen.
|
||||||
|
Applications must be able to handle unknown zone_set IDs. In particular,
|
||||||
|
because the zone_set ID is guaranteed to increment, an application holding
|
||||||
|
a zone_set ID higher than the ID in this signal can usually simply
|
||||||
|
discard the signal.
|
||||||
|
|
||||||
|
Supported keys in the @options vardict include:
|
||||||
|
|
||||||
|
* ``zone_set`` (``u``)
|
||||||
|
|
||||||
|
The zone_set ID of the invalidated zone set as described in
|
||||||
|
org.freedesktop.portal.InputCapture.GetZones()
|
||||||
|
-->
|
||||||
|
<signal name="ZonesChanged">
|
||||||
|
<arg type="o" name="session_handle" direction="out"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
|
||||||
|
<arg type="a{sv}" name="options" direction="out"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
SupportedCapabilities:
|
||||||
|
|
||||||
|
A bitmask of supported capabilities. This list is constant, it is not the list of
|
||||||
|
capabilities currently available but rather which capabilies are
|
||||||
|
implemented by the portal.
|
||||||
|
|
||||||
|
Applications must ignore unknown capabilities.
|
||||||
|
|
||||||
|
Currently defined types are:
|
||||||
|
|
||||||
|
- ``1``: KEYBOARD
|
||||||
|
- ``2``: POINTER
|
||||||
|
- ``4``: TOUCHSCREEN
|
||||||
|
-->
|
||||||
|
<property name="SupportedCapabilities" type="u" access="read"/>
|
||||||
|
<property name="version" type="u" access="read"/>
|
||||||
|
</interface>
|
||||||
|
</node>
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2015 Red Hat, Inc.
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Author: Alexander Larsson <alexl@redhat.com>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
|
||||||
|
<!--
|
||||||
|
org.freedesktop.portal.Request:
|
||||||
|
@short_description: Shared request interface
|
||||||
|
|
||||||
|
The Request interface is shared by all portal interfaces. When a
|
||||||
|
portal method is called, the reply includes a handle (i.e. object path)
|
||||||
|
for a Request object, which will stay alive for the duration of the
|
||||||
|
user interaction related to the method call.
|
||||||
|
|
||||||
|
The portal indicates that a portal request interaction is over by
|
||||||
|
emitting the #org.freedesktop.portal.Request::Response signal on the
|
||||||
|
Request object.
|
||||||
|
|
||||||
|
The application can abort the interaction calling
|
||||||
|
org.freedesktop.portal.Request.Close() on the Request object.
|
||||||
|
|
||||||
|
Since version 0.9 of xdg-desktop-portal, the handle will be of the form
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
/org/freedesktop/portal/desktop/request/SENDER/TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
where ``SENDER`` is the callers unique name, with the initial ``':'`` removed and
|
||||||
|
all ``'.'`` replaced by ``'_'``, and ``TOKEN`` is a unique token that the caller provided
|
||||||
|
with the handle_token key in the options vardict.
|
||||||
|
|
||||||
|
This change was made to let applications subscribe to the Response signal before
|
||||||
|
making the initial portal call, thereby avoiding a race condition. It is recommended
|
||||||
|
that the caller should verify that the returned handle is what it expected, and update
|
||||||
|
its signal subscription if it isn't. This ensures that applications will work with both
|
||||||
|
old and new versions of xdg-desktop-portal.
|
||||||
|
|
||||||
|
The token that the caller provides should be unique and not guessable. To avoid clashes
|
||||||
|
with calls made from unrelated libraries, it is a good idea to use a per-library prefix
|
||||||
|
combined with a random number.
|
||||||
|
-->
|
||||||
|
<interface name="org.freedesktop.portal.Request">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Close:
|
||||||
|
|
||||||
|
Closes the portal request to which this object refers and ends all
|
||||||
|
related user interaction (dialogs, etc).
|
||||||
|
|
||||||
|
A Response signal will not be emitted in this case.
|
||||||
|
-->
|
||||||
|
<method name="Close">
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Response:
|
||||||
|
@response: Numeric response
|
||||||
|
@results: Vardict with results. The keys and values in the vardict depend on the request.
|
||||||
|
|
||||||
|
Emitted when the user interaction for a portal request is over.
|
||||||
|
|
||||||
|
The @response indicates how the user interaction ended:
|
||||||
|
|
||||||
|
- 0: Success, the request is carried out
|
||||||
|
- 1: The user cancelled the interaction
|
||||||
|
- 2: The user interaction was ended in some other way
|
||||||
|
-->
|
||||||
|
<signal name="Response">
|
||||||
|
<arg type="u" name="response"/>
|
||||||
|
<arg type="a{sv}" name="results"/>
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
|
||||||
|
</signal>
|
||||||
|
</interface>
|
||||||
|
</node>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2017 Red Hat, Inc.
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Author: Jonas Ådahl <jadahl@redhat.com>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
|
||||||
|
<!--
|
||||||
|
org.freedesktop.portal.Session:
|
||||||
|
@short_description: Shared session interface
|
||||||
|
|
||||||
|
The Session interface is shared by all portal interfaces that involve
|
||||||
|
long lived sessions. When a method that creates a session is called, if
|
||||||
|
successful, the reply will include a session handle (i.e. object path) for
|
||||||
|
a Session object, which will stay alive for the duration of the session.
|
||||||
|
|
||||||
|
The duration of the session is defined by the interface that creates it.
|
||||||
|
For convenience, the interface contains a method
|
||||||
|
org.freedesktop.portal.Session.Close(), and a signal
|
||||||
|
#org.freedesktop.portal.Session::Closed. Whether it is allowed to directly
|
||||||
|
call org.freedesktop.portal.Session.Close() depends on the interface.
|
||||||
|
|
||||||
|
The handle of a session will be of the form
|
||||||
|
``/org/freedesktop/portal/desktop/session/SENDER/TOKEN``, where ``SENDER``
|
||||||
|
is the caller's unique name, with the initial ``:`` removed and all ``.``
|
||||||
|
replaced by ``_``, and ``TOKEN`` is a unique token that the caller
|
||||||
|
provided with the ``session_handle_token`` key in the options vardict of
|
||||||
|
the method creating the session.
|
||||||
|
|
||||||
|
The token that the caller provides should be unique and not guessable. To
|
||||||
|
avoid clashes with calls made from unrelated libraries, it is a good idea
|
||||||
|
to use a per-library prefix combined with a random number.
|
||||||
|
|
||||||
|
A client who started a session vanishing from the D-Bus is equivalent to
|
||||||
|
closing all active sessions made by said client.
|
||||||
|
-->
|
||||||
|
<interface name="org.freedesktop.portal.Session">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Close:
|
||||||
|
|
||||||
|
Closes the portal session to which this object refers and ends all
|
||||||
|
related user interaction (dialogs, etc).
|
||||||
|
-->
|
||||||
|
<method name="Close">
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Closed:
|
||||||
|
@details: A key value Vardict with details about the closed session.
|
||||||
|
|
||||||
|
Emitted when a session is closed.
|
||||||
|
|
||||||
|
The content of @details is specified by the interface creating the session.
|
||||||
|
-->
|
||||||
|
<signal name="Closed">
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
|
||||||
|
<arg type="a{sv}" name="details"/>
|
||||||
|
</signal>
|
||||||
|
<property name="version" type="u" access="read"/>
|
||||||
|
</interface>
|
||||||
|
</node>
|
|
@ -43,6 +43,8 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
if (TARGET KF6::ModemManagerQt)
|
if (TARGET KF6::ModemManagerQt)
|
||||||
add_subdirectory(mmtelephony)
|
add_subdirectory(mmtelephony)
|
||||||
endif()
|
endif()
|
||||||
|
add_subdirectory(shareinputdevices)
|
||||||
|
add_subdirectory(shareinputdevicesremote)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NOT APPLE)
|
if(NOT APPLE)
|
||||||
|
|
19
plugins/shareinputdevices/CMakeLists.txt
Normal file
19
plugins/shareinputdevices/CMakeLists.txt
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
kdeconnect_add_plugin(kdeconnect_shareinputdevices SOURCES shareinputdevicesplugin.cpp inputcapturesession.cpp)
|
||||||
|
|
||||||
|
pkg_check_modules(PKG_libei REQUIRED IMPORTED_TARGET libei-1.0)
|
||||||
|
|
||||||
|
target_link_libraries(kdeconnect_shareinputdevices
|
||||||
|
kdeconnectcore
|
||||||
|
kdeconnectinterfaces
|
||||||
|
KF6::I18n
|
||||||
|
Qt::GuiPrivate
|
||||||
|
PkgConfig::PKG_libei
|
||||||
|
)
|
||||||
|
|
||||||
|
kdeconnect_add_kcm(kdeconnect_shareinputdevices_config SOURCES shareinputdevices_config.cpp)
|
||||||
|
ki18n_wrap_ui(kdeconnect_shareinputdevices_config shareinputdevices_config.ui)
|
||||||
|
target_link_libraries(kdeconnect_shareinputdevices_config
|
||||||
|
kdeconnectpluginkcm
|
||||||
|
kdeconnectinterfaces
|
||||||
|
KF6::I18n
|
||||||
|
)
|
1
plugins/shareinputdevices/README
Normal file
1
plugins/shareinputdevices/README
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This plugin allows sharing keyboard and mouse with another machine.
|
450
plugins/shareinputdevices/inputcapturesession.cpp
Normal file
450
plugins/shareinputdevices/inputcapturesession.cpp
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "inputcapturesession.h"
|
||||||
|
#include "plugin_shareinputdevices_debug.h"
|
||||||
|
|
||||||
|
#include "generated/systeminterfaces/inputcapture.h"
|
||||||
|
#include "generated/systeminterfaces/request.h"
|
||||||
|
#include "generated/systeminterfaces/session.h"
|
||||||
|
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
#include <private/qxkbcommon_p.h>
|
||||||
|
|
||||||
|
#include <libei.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
static QString portalName()
|
||||||
|
{
|
||||||
|
return u"org.freedesktop.portal.Desktop"_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString portalPath()
|
||||||
|
{
|
||||||
|
return u"/org/freedesktop/portal/desktop"_s;
|
||||||
|
};
|
||||||
|
|
||||||
|
static QString requestPath(const QString &token)
|
||||||
|
{
|
||||||
|
static QString senderString = QDBusConnection::sessionBus().baseService().remove(0, 1).replace(u'.', u'_');
|
||||||
|
return u"%1/request/%2/%3"_s.arg(portalPath()).arg(senderString).arg(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Xkb
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Xkb()
|
||||||
|
{
|
||||||
|
m_xkbcontext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS));
|
||||||
|
m_xkbkeymap.reset(xkb_keymap_new_from_names(m_xkbcontext.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||||
|
m_xkbstate.reset(xkb_state_new(m_xkbkeymap.get()));
|
||||||
|
}
|
||||||
|
Xkb(int keymapFd, int size)
|
||||||
|
{
|
||||||
|
m_xkbcontext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS));
|
||||||
|
char *map = static_cast<char *>(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, keymapFd, 0));
|
||||||
|
if (map != MAP_FAILED) {
|
||||||
|
m_xkbkeymap.reset(xkb_keymap_new_from_string(m_xkbcontext.get(), map, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||||
|
munmap(map, size);
|
||||||
|
}
|
||||||
|
close(keymapFd);
|
||||||
|
if (!m_xkbkeymap) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Failed to create keymap";
|
||||||
|
m_xkbkeymap.reset(xkb_keymap_new_from_names(m_xkbcontext.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||||
|
}
|
||||||
|
m_xkbstate.reset(xkb_state_new(m_xkbkeymap.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateModifiers(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group)
|
||||||
|
{
|
||||||
|
xkb_state_update_mask(m_xkbstate.get(), depressed, latched, locked, 0, 0, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateKey(uint32_t key, bool pressed)
|
||||||
|
{
|
||||||
|
xkb_state_update_key(m_xkbstate.get(), key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
xkb_state *currentState() const
|
||||||
|
{
|
||||||
|
return m_xkbstate.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template<auto D>
|
||||||
|
using deleter = std::integral_constant<decltype(D), D>;
|
||||||
|
std::unique_ptr<xkb_context, deleter<xkb_context_unref>> m_xkbcontext;
|
||||||
|
std::unique_ptr<xkb_keymap, deleter<xkb_keymap_unref>> m_xkbkeymap;
|
||||||
|
std::unique_ptr<xkb_state, deleter<xkb_state_unref>> m_xkbstate;
|
||||||
|
};
|
||||||
|
|
||||||
|
InputCaptureSession::InputCaptureSession(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_xkb(new Xkb)
|
||||||
|
, m_inputCapturePortal(new OrgFreedesktopPortalInputCaptureInterface(portalName(), portalPath(), QDBusConnection::sessionBus(), this))
|
||||||
|
|
||||||
|
{
|
||||||
|
connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::Disabled, this, &InputCaptureSession::disabled);
|
||||||
|
connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::Activated, this, &InputCaptureSession::activated);
|
||||||
|
connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::Deactivated, this, &InputCaptureSession::deactivated);
|
||||||
|
connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::ZonesChanged, this, &InputCaptureSession::zonesChanged);
|
||||||
|
|
||||||
|
const QString token = u"kdeconnect_shareinputdevices%1"_s.arg(QRandomGenerator::global()->generate());
|
||||||
|
auto request = new OrgFreedesktopPortalRequestInterface(portalName(), requestPath(token), QDBusConnection::sessionBus(), this);
|
||||||
|
connect(request, &OrgFreedesktopPortalRequestInterface::Response, request, &QObject::deleteLater);
|
||||||
|
connect(request, &OrgFreedesktopPortalRequestInterface::Response, this, &InputCaptureSession::sessionCreated);
|
||||||
|
auto call = m_inputCapturePortal->CreateSession(QString(), {{u"handle_token"_s, token}, {u"session_handle_token"_s, token}, {u"capabilities"_s, 3u}});
|
||||||
|
connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, request, [request](QDBusPendingCallWatcher *watcher) {
|
||||||
|
watcher->deleteLater();
|
||||||
|
if (watcher->isError()) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error creating input capture session" << watcher->error();
|
||||||
|
request->deleteLater();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
InputCaptureSession::~InputCaptureSession()
|
||||||
|
{
|
||||||
|
if (m_session) {
|
||||||
|
m_session->Close();
|
||||||
|
}
|
||||||
|
if (m_ei) {
|
||||||
|
ei_unref(m_ei);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::sessionCreated(uint response, const QVariantMap &results)
|
||||||
|
{
|
||||||
|
if (response != 0) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Couldn't create input capture session";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_session = std::make_unique<OrgFreedesktopPortalSessionInterface>(portalName(),
|
||||||
|
results[u"session_handle"_s].value<QDBusObjectPath>().path(),
|
||||||
|
QDBusConnection::sessionBus());
|
||||||
|
connect(m_session.get(), &OrgFreedesktopPortalSessionInterface::Closed, this, &InputCaptureSession::sessionClosed);
|
||||||
|
|
||||||
|
auto call = m_inputCapturePortal->ConnectToEIS(QDBusObjectPath(m_session->path()), {});
|
||||||
|
connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
|
||||||
|
watcher->deleteLater();
|
||||||
|
QDBusReply<QDBusUnixFileDescriptor> reply = *watcher;
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error getting eis fd" << watcher->error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qCDebug(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Received ei fd" << reply.value().fileDescriptor();
|
||||||
|
setupEi(reply.value().takeFileDescriptor());
|
||||||
|
});
|
||||||
|
getZones();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::getZones()
|
||||||
|
{
|
||||||
|
const QString token = u"kdeconnect_shareinputdevices%1"_s.arg(QRandomGenerator::global()->generate());
|
||||||
|
auto request = new OrgFreedesktopPortalRequestInterface(portalName(), requestPath(token), QDBusConnection::sessionBus(), this);
|
||||||
|
connect(request, &OrgFreedesktopPortalRequestInterface::Response, request, &QObject::deleteLater);
|
||||||
|
connect(request, &OrgFreedesktopPortalRequestInterface::Response, this, &InputCaptureSession::zonesReceived);
|
||||||
|
auto call = m_inputCapturePortal->GetZones(QDBusObjectPath(m_session->path()), {{u"handle_token"_s, token}});
|
||||||
|
connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, request, [request](QDBusPendingCallWatcher *watcher) {
|
||||||
|
watcher->deleteLater();
|
||||||
|
if (watcher->isError()) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error getting zones" << watcher->error();
|
||||||
|
request->deleteLater();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::zonesReceived(uint response, const QVariantMap &results)
|
||||||
|
{
|
||||||
|
if (response != 0) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Couldn't create input capture session";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_currentZoneSet = results[u"zone_set"_s].toUInt();
|
||||||
|
m_currentZones.clear();
|
||||||
|
const QDBusArgument zoneArgument = results[u"zones"_s].value<QDBusArgument>();
|
||||||
|
zoneArgument.beginArray();
|
||||||
|
while (!zoneArgument.atEnd()) {
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
uint x;
|
||||||
|
uint y;
|
||||||
|
zoneArgument.beginStructure();
|
||||||
|
zoneArgument >> width >> height >> x >> y;
|
||||||
|
zoneArgument.endStructure();
|
||||||
|
m_currentZones.push_back(QRect(x, y, width, height));
|
||||||
|
}
|
||||||
|
zoneArgument.endArray();
|
||||||
|
|
||||||
|
setUpBarrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::setUpBarrier()
|
||||||
|
{
|
||||||
|
// Find the left/right/bottom/top-most screen
|
||||||
|
if (m_barrierEdge == Qt::LeftEdge) {
|
||||||
|
std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) {
|
||||||
|
return lhs.x() < rhs.x();
|
||||||
|
});
|
||||||
|
const auto &zone = m_currentZones.front();
|
||||||
|
// Deliberate QRect::bottom usage, on a 1920x1080 screen this needs to be 1079
|
||||||
|
m_barrier = {zone.x(), zone.y(), zone.x(), zone.bottom()};
|
||||||
|
} else if (m_barrierEdge == Qt::RightEdge) {
|
||||||
|
std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) {
|
||||||
|
return lhs.x() + lhs.width() > rhs.x() + rhs.width();
|
||||||
|
});
|
||||||
|
const auto &zone = m_currentZones.front();
|
||||||
|
m_barrier = {zone.x() + zone.width(), zone.y(), zone.x() + zone.width(), zone.bottom()};
|
||||||
|
} else if (m_barrierEdge == Qt::TopEdge) {
|
||||||
|
std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) {
|
||||||
|
return lhs.y() < rhs.y();
|
||||||
|
});
|
||||||
|
const auto &zone = m_currentZones.front();
|
||||||
|
// Same here with QRect::right
|
||||||
|
m_barrier = {zone.x(), zone.y(), zone.right(), zone.y()};
|
||||||
|
} else {
|
||||||
|
std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) {
|
||||||
|
return lhs.y() + lhs.height() > rhs.y() + rhs.height();
|
||||||
|
});
|
||||||
|
const auto &zone = m_currentZones.front();
|
||||||
|
m_barrier = {zone.x(), zone.y() + zone.height(), zone.right(), zone.y() + zone.height()};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString token = u"kdeconnect_shareinputdevices%1"_s.arg(QRandomGenerator::global()->generate());
|
||||||
|
auto request = new OrgFreedesktopPortalRequestInterface(portalName(), requestPath(token), QDBusConnection::sessionBus(), this);
|
||||||
|
connect(request, &OrgFreedesktopPortalRequestInterface::Response, request, &QObject::deleteLater);
|
||||||
|
connect(request, &OrgFreedesktopPortalRequestInterface::Response, this, &InputCaptureSession::barriersSet);
|
||||||
|
auto call = m_inputCapturePortal->SetPointerBarriers(
|
||||||
|
QDBusObjectPath(m_session->path()),
|
||||||
|
{{u"handle_token"_s, token}},
|
||||||
|
{{{u"barrier_id"_s, 1}, {u"position"_s, QVariant::fromValue(QList<int>{m_barrier.x1(), m_barrier.y1(), m_barrier.x2(), m_barrier.y2()})}}},
|
||||||
|
m_currentZoneSet);
|
||||||
|
connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, request, [request](QDBusPendingCallWatcher *watcher) {
|
||||||
|
watcher->deleteLater();
|
||||||
|
if (watcher->isError()) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error setting barriers" << watcher->error();
|
||||||
|
request->deleteLater();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::barriersSet(uint response, const QVariantMap &results)
|
||||||
|
{
|
||||||
|
if (response != 0) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Couldn't set barriers";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto failedBarriers = qdbus_cast<QList<uint>>(results[u"failed_barriers"_s].value<QDBusArgument>());
|
||||||
|
if (!failedBarriers.empty()) {
|
||||||
|
qCInfo(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Failed barriers" << failedBarriers;
|
||||||
|
}
|
||||||
|
enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::enable()
|
||||||
|
{
|
||||||
|
auto call = m_inputCapturePortal->Enable(QDBusObjectPath(m_session->path()), {});
|
||||||
|
connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) {
|
||||||
|
watcher->deleteLater();
|
||||||
|
if (watcher->isError()) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Failed enabling input capture session" << watcher->error();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::setBarrierEdge(Qt::Edge edge)
|
||||||
|
{
|
||||||
|
if (edge != m_barrierEdge) {
|
||||||
|
m_barrierEdge = edge;
|
||||||
|
if (!m_currentZones.empty()) {
|
||||||
|
setUpBarrier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::release(const QPointF &position)
|
||||||
|
{
|
||||||
|
qDebug() << "releasing with" << position;
|
||||||
|
m_inputCapturePortal->Release(QDBusObjectPath(m_session->path()), {{u"cursor_position"_s, position}});
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::sessionClosed()
|
||||||
|
{
|
||||||
|
qCCritical(KDECONNECT_PLUGIN_SHAREINPUTDEVICES()) << "input capture session was closed";
|
||||||
|
m_session.reset();
|
||||||
|
m_eiNotifier.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::activated(const QDBusObjectPath &sessionHandle, const QVariantMap &options)
|
||||||
|
{
|
||||||
|
if (!m_session || sessionHandle.path() != m_session->path()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_currentActivationId = options[u"activation_id"_s].toUInt();
|
||||||
|
// uint barrier_id = options[u"barrier_id"].toUInt();
|
||||||
|
auto cursorPosition = qdbus_cast<QPointF>(options[u"cursor_position"_s].value<QDBusArgument>());
|
||||||
|
Q_EMIT started(m_barrier, cursorPosition - m_barrier.p1());
|
||||||
|
for (const auto &event : queuedEiEvents) {
|
||||||
|
handleEiEvent(event);
|
||||||
|
}
|
||||||
|
queuedEiEvents.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::deactivated(const QDBusObjectPath &sessionHandle, const QVariantMap &options)
|
||||||
|
{
|
||||||
|
if (!m_session || sessionHandle.path() != m_session->path()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto deactivatedId = options[u"activation_id"_s].toUInt();
|
||||||
|
Q_UNUSED(deactivatedId)
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::disabled(const QDBusObjectPath &sessionHandle, const QVariantMap &options)
|
||||||
|
{
|
||||||
|
if (!m_session || sessionHandle.path() != m_session->path()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto disabledId = options[u"activation_id"_s].toUInt();
|
||||||
|
Q_UNUSED(disabledId)
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::zonesChanged(const QDBusObjectPath &sessionHandle, const QVariantMap &options)
|
||||||
|
{
|
||||||
|
if (!m_session || sessionHandle.path() != m_session->path()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options[u"zone_set"_s].toUInt() >= m_currentZoneSet) {
|
||||||
|
getZones();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::setupEi(int fd)
|
||||||
|
{
|
||||||
|
m_ei = ei_new_receiver(nullptr);
|
||||||
|
ei_setup_backend_fd(m_ei, fd);
|
||||||
|
m_eiNotifier = std::make_unique<QSocketNotifier>(fd, QSocketNotifier::Read);
|
||||||
|
connect(m_eiNotifier.get(), &QSocketNotifier::activated, this, [this] {
|
||||||
|
ei_dispatch(m_ei);
|
||||||
|
while (auto event = ei_get_event(m_ei)) {
|
||||||
|
handleEiEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputCaptureSession::handleEiEvent(ei_event *event)
|
||||||
|
{
|
||||||
|
const auto type = ei_event_get_type(event);
|
||||||
|
constexpr std::array inputEvents = {
|
||||||
|
EI_EVENT_FRAME,
|
||||||
|
EI_EVENT_POINTER_MOTION,
|
||||||
|
EI_EVENT_POINTER_MOTION_ABSOLUTE,
|
||||||
|
EI_EVENT_BUTTON_BUTTON,
|
||||||
|
EI_EVENT_SCROLL_DELTA,
|
||||||
|
EI_EVENT_SCROLL_DISCRETE,
|
||||||
|
EI_EVENT_SCROLL_STOP,
|
||||||
|
EI_EVENT_SCROLL_CANCEL,
|
||||||
|
EI_EVENT_KEYBOARD_MODIFIERS,
|
||||||
|
EI_EVENT_KEYBOARD_KEY,
|
||||||
|
EI_EVENT_TOUCH_DOWN,
|
||||||
|
EI_EVENT_TOUCH_MOTION,
|
||||||
|
EI_EVENT_TOUCH_UP,
|
||||||
|
};
|
||||||
|
if (m_currentEisSequence > m_currentActivationId && std::find(inputEvents.begin(), inputEvents.end(), type) != inputEvents.end()) {
|
||||||
|
// Wait until DBus activated signal to have correct start position
|
||||||
|
queuedEiEvents.push_back(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case EI_EVENT_CONNECT:
|
||||||
|
qCDebug(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Connected to ei";
|
||||||
|
break;
|
||||||
|
case EI_EVENT_DISCONNECT:
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Disconnected from ei";
|
||||||
|
break;
|
||||||
|
case EI_EVENT_SEAT_ADDED: {
|
||||||
|
auto seat = ei_event_get_seat(event);
|
||||||
|
ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, nullptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EI_EVENT_SEAT_REMOVED:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_DEVICE_ADDED: {
|
||||||
|
auto device = ei_event_get_device(event);
|
||||||
|
if (ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD)) {
|
||||||
|
auto keymap = ei_device_keyboard_get_keymap(device);
|
||||||
|
if (ei_keymap_get_type(keymap) != EI_KEYMAP_TYPE_XKB) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_xkb.reset(new Xkb(ei_keymap_get_fd(keymap), ei_keymap_get_size(keymap)));
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case EI_EVENT_DEVICE_REMOVED:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_DEVICE_START_EMULATING:
|
||||||
|
m_currentEisSequence = ei_event_emulating_get_sequence(event);
|
||||||
|
break;
|
||||||
|
case EI_EVENT_DEVICE_STOP_EMULATING:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_FRAME:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_POINTER_MOTION:
|
||||||
|
if (m_currentEisSequence < m_currentActivationId) {
|
||||||
|
queuedEiEvents.push_back(event);
|
||||||
|
}
|
||||||
|
Q_EMIT mouseMove(ei_event_pointer_get_dx(event), ei_event_pointer_get_dy(event));
|
||||||
|
break;
|
||||||
|
case EI_EVENT_POINTER_MOTION_ABSOLUTE:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_BUTTON_BUTTON:
|
||||||
|
Q_EMIT mouseButton(ei_event_button_get_button(event), ei_event_button_get_is_press(event));
|
||||||
|
break;
|
||||||
|
case EI_EVENT_SCROLL_DELTA:
|
||||||
|
Q_EMIT scrollDelta(ei_event_scroll_get_dx(event), ei_event_scroll_get_dy(event));
|
||||||
|
break;
|
||||||
|
case EI_EVENT_SCROLL_DISCRETE:
|
||||||
|
Q_EMIT scrollDiscrete(ei_event_scroll_get_discrete_dx(event), ei_event_scroll_get_discrete_dy(event));
|
||||||
|
break;
|
||||||
|
case EI_EVENT_SCROLL_STOP:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_SCROLL_CANCEL:
|
||||||
|
break;
|
||||||
|
case EI_EVENT_KEYBOARD_MODIFIERS:
|
||||||
|
m_xkb->updateModifiers(ei_event_keyboard_get_xkb_mods_depressed(event),
|
||||||
|
ei_event_keyboard_get_xkb_mods_latched(event),
|
||||||
|
ei_event_keyboard_get_xkb_mods_locked(event),
|
||||||
|
ei_event_keyboard_get_xkb_group(event));
|
||||||
|
break;
|
||||||
|
case EI_EVENT_KEYBOARD_KEY: {
|
||||||
|
auto xkbKey = ei_event_keyboard_get_key(event) + 8;
|
||||||
|
m_xkb->updateKey(xkbKey, ei_event_keyboard_get_key_is_press(event));
|
||||||
|
// mousepad plugin does press/release in one, trigger on press like remotekeyboard
|
||||||
|
if (ei_event_keyboard_get_key_is_press(event)) {
|
||||||
|
xkb_keysym_t sym = xkb_state_key_get_one_sym(m_xkb->currentState(), xkbKey);
|
||||||
|
Qt::KeyboardModifiers modifiers = QXkbCommon::modifiers(m_xkb->currentState(), sym);
|
||||||
|
auto qtKey = static_cast<Qt::Key>(QXkbCommon::keysymToQtKey(sym, modifiers));
|
||||||
|
const QString text = QXkbCommon::lookupStringNoKeysymTransformations(sym);
|
||||||
|
Q_EMIT key(qtKey, modifiers, text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EI_EVENT_TOUCH_DOWN:
|
||||||
|
case EI_EVENT_TOUCH_MOTION:
|
||||||
|
case EI_EVENT_TOUCH_UP:
|
||||||
|
case EI_EVENT_DEVICE_PAUSED:
|
||||||
|
case EI_EVENT_DEVICE_RESUMED:
|
||||||
|
qCDebug(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Unexpected event of type" << ei_event_get_type(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ei_event_unref(event);
|
||||||
|
}
|
70
plugins/shareinputdevices/inputcapturesession.h
Normal file
70
plugins/shareinputdevices/inputcapturesession.h
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDBusObjectPath>
|
||||||
|
#include <QLine>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class OrgFreedesktopPortalInputCaptureInterface;
|
||||||
|
class QSocketNotifier;
|
||||||
|
class OrgFreedesktopPortalSessionInterface;
|
||||||
|
class Xkb;
|
||||||
|
struct ei;
|
||||||
|
struct ei_event;
|
||||||
|
|
||||||
|
class InputCaptureSession : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
InputCaptureSession(QObject *parent);
|
||||||
|
~InputCaptureSession();
|
||||||
|
|
||||||
|
void setBarrierEdge(Qt::Edge edge);
|
||||||
|
void release(const QPointF &position);
|
||||||
|
Q_SIGNALS:
|
||||||
|
void started(const QLine &barrier, const QPointF &delta);
|
||||||
|
void mouseMove(double dx, double dy);
|
||||||
|
void mouseButton(int button, bool pressed);
|
||||||
|
void scrollDelta(double dx, double dy);
|
||||||
|
void scrollDiscrete(int dx, int dy);
|
||||||
|
void key(Qt::Key key, Qt::KeyboardModifiers modifiers, const QString &text);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void getZones();
|
||||||
|
void setUpBarrier();
|
||||||
|
void enable();
|
||||||
|
|
||||||
|
void sessionCreated(uint response, const QVariantMap &options);
|
||||||
|
void zonesReceived(uint response, const QVariantMap &results);
|
||||||
|
void barriersSet(uint response, const QVariantMap &results);
|
||||||
|
|
||||||
|
void sessionClosed();
|
||||||
|
void disabled(const QDBusObjectPath &sessionHandle, const QVariantMap &options);
|
||||||
|
void activated(const QDBusObjectPath &sessionHandle, const QVariantMap &options);
|
||||||
|
void deactivated(const QDBusObjectPath &sessionHandle, const QVariantMap &options);
|
||||||
|
void zonesChanged(const QDBusObjectPath &sessionHandle, const QVariantMap &options);
|
||||||
|
|
||||||
|
void setupEi(int fd);
|
||||||
|
void handleEiEvent(ei_event *event);
|
||||||
|
|
||||||
|
Qt::Edge m_barrierEdge;
|
||||||
|
QLine m_barrier;
|
||||||
|
|
||||||
|
uint m_currentZoneSet = 0;
|
||||||
|
QList<QRect> m_currentZones;
|
||||||
|
uint m_currentActivationId = 0;
|
||||||
|
uint m_currentEisSequence = 0;
|
||||||
|
QList<ei_event *> queuedEiEvents;
|
||||||
|
std::unique_ptr<QSocketNotifier> m_eiNotifier;
|
||||||
|
std::unique_ptr<OrgFreedesktopPortalSessionInterface> m_session;
|
||||||
|
std::unique_ptr<Xkb> m_xkb;
|
||||||
|
ei *m_ei = nullptr;
|
||||||
|
OrgFreedesktopPortalInputCaptureInterface *m_inputCapturePortal;
|
||||||
|
};
|
23
plugins/shareinputdevices/kdeconnect_shareinputdevices.json
Normal file
23
plugins/shareinputdevices/kdeconnect_shareinputdevices.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "David Redondo",
|
||||||
|
"Email": "kde@david-redondo.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"EnabledByDefault": false,
|
||||||
|
"Icon": "dialog-input-devices",
|
||||||
|
"License": "GPL",
|
||||||
|
"Name": "Share input devices",
|
||||||
|
"Description": "Share mouse and keyboard with the remote device"
|
||||||
|
},
|
||||||
|
"X-KDE-ConfigModule": "kdeconnect/kcms/kdeconnect_shareinputdevices_config",
|
||||||
|
"X-KdeConnect-OutgoingPacketType": [
|
||||||
|
"kdeconnect.mousepad.request",
|
||||||
|
"kdeconnect.shareinputdevices.request"
|
||||||
|
],
|
||||||
|
"X-KdeConnect-SupportedPacketType": [
|
||||||
|
"kdeconnect.shareinputdevices"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQml.Models
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kcmutils as KCMUtils
|
||||||
|
import org.kde.kdeconnect 1.0
|
||||||
|
import org.kde.kitemmodels as KItemModels
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
Kirigami.FormLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string device
|
||||||
|
readonly property string pluginId: "kdeconnect_shareinputdevices"
|
||||||
|
|
||||||
|
KdeConnectPluginConfig {
|
||||||
|
id: config
|
||||||
|
deviceId: device
|
||||||
|
pluginName: pluginId
|
||||||
|
|
||||||
|
onConfigChanged: edgeComboBox.currentIndex = edgeComboBox.indexOfValue(getInt("edge", Qt.LeftEdge))
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Kirigami.FormData.label: i18nc("@label:listbox", "Leave Screen at")
|
||||||
|
QQC2.ComboBox {
|
||||||
|
id: edgeComboBox
|
||||||
|
textRole: "text"
|
||||||
|
valueRole: "value"
|
||||||
|
model: [
|
||||||
|
{
|
||||||
|
text: i18nc("@item:inlistbox top edge of screen", "Top"),
|
||||||
|
value: Qt.TopEdge
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18nc("@item:inlistbox left edge of screen", "Left"),
|
||||||
|
value: Qt.LeftEdge
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18nc("@item:inlistbox right edge of screen", "Right"),
|
||||||
|
value: Qt.RightEdge
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18nc("@item:inlistbox top edge of screen", "Bottom"),
|
||||||
|
value: Qt.BottomEdge
|
||||||
|
}
|
||||||
|
]
|
||||||
|
Component.onCompleted: currentIndex = edgeComboBox.indexOfValue(config.getInt("edge", Qt.LeftEdge))
|
||||||
|
}
|
||||||
|
KCMUtils.ContextualHelpButton {
|
||||||
|
icon.name: "data-warning"
|
||||||
|
toolTipText: i18nc("@info:tooltip", "Another device already reserved this edge. This may lead to unexpected results when both are connected at the same time.")
|
||||||
|
visible: configInstantiator.children.some((deviceConfig) => deviceConfig.getInt("edge", Qt.LeftEdge) == edgeComboBox.currentValue)
|
||||||
|
Instantiator {
|
||||||
|
id: configInstantiator
|
||||||
|
readonly property var children: Array.from({length: configInstantiator.count}, (value, index) => configInstantiator.objectAt(index))
|
||||||
|
delegate: KdeConnectPluginConfig {
|
||||||
|
deviceId: model.deviceId
|
||||||
|
pluginName: pluginId
|
||||||
|
|
||||||
|
}
|
||||||
|
model: KItemModels.KSortFilterProxyModel {
|
||||||
|
sourceModel: DevicesModel {
|
||||||
|
}
|
||||||
|
filterRowCallback: function(source_row, source_parent) {
|
||||||
|
const device = sourceModel.getDevice(source_row)
|
||||||
|
return device.id() !== root.device && device.isPluginEnabled(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
80
plugins/shareinputdevices/shareinputdevices_config.cpp
Normal file
80
plugins/shareinputdevices/shareinputdevices_config.cpp
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "shareinputdevices_config.h"
|
||||||
|
|
||||||
|
#include <interfaces/dbusinterfaces.h>
|
||||||
|
|
||||||
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
K_PLUGIN_CLASS(ShareInputDevicesConfig)
|
||||||
|
|
||||||
|
constexpr auto defaultEdge = Qt::LeftEdge;
|
||||||
|
constexpr auto defaultIndex = 1;
|
||||||
|
|
||||||
|
ShareInputDevicesConfig::ShareInputDevicesConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
|
||||||
|
: KdeConnectPluginKcm(parent, data, args)
|
||||||
|
, m_daemon(new DaemonDbusInterface)
|
||||||
|
{
|
||||||
|
m_ui.setupUi(widget());
|
||||||
|
m_ui.edgeContextualWarning->setVisible(false);
|
||||||
|
m_ui.edgeComboBox->setItemData(0, Qt::TopEdge);
|
||||||
|
m_ui.edgeComboBox->setItemData(1, Qt::LeftEdge);
|
||||||
|
m_ui.edgeComboBox->setItemData(2, Qt::RightEdge);
|
||||||
|
m_ui.edgeComboBox->setItemData(3, Qt::BottomEdge);
|
||||||
|
connect(m_ui.edgeComboBox, &QComboBox::currentIndexChanged, this, &ShareInputDevicesConfig::updateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesConfig::checkEdge()
|
||||||
|
{
|
||||||
|
m_ui.edgeContextualWarning->setVisible(false);
|
||||||
|
const QStringList devices = m_daemon->devices();
|
||||||
|
for (const auto &device : devices) {
|
||||||
|
if (device == deviceId()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!DeviceDbusInterface(device).isPluginEnabled(config()->pluginName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (KdeConnectPluginConfig(device, config()->pluginName()).getInt(u"edge"_s, defaultEdge) == m_ui.edgeComboBox->currentData()) {
|
||||||
|
m_ui.edgeContextualWarning->setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesConfig::updateState()
|
||||||
|
{
|
||||||
|
const int current = m_ui.edgeComboBox->currentData().toInt();
|
||||||
|
unmanagedWidgetChangeState(current != config()->getInt(u"edge"_s, defaultEdge));
|
||||||
|
unmanagedWidgetDefaultState(current == defaultEdge);
|
||||||
|
checkEdge();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesConfig::defaults()
|
||||||
|
{
|
||||||
|
KCModule::defaults();
|
||||||
|
m_ui.edgeComboBox->setCurrentIndex(defaultIndex);
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesConfig::load()
|
||||||
|
{
|
||||||
|
KCModule::load();
|
||||||
|
const int index = m_ui.edgeComboBox->findData((config()->getInt(u"edge"_s, defaultEdge)));
|
||||||
|
m_ui.edgeComboBox->setCurrentIndex(index != -1 ? index : defaultIndex);
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesConfig::save()
|
||||||
|
{
|
||||||
|
KCModule::save();
|
||||||
|
config()->set(u"edge"_s, m_ui.edgeComboBox->currentData());
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "shareinputdevices_config.moc"
|
29
plugins/shareinputdevices/shareinputdevices_config.h
Normal file
29
plugins/shareinputdevices/shareinputdevices_config.h
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kcmplugin/kdeconnectpluginkcm.h"
|
||||||
|
#include "ui_shareinputdevices_config.h"
|
||||||
|
|
||||||
|
class DaemonDbusInterface;
|
||||||
|
|
||||||
|
class ShareInputDevicesConfig : public KdeConnectPluginKcm
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
ShareInputDevicesConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &args);
|
||||||
|
|
||||||
|
void save() override;
|
||||||
|
void load() override;
|
||||||
|
void defaults() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateState();
|
||||||
|
void checkEdge();
|
||||||
|
Ui::ShareInputDevicesConfig m_ui;
|
||||||
|
DaemonDbusInterface *m_daemon;
|
||||||
|
};
|
72
plugins/shareinputdevices/shareinputdevices_config.ui
Normal file
72
plugins/shareinputdevices/shareinputdevices_config.ui
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ShareInputDevicesConfig</class>
|
||||||
|
<widget class="QWidget" name="ShareInputDevicesConfig">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>298</width>
|
||||||
|
<height>66</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="edgeLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string comment="@label:listbox">Leave Screen at</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="edgeComboBox">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string comment="@item:inlistbox top edge of screen">Top</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string comment="@item:inlistbox left edge of screen">Left</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string comment="@item:inlistbox right edge of screen">Right</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string comment="@item:inlistbox bottom edge of screen">Bottom</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="KContextualHelpButton" name="edgeContextualWarning">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="data-warning"/>
|
||||||
|
</property>
|
||||||
|
<property name="contextualHelpText">
|
||||||
|
<string comment="@info:tooltip">Another device already reserved this edge. This may lead to unexpected results when both are connected at the same time.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>KContextualHelpButton</class>
|
||||||
|
<extends>QToolButton</extends>
|
||||||
|
<header>kcontextualhelpbutton.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
145
plugins/shareinputdevices/shareinputdevicesplugin.cpp
Normal file
145
plugins/shareinputdevices/shareinputdevicesplugin.cpp
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
#include "shareinputdevicesplugin.h"
|
||||||
|
|
||||||
|
#include "inputcapturesession.h"
|
||||||
|
#include "plugin_shareinputdevices_debug.h"
|
||||||
|
|
||||||
|
#include <core/daemon.h>
|
||||||
|
#include <core/device.h>
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QPointF>
|
||||||
|
|
||||||
|
#include <linux/input-event-codes.h>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
#define PACKET_TYPE_MOUSEPAD_REQUEST u"kdeconnect.mousepad.request"_s
|
||||||
|
#define PACKET_TYPE_SHAREINPUTDEVICES u"kdeconnect.shareinputdevices"_s
|
||||||
|
#define PACKET_TYPE_SHAREINPUTDEVICES_REQUEST u"kdeconnect.shareinputdevices.request"_s
|
||||||
|
|
||||||
|
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},
|
||||||
|
};
|
||||||
|
|
||||||
|
K_PLUGIN_CLASS_WITH_JSON(ShareInputDevicesPlugin, "kdeconnect_shareinputdevices.json")
|
||||||
|
|
||||||
|
ShareInputDevicesPlugin::ShareInputDevicesPlugin(QObject *parent, const QVariantList &args)
|
||||||
|
: KdeConnectPlugin(parent, args)
|
||||||
|
, m_inputCaptureSession(new InputCaptureSession(this))
|
||||||
|
{
|
||||||
|
connect(m_inputCaptureSession, &InputCaptureSession::started, this, [this](const QLine &barrier, const QPointF &delta) {
|
||||||
|
m_activatedBarrier = barrier;
|
||||||
|
NetworkPacket packet(PACKET_TYPE_SHAREINPUTDEVICES_REQUEST, {{u"startEdge"_s, configuredEdge()}, {u"deltax"_s, delta.x()}, {u"deltay"_s, delta.y()}});
|
||||||
|
sendPacket(packet);
|
||||||
|
});
|
||||||
|
connect(m_inputCaptureSession, &InputCaptureSession::mouseMove, this, [this](double x, double y) {
|
||||||
|
NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"dx"_s, x}, {u"dy"_s, y}});
|
||||||
|
sendPacket(packet);
|
||||||
|
});
|
||||||
|
connect(m_inputCaptureSession, &InputCaptureSession::mouseButton, this, [this](int button, bool pressed) {
|
||||||
|
NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||||
|
// mousepad only supports separate press and release for left click currently, so send event on release even though not entirely correct
|
||||||
|
if (button == BTN_LEFT) {
|
||||||
|
packet.set(pressed ? u"singlehold"_s : u"singlerelease"_s, true);
|
||||||
|
} else if (button == BTN_RIGHT && pressed) {
|
||||||
|
packet.set(u"rightclick"_s, true);
|
||||||
|
} else if (button == BTN_MIDDLE) {
|
||||||
|
packet.set(u"middleclick"_s, true);
|
||||||
|
}
|
||||||
|
sendPacket(packet);
|
||||||
|
});
|
||||||
|
connect(m_inputCaptureSession, &InputCaptureSession::scrollDelta, this, [this](double x, double y) {
|
||||||
|
qDebug() << "scrollDelta" << x << y;
|
||||||
|
// scrollDirection in kdeconnect is inverted compared to what we get here
|
||||||
|
NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"scroll"_s, true}, {u"dx"_s, x}, {u"dy"_s, y}});
|
||||||
|
sendPacket(packet);
|
||||||
|
});
|
||||||
|
connect(m_inputCaptureSession, &InputCaptureSession::scrollDiscrete, this, [this](int x, int y) {
|
||||||
|
qDebug() << "scolldiscrete" << x << y;
|
||||||
|
constexpr auto anglePer120Step = 15 / 120.0;
|
||||||
|
NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"scroll"_s, true}, {u"dx"_s, x * anglePer120Step}, {u"dy"_s, -y * anglePer120Step}});
|
||||||
|
sendPacket(packet);
|
||||||
|
});
|
||||||
|
connect(m_inputCaptureSession, &InputCaptureSession::key, this, [this](Qt::Key key, Qt::KeyboardModifiers modifiers, const QString &text) {
|
||||||
|
qDebug() << "sending key" << text << modifiers << specialKeysMap.value(key);
|
||||||
|
NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST,
|
||||||
|
{
|
||||||
|
{u"key"_s, text},
|
||||||
|
{u"specialKey"_s, specialKeysMap.value(key)},
|
||||||
|
{u"shift"_s, static_cast<bool>(modifiers & Qt::ShiftModifier)},
|
||||||
|
{u"ctrl"_s, static_cast<bool>(modifiers & Qt::ControlModifier)},
|
||||||
|
{u"alt"_s, static_cast<bool>(modifiers & Qt::AltModifier)},
|
||||||
|
{u"super"_s, static_cast<bool>(modifiers & Qt::MetaModifier)},
|
||||||
|
});
|
||||||
|
sendPacket(packet);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_inputCaptureSession->setBarrierEdge(configuredEdge());
|
||||||
|
connect(config(), &KdeConnectPluginConfig::configChanged, this, [this] {
|
||||||
|
m_inputCaptureSession->setBarrierEdge(configuredEdge());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::Edge ShareInputDevicesPlugin::configuredEdge() const
|
||||||
|
{
|
||||||
|
return static_cast<Qt::Edge>(config()->getInt(u"edge"_s, Qt::LeftEdge));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesPlugin::receivePacket(const NetworkPacket &np)
|
||||||
|
{
|
||||||
|
if (np.type() == PACKET_TYPE_SHAREINPUTDEVICES) {
|
||||||
|
if (np.has(u"releaseDeltax"_s)) {
|
||||||
|
const QPointF releaseDelta{np.get<double>(u"releaseDeltax"_s), np.get<double>(u"releaseDeltay"_s)};
|
||||||
|
const QPointF releasePosition = m_activatedBarrier.p1() + releaseDelta;
|
||||||
|
m_inputCaptureSession->release(releasePosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ShareInputDevicesPlugin::dbusPath() const
|
||||||
|
{
|
||||||
|
return QLatin1String("/modules/kdeconnect/devices/%1/shareinputdevices").arg(device()->id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "shareinputdevicesplugin.moc"
|
31
plugins/shareinputdevices/shareinputdevicesplugin.h
Normal file
31
plugins/shareinputdevices/shareinputdevicesplugin.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLine>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointF>
|
||||||
|
|
||||||
|
#include <core/kdeconnectplugin.h>
|
||||||
|
|
||||||
|
class InputCaptureSession;
|
||||||
|
class QScreen;
|
||||||
|
|
||||||
|
class ShareInputDevicesPlugin : public KdeConnectPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
ShareInputDevicesPlugin(QObject *parent, const QVariantList &args);
|
||||||
|
|
||||||
|
void receivePacket(const NetworkPacket &np) override;
|
||||||
|
QString dbusPath() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Qt::Edge configuredEdge() const;
|
||||||
|
InputCaptureSession *m_inputCaptureSession;
|
||||||
|
QLine m_activatedBarrier;
|
||||||
|
};
|
7
plugins/shareinputdevicesremote/CMakeLists.txt
Normal file
7
plugins/shareinputdevicesremote/CMakeLists.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
kdeconnect_add_plugin(kdeconnect_shareinputdevicesremote SOURCES shareinputdevicesremoteplugin.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(kdeconnect_shareinputdevicesremote
|
||||||
|
kdeconnectcore
|
||||||
|
Qt::Gui
|
||||||
|
KF6::I18n
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "David Redondo",
|
||||||
|
"Email": "kde@david-redondo.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"EnabledByDefault": true,
|
||||||
|
"Icon": "dialog-input-devices",
|
||||||
|
"License": "GPL",
|
||||||
|
"Hidden": true
|
||||||
|
|
||||||
|
},
|
||||||
|
"X-KdeConnect-OutgoingPacketType": [
|
||||||
|
"kdeconnect.shareinputdevices"
|
||||||
|
],
|
||||||
|
"X-KdeConnect-SupportedPacketType": [
|
||||||
|
"kdeconnect.mousepad.request",
|
||||||
|
"kdeconnect.shareinputdevices.request"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
#include "shareinputdevicesremoteplugin.h"
|
||||||
|
|
||||||
|
#include <core/daemon.h>
|
||||||
|
#include <core/device.h>
|
||||||
|
|
||||||
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QScreen>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
#define PACKET_TYPE_MOUSEPAD_REQUEST u"kdeconnect.mousepad.request"_s
|
||||||
|
#define PACKET_TYPE_SHAREINPUTDEVICES u"kdeconnect.shareinputdevices"_s
|
||||||
|
#define PACKET_TYPE_SHAREINPUTDEVICES_REQUEST u"kdeconnect.shareinputdevices.request"_s
|
||||||
|
|
||||||
|
K_PLUGIN_CLASS_WITH_JSON(ShareInputDevicesRemotePlugin, "kdeconnect_shareinputdevicesremote.json")
|
||||||
|
|
||||||
|
ShareInputDevicesRemotePlugin::ShareInputDevicesRemotePlugin(QObject *parent, const QVariantList &args)
|
||||||
|
: KdeConnectPlugin(parent, args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareInputDevicesRemotePlugin::receivePacket(const NetworkPacket &np)
|
||||||
|
{
|
||||||
|
if (np.type() == PACKET_TYPE_SHAREINPUTDEVICES_REQUEST) {
|
||||||
|
if (np.has(u"startEdge"_s)) {
|
||||||
|
const Qt::Edge startEdge = np.get<Qt::Edge>(u"startEdge"_s);
|
||||||
|
const QPointF delta = {np.get<double>(u"deltax"_s), np.get<double>(u"deltay"_s)};
|
||||||
|
auto screens = QGuiApplication::screens();
|
||||||
|
auto mousePadPlugin = device()->plugin(u"kdeconnect_mousepad"_s);
|
||||||
|
if (screens.empty() || !mousePadPlugin) {
|
||||||
|
NetworkPacket packet(PACKET_TYPE_SHAREINPUTDEVICES, {{u"releaseDelta"_s, QPointF(0, 0)}});
|
||||||
|
sendPacket(packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// This is opposite to the sorting in inputcaptureession because here the opposite edge is wanted.
|
||||||
|
// For example if on the source side a left barrier was crossed, this is one is entered from the right.
|
||||||
|
if (startEdge == Qt::LeftEdge) {
|
||||||
|
std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) {
|
||||||
|
return lhs->geometry().x() + lhs->geometry().width() > rhs->geometry().x() + rhs->geometry().width();
|
||||||
|
});
|
||||||
|
m_enterEdge = Qt::RightEdge;
|
||||||
|
m_currentPosition = screens.front()->geometry().topRight() + delta;
|
||||||
|
} else if (startEdge == Qt::RightEdge) {
|
||||||
|
std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) {
|
||||||
|
return lhs->geometry().x() < rhs->geometry().x();
|
||||||
|
});
|
||||||
|
m_enterEdge = Qt::LeftEdge;
|
||||||
|
m_currentPosition = screens.front()->geometry().topLeft() + delta;
|
||||||
|
} else if (startEdge == Qt::TopEdge) {
|
||||||
|
std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) {
|
||||||
|
return lhs->geometry().y() + lhs->geometry().height() > rhs->geometry().y() + rhs->geometry().height();
|
||||||
|
});
|
||||||
|
m_enterEdge = Qt::BottomEdge;
|
||||||
|
m_currentPosition = screens.front()->geometry().bottomLeft() + delta;
|
||||||
|
} else {
|
||||||
|
std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) {
|
||||||
|
return lhs->geometry().y() < rhs->geometry().y();
|
||||||
|
});
|
||||||
|
m_enterEdge = Qt::TopEdge;
|
||||||
|
m_currentPosition = screens.front()->geometry().topLeft() + delta;
|
||||||
|
}
|
||||||
|
m_enterScreen = screens.front();
|
||||||
|
// Send a packet from this local device to the mousepad plugin because the position could only
|
||||||
|
// be calculated here
|
||||||
|
NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"x"_s, m_currentPosition.x()}, {u"y"_s, m_currentPosition.y()}});
|
||||||
|
mousePadPlugin->receivePacket(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (np.type() == PACKET_TYPE_MOUSEPAD_REQUEST) {
|
||||||
|
if (np.has(u"dx"_s) && m_enterScreen) {
|
||||||
|
m_currentPosition += QPointF(np.get<double>(u"dx"_s), np.get<double>(u"dy"_s));
|
||||||
|
auto sendRelease = [this](const QPointF &delta) {
|
||||||
|
NetworkPacket packet(PACKET_TYPE_SHAREINPUTDEVICES, {{u"releaseDeltax"_s, delta.x()}, {u"releaseDeltay"_s, delta.y()}});
|
||||||
|
sendPacket(packet);
|
||||||
|
};
|
||||||
|
if (m_enterEdge == Qt::LeftEdge && m_currentPosition.x() < m_enterScreen->geometry().x()) {
|
||||||
|
const QPointF delta = m_currentPosition - m_enterScreen->geometry().topLeft();
|
||||||
|
sendRelease(delta);
|
||||||
|
} else if (m_enterEdge == Qt::RightEdge && m_currentPosition.x() > m_enterScreen->geometry().x() + m_enterScreen->geometry().width()) {
|
||||||
|
const QPointF delta = m_currentPosition - m_enterScreen->geometry().topRight();
|
||||||
|
sendRelease(delta);
|
||||||
|
} else if (m_enterEdge == Qt::TopEdge && m_currentPosition.y() < m_enterScreen->geometry().y()) {
|
||||||
|
const QPointF delta = m_currentPosition - m_enterScreen->geometry().topLeft();
|
||||||
|
sendRelease(delta);
|
||||||
|
} else if (m_enterEdge == Qt::BottomEdge && m_currentPosition.y() > m_enterScreen->geometry().y() + m_enterScreen->geometry().height()) {
|
||||||
|
const QPointF delta = m_currentPosition - m_enterScreen->geometry().bottomLeft();
|
||||||
|
sendRelease(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ShareInputDevicesRemotePlugin::dbusPath() const
|
||||||
|
{
|
||||||
|
return QLatin1String("/modules/kdeconnect/devices/%1/shareinputdevicesremote").arg(device()->id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "shareinputdevicesremoteplugin.moc"
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLine>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointF>
|
||||||
|
|
||||||
|
#include <core/kdeconnectplugin.h>
|
||||||
|
|
||||||
|
class InputCaptureSession;
|
||||||
|
class QScreen;
|
||||||
|
|
||||||
|
class ShareInputDevicesRemotePlugin : public KdeConnectPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
ShareInputDevicesRemotePlugin(QObject *parent, const QVariantList &args);
|
||||||
|
|
||||||
|
void receivePacket(const NetworkPacket &np) override;
|
||||||
|
QString dbusPath() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScreen *m_enterScreen;
|
||||||
|
Qt::Edge m_enterEdge;
|
||||||
|
QPointF m_enterPosition;
|
||||||
|
QPointF m_currentPosition;
|
||||||
|
};
|
Loading…
Reference in a new issue