382 lines
10 KiB
JavaScript
382 lines
10 KiB
JavaScript
|
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||
|
//
|
||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
||
|
import Gdk from 'gi://Gdk';
|
||
|
import GObject from 'gi://GObject';
|
||
|
|
||
|
import * as Components from '../components/index.js';
|
||
|
import {InputDialog} from '../ui/mousepad.js';
|
||
|
import Plugin from '../plugin.js';
|
||
|
|
||
|
|
||
|
export const Metadata = {
|
||
|
label: _('Mousepad'),
|
||
|
description: _('Enables the paired device to act as a remote mouse and keyboard'),
|
||
|
id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Mousepad',
|
||
|
incomingCapabilities: [
|
||
|
'kdeconnect.mousepad.echo',
|
||
|
'kdeconnect.mousepad.request',
|
||
|
'kdeconnect.mousepad.keyboardstate',
|
||
|
],
|
||
|
outgoingCapabilities: [
|
||
|
'kdeconnect.mousepad.echo',
|
||
|
'kdeconnect.mousepad.request',
|
||
|
'kdeconnect.mousepad.keyboardstate',
|
||
|
],
|
||
|
actions: {
|
||
|
keyboard: {
|
||
|
label: _('Remote Input'),
|
||
|
icon_name: 'input-keyboard-symbolic',
|
||
|
|
||
|
parameter_type: null,
|
||
|
incoming: [
|
||
|
'kdeconnect.mousepad.echo',
|
||
|
'kdeconnect.mousepad.keyboardstate',
|
||
|
],
|
||
|
outgoing: ['kdeconnect.mousepad.request'],
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A map of "KDE Connect" keyvals to Gdk
|
||
|
*/
|
||
|
const KeyMap = new Map([
|
||
|
[1, Gdk.KEY_BackSpace],
|
||
|
[2, Gdk.KEY_Tab],
|
||
|
[3, Gdk.KEY_Linefeed],
|
||
|
[4, Gdk.KEY_Left],
|
||
|
[5, Gdk.KEY_Up],
|
||
|
[6, Gdk.KEY_Right],
|
||
|
[7, Gdk.KEY_Down],
|
||
|
[8, Gdk.KEY_Page_Up],
|
||
|
[9, Gdk.KEY_Page_Down],
|
||
|
[10, Gdk.KEY_Home],
|
||
|
[11, Gdk.KEY_End],
|
||
|
[12, Gdk.KEY_Return],
|
||
|
[13, Gdk.KEY_Delete],
|
||
|
[14, Gdk.KEY_Escape],
|
||
|
[15, Gdk.KEY_Sys_Req],
|
||
|
[16, Gdk.KEY_Scroll_Lock],
|
||
|
[17, 0],
|
||
|
[18, 0],
|
||
|
[19, 0],
|
||
|
[20, 0],
|
||
|
[21, Gdk.KEY_F1],
|
||
|
[22, Gdk.KEY_F2],
|
||
|
[23, Gdk.KEY_F3],
|
||
|
[24, Gdk.KEY_F4],
|
||
|
[25, Gdk.KEY_F5],
|
||
|
[26, Gdk.KEY_F6],
|
||
|
[27, Gdk.KEY_F7],
|
||
|
[28, Gdk.KEY_F8],
|
||
|
[29, Gdk.KEY_F9],
|
||
|
[30, Gdk.KEY_F10],
|
||
|
[31, Gdk.KEY_F11],
|
||
|
[32, Gdk.KEY_F12],
|
||
|
]);
|
||
|
|
||
|
const KeyMapCodes = new Map([
|
||
|
[1, 14],
|
||
|
[2, 15],
|
||
|
[3, 101],
|
||
|
[4, 105],
|
||
|
[5, 103],
|
||
|
[6, 106],
|
||
|
[7, 108],
|
||
|
[8, 104],
|
||
|
[9, 109],
|
||
|
[10, 102],
|
||
|
[11, 107],
|
||
|
[12, 28],
|
||
|
[13, 111],
|
||
|
[14, 1],
|
||
|
[15, 99],
|
||
|
[16, 70],
|
||
|
[17, 0],
|
||
|
[18, 0],
|
||
|
[19, 0],
|
||
|
[20, 0],
|
||
|
[21, 59],
|
||
|
[22, 60],
|
||
|
[23, 61],
|
||
|
[24, 62],
|
||
|
[25, 63],
|
||
|
[26, 64],
|
||
|
[27, 65],
|
||
|
[28, 66],
|
||
|
[29, 67],
|
||
|
[30, 68],
|
||
|
[31, 87],
|
||
|
[32, 88],
|
||
|
]);
|
||
|
|
||
|
/**
|
||
|
* Mousepad Plugin
|
||
|
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mousepad
|
||
|
*
|
||
|
* TODO: support outgoing mouse events?
|
||
|
*/
|
||
|
const MousepadPlugin = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectMousepadPlugin',
|
||
|
Properties: {
|
||
|
'state': GObject.ParamSpec.boolean(
|
||
|
'state',
|
||
|
'State',
|
||
|
'Remote keyboard state',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
false
|
||
|
),
|
||
|
},
|
||
|
}, class MousepadPlugin extends Plugin {
|
||
|
_init(device) {
|
||
|
super._init(device, 'mousepad');
|
||
|
|
||
|
if (!globalThis.HAVE_GNOME)
|
||
|
this._input = Components.acquire('ydotool');
|
||
|
else
|
||
|
this._input = Components.acquire('input');
|
||
|
|
||
|
this._shareControlChangedId = this.settings.connect(
|
||
|
'changed::share-control',
|
||
|
this._sendState.bind(this)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
get state() {
|
||
|
if (this._state === undefined)
|
||
|
this._state = false;
|
||
|
|
||
|
return this._state;
|
||
|
}
|
||
|
|
||
|
connected() {
|
||
|
super.connected();
|
||
|
|
||
|
this._sendState();
|
||
|
}
|
||
|
|
||
|
disconnected() {
|
||
|
super.disconnected();
|
||
|
|
||
|
this._state = false;
|
||
|
this.notify('state');
|
||
|
}
|
||
|
|
||
|
handlePacket(packet) {
|
||
|
switch (packet.type) {
|
||
|
case 'kdeconnect.mousepad.request':
|
||
|
this._handleInput(packet.body);
|
||
|
break;
|
||
|
|
||
|
case 'kdeconnect.mousepad.echo':
|
||
|
this._handleEcho(packet.body);
|
||
|
break;
|
||
|
|
||
|
case 'kdeconnect.mousepad.keyboardstate':
|
||
|
this._handleState(packet);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle a input event.
|
||
|
*
|
||
|
* @param {Object} input - The body of a `kdeconnect.mousepad.request`
|
||
|
*/
|
||
|
_handleInput(input) {
|
||
|
if (!this.settings.get_boolean('share-control'))
|
||
|
return;
|
||
|
|
||
|
let keysym;
|
||
|
let modifiers = 0;
|
||
|
const modifiers_codes = [];
|
||
|
|
||
|
// These are ordered, as much as possible, to create the shortest code
|
||
|
// path for high-frequency, low-latency events (eg. mouse movement)
|
||
|
switch (true) {
|
||
|
case input.hasOwnProperty('scroll'):
|
||
|
this._input.scrollPointer(input.dx, input.dy);
|
||
|
break;
|
||
|
|
||
|
case (input.hasOwnProperty('dx') && input.hasOwnProperty('dy')):
|
||
|
this._input.movePointer(input.dx, input.dy);
|
||
|
break;
|
||
|
|
||
|
case (input.hasOwnProperty('key') || input.hasOwnProperty('specialKey')):
|
||
|
// NOTE: \u0000 sometimes sent in advance of a specialKey packet
|
||
|
if (input.key && input.key === '\u0000')
|
||
|
return;
|
||
|
|
||
|
// Modifiers
|
||
|
if (input.alt) {
|
||
|
modifiers |= Gdk.ModifierType.MOD1_MASK;
|
||
|
modifiers_codes.push(56);
|
||
|
}
|
||
|
|
||
|
if (input.ctrl) {
|
||
|
modifiers |= Gdk.ModifierType.CONTROL_MASK;
|
||
|
modifiers_codes.push(29);
|
||
|
}
|
||
|
|
||
|
if (input.shift) {
|
||
|
modifiers |= Gdk.ModifierType.SHIFT_MASK;
|
||
|
modifiers_codes.push(42);
|
||
|
}
|
||
|
|
||
|
if (input.super) {
|
||
|
modifiers |= Gdk.ModifierType.SUPER_MASK;
|
||
|
modifiers_codes.push(125);
|
||
|
}
|
||
|
|
||
|
// Regular key (printable ASCII or Unicode)
|
||
|
if (input.key) {
|
||
|
if (!globalThis.HAVE_GNOME)
|
||
|
this._input.pressKeys(input.key, modifiers_codes);
|
||
|
else
|
||
|
this._input.pressKeys(input.key, modifiers);
|
||
|
|
||
|
this._sendEcho(input);
|
||
|
|
||
|
// Special key (eg. non-printable ASCII)
|
||
|
} else if (input.specialKey && KeyMap.has(input.specialKey)) {
|
||
|
if (!globalThis.HAVE_GNOME) {
|
||
|
keysym = KeyMapCodes.get(input.specialKey);
|
||
|
this._input.pressKeys(keysym, modifiers_codes);
|
||
|
} else {
|
||
|
keysym = KeyMap.get(input.specialKey);
|
||
|
this._input.pressKeys(keysym, modifiers);
|
||
|
}
|
||
|
|
||
|
this._sendEcho(input);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case input.hasOwnProperty('singleclick'):
|
||
|
this._input.clickPointer(Gdk.BUTTON_PRIMARY);
|
||
|
break;
|
||
|
|
||
|
case input.hasOwnProperty('doubleclick'):
|
||
|
this._input.doubleclickPointer(Gdk.BUTTON_PRIMARY);
|
||
|
break;
|
||
|
|
||
|
case input.hasOwnProperty('middleclick'):
|
||
|
this._input.clickPointer(Gdk.BUTTON_MIDDLE);
|
||
|
break;
|
||
|
|
||
|
case input.hasOwnProperty('rightclick'):
|
||
|
this._input.clickPointer(Gdk.BUTTON_SECONDARY);
|
||
|
break;
|
||
|
|
||
|
case input.hasOwnProperty('singlehold'):
|
||
|
this._input.pressPointer(Gdk.BUTTON_PRIMARY);
|
||
|
break;
|
||
|
|
||
|
case input.hasOwnProperty('singlerelease'):
|
||
|
this._input.releasePointer(Gdk.BUTTON_PRIMARY);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
logError(new Error('Unknown input'));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an echo/ACK of a event we sent, displaying it the dialog entry.
|
||
|
*
|
||
|
* @param {Object} input - The body of a `kdeconnect.mousepad.echo`
|
||
|
*/
|
||
|
_handleEcho(input) {
|
||
|
if (!this._dialog || !this._dialog.visible)
|
||
|
return;
|
||
|
|
||
|
// Skip modifiers
|
||
|
if (input.alt || input.ctrl || input.super)
|
||
|
return;
|
||
|
|
||
|
if (input.key) {
|
||
|
this._dialog._isAck = true;
|
||
|
this._dialog.entry.buffer.text += input.key;
|
||
|
this._dialog._isAck = false;
|
||
|
} else if (KeyMap.get(input.specialKey) === Gdk.KEY_BackSpace) {
|
||
|
this._dialog.entry.emit('backspace');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle a state change from the remote keyboard. This is an indication
|
||
|
* that the remote keyboard is ready to accept input.
|
||
|
*
|
||
|
* @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet
|
||
|
*/
|
||
|
_handleState(packet) {
|
||
|
this._state = !!packet.body.state;
|
||
|
this.notify('state');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send an echo/ACK of @input, if requested
|
||
|
*
|
||
|
* @param {Object} input - The body of a 'kdeconnect.mousepad.request'
|
||
|
*/
|
||
|
_sendEcho(input) {
|
||
|
if (!input.sendAck)
|
||
|
return;
|
||
|
|
||
|
delete input.sendAck;
|
||
|
input.isAck = true;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.echo',
|
||
|
body: input,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send the local keyboard state
|
||
|
*
|
||
|
* @param {boolean} state - Whether we're ready to accept input
|
||
|
*/
|
||
|
_sendState() {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.keyboardstate',
|
||
|
body: {
|
||
|
state: this.settings.get_boolean('share-control'),
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Open the Keyboard Input dialog
|
||
|
*/
|
||
|
keyboard() {
|
||
|
if (this._dialog === undefined) {
|
||
|
this._dialog = new InputDialog({
|
||
|
device: this.device,
|
||
|
plugin: this,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this._dialog.present();
|
||
|
}
|
||
|
|
||
|
destroy() {
|
||
|
if (this._input !== undefined) {
|
||
|
if (!globalThis.HAVE_GNOME)
|
||
|
this._input = Components.release('ydotool');
|
||
|
else
|
||
|
this._input = Components.release('input');
|
||
|
}
|
||
|
|
||
|
if (this._dialog !== undefined)
|
||
|
this._dialog.destroy();
|
||
|
|
||
|
this.settings.disconnect(this._shareControlChangedId);
|
||
|
|
||
|
super.destroy();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
export default MousepadPlugin;
|