632 lines
18 KiB
JavaScript
632 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
const Atspi = imports.gi.Atspi;
|
|
const Gdk = imports.gi.Gdk;
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const GObject = imports.gi.GObject;
|
|
const Gtk = imports.gi.Gtk;
|
|
|
|
const PluginsBase = imports.service.plugins.base;
|
|
|
|
|
|
var Metadata = {
|
|
label: _('Mousepad'),
|
|
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: _('Keyboard'),
|
|
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]
|
|
]);
|
|
|
|
|
|
/**
|
|
* Mousepad Plugin
|
|
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mousepad
|
|
*
|
|
* TODO: support outgoing mouse events?
|
|
*/
|
|
var Plugin = GObject.registerClass({
|
|
GTypeName: 'GSConnectMousepadPlugin',
|
|
Properties: {
|
|
'state': GObject.ParamSpec.boolean(
|
|
'state',
|
|
'State',
|
|
'Remote keyboard state',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'share-control': GObject.ParamSpec.boolean(
|
|
'share-control',
|
|
'Share Control',
|
|
'Share control of mouse & keyboard',
|
|
GObject.ParamFlags.READWRITE,
|
|
false
|
|
)
|
|
}
|
|
}, class Plugin extends PluginsBase.Plugin {
|
|
|
|
_init(device) {
|
|
super._init(device, 'mousepad');
|
|
|
|
this._input = this.service.components.get('input');
|
|
|
|
this.settings.bind(
|
|
'share-control',
|
|
this,
|
|
'share-control',
|
|
Gio.SettingsBindFlags.GET
|
|
);
|
|
|
|
this._stateId = 0;
|
|
}
|
|
|
|
connected() {
|
|
super.connected();
|
|
|
|
this.sendState();
|
|
}
|
|
|
|
disconnected() {
|
|
super.disconnected();
|
|
|
|
// Set the keyboard state to inactive
|
|
this._state = false;
|
|
this._stateId = 0;
|
|
this.notify('state');
|
|
}
|
|
|
|
get state() {
|
|
if (this._state === undefined) {
|
|
this._state = false;
|
|
}
|
|
|
|
return this._state;
|
|
}
|
|
|
|
get virtual_keyboard() {
|
|
return this._virtual_keyboard;
|
|
}
|
|
|
|
handlePacket(packet) {
|
|
switch (packet.type) {
|
|
case 'kdeconnect.mousepad.request':
|
|
if (this.share_control) {
|
|
this._handleInput(packet.body);
|
|
}
|
|
break;
|
|
|
|
case 'kdeconnect.mousepad.echo':
|
|
this._handleEcho(packet.body);
|
|
break;
|
|
|
|
case 'kdeconnect.mousepad.keyboardstate':
|
|
this._handleState(packet);
|
|
break;
|
|
}
|
|
}
|
|
|
|
_handleInput(input) {
|
|
let keysym;
|
|
let modifiers = 0;
|
|
|
|
// 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 || input.ctrl || input.shift || input.super) {
|
|
if (input.alt) modifiers |= Gdk.ModifierType.MOD1_MASK;
|
|
if (input.ctrl) modifiers |= Gdk.ModifierType.CONTROL_MASK;
|
|
if (input.shift) modifiers |= Gdk.ModifierType.SHIFT_MASK;
|
|
if (input.super) modifiers |= Gdk.ModifierType.SUPER_MASK;
|
|
}
|
|
|
|
// Regular key (printable ASCII or Unicode)
|
|
if (input.key) {
|
|
this._input.pressKey(input.key, modifiers);
|
|
this.sendEcho(input);
|
|
|
|
// Special key (eg. non-printable ASCII)
|
|
} else if (input.specialKey && KeyMap.has(input.specialKey)) {
|
|
keysym = KeyMap.get(input.specialKey);
|
|
this._input.pressKey(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'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send an echo/ACK of @input, if requested
|
|
*
|
|
* @param {object} input - 'body' of a 'kdeconnect.mousepad.request' packet
|
|
*/
|
|
sendEcho(input) {
|
|
if (input.sendAck) {
|
|
delete input.sendAck;
|
|
input.isAck = true;
|
|
|
|
this.device.sendPacket({
|
|
type: 'kdeconnect.mousepad.echo',
|
|
body: input
|
|
});
|
|
}
|
|
}
|
|
|
|
_handleEcho(input) {
|
|
if (!this._dialog || !this._dialog.visible) {
|
|
return;
|
|
}
|
|
|
|
if (input.alt || input.ctrl || input.super) {
|
|
return;
|
|
}
|
|
|
|
if (input.key) {
|
|
this._dialog.text.buffer.text += input.key;
|
|
} else if (KeyMap.get(input.specialKey) === Gdk.KEY_BackSpace) {
|
|
this._dialog.text.emit('backspace');
|
|
}
|
|
}
|
|
|
|
_handleState(packet) {
|
|
// FIXME: ensure we don't get packets out of order
|
|
if (packet.id > this._stateId) {
|
|
this._state = packet.body.state;
|
|
this._stateId = packet.id;
|
|
this.notify('state');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.share_control
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Open the Keyboard Input dialog
|
|
*/
|
|
keyboard() {
|
|
if (!this._dialog) {
|
|
this._dialog = new KeyboardInputDialog({
|
|
device: this.device,
|
|
plugin: this
|
|
});
|
|
}
|
|
|
|
this._dialog.present();
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* A map of Gdk to "KDE Connect" keyvals
|
|
*/
|
|
const ReverseKeyMap = new Map([
|
|
[Gdk.KEY_BackSpace, 1],
|
|
[Gdk.KEY_Tab, 2],
|
|
[Gdk.KEY_Linefeed, 3],
|
|
[Gdk.KEY_Left, 4],
|
|
[Gdk.KEY_Up, 5],
|
|
[Gdk.KEY_Right, 6],
|
|
[Gdk.KEY_Down, 7],
|
|
[Gdk.KEY_Page_Up, 8],
|
|
[Gdk.KEY_Page_Down, 9],
|
|
[Gdk.KEY_Home, 10],
|
|
[Gdk.KEY_End, 11],
|
|
[Gdk.KEY_Return, 12],
|
|
[Gdk.KEY_Delete, 13],
|
|
[Gdk.KEY_Escape, 14],
|
|
[Gdk.KEY_Sys_Req, 15],
|
|
[Gdk.KEY_Scroll_Lock, 16],
|
|
[Gdk.KEY_F1, 21],
|
|
[Gdk.KEY_F2, 22],
|
|
[Gdk.KEY_F3, 23],
|
|
[Gdk.KEY_F4, 24],
|
|
[Gdk.KEY_F5, 25],
|
|
[Gdk.KEY_F6, 26],
|
|
[Gdk.KEY_F7, 27],
|
|
[Gdk.KEY_F8, 28],
|
|
[Gdk.KEY_F9, 29],
|
|
[Gdk.KEY_F10, 30],
|
|
[Gdk.KEY_F11, 31],
|
|
[Gdk.KEY_F12, 32]
|
|
]);
|
|
|
|
|
|
/**
|
|
* A list of keyvals we consider modifiers
|
|
*/
|
|
const MOD_KEYS = [
|
|
Gdk.KEY_Alt_L,
|
|
Gdk.KEY_Alt_R,
|
|
Gdk.KEY_Caps_Lock,
|
|
Gdk.KEY_Control_L,
|
|
Gdk.KEY_Control_R,
|
|
Gdk.KEY_Meta_L,
|
|
Gdk.KEY_Meta_R,
|
|
Gdk.KEY_Num_Lock,
|
|
Gdk.KEY_Shift_L,
|
|
Gdk.KEY_Shift_R,
|
|
Gdk.KEY_Super_L,
|
|
Gdk.KEY_Super_R
|
|
];
|
|
|
|
|
|
/**
|
|
* Some convenience functions for checking keyvals for modifiers
|
|
*/
|
|
const isAlt = (key) => [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R].includes(key);
|
|
const isCtrl = (key) => [Gdk.KEY_Control_L, Gdk.KEY_Control_R].includes(key);
|
|
const isShift = (key) => [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R].includes(key);
|
|
const isSuper = (key) => [Gdk.KEY_Super_L, Gdk.KEY_Super_R].includes(key);
|
|
|
|
|
|
var KeyboardInputDialog = GObject.registerClass({
|
|
GTypeName: 'GSConnectMousepadKeyboardInputDialog',
|
|
Properties: {
|
|
'device': GObject.ParamSpec.object(
|
|
'device',
|
|
'Device',
|
|
'The device associated with this window',
|
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
|
GObject.Object
|
|
),
|
|
'plugin': GObject.ParamSpec.object(
|
|
'plugin',
|
|
'Plugin',
|
|
'The mousepad plugin associated with this window',
|
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
|
GObject.Object
|
|
)
|
|
}
|
|
}, class KeyboardInputDialog extends Gtk.Dialog {
|
|
|
|
_init(params) {
|
|
super._init(Object.assign({
|
|
use_header_bar: true,
|
|
default_width: 480,
|
|
window_position: Gtk.WindowPosition.CENTER
|
|
}, params));
|
|
|
|
let headerbar = this.get_titlebar();
|
|
headerbar.title = _('Keyboard');
|
|
headerbar.subtitle = this.device.name;
|
|
|
|
// Main Box
|
|
let content = this.get_content_area();
|
|
content.border_width = 0;
|
|
|
|
// Infobar
|
|
this.infobar = new Gtk.Revealer();
|
|
content.add(this.infobar);
|
|
|
|
let bar = new Gtk.InfoBar({message_type: Gtk.MessageType.WARNING});
|
|
this.infobar.add(bar);
|
|
|
|
let infoicon = new Gtk.Image({icon_name: 'dialog-warning-symbolic'});
|
|
bar.get_content_area().add(infoicon);
|
|
|
|
let infolabel = new Gtk.Label({
|
|
// TRANSLATORS: Displayed when the remote keyboard is not ready to accept input
|
|
label: _('Remote keyboard on %s is not active').format(this.device.name)
|
|
});
|
|
bar.get_content_area().add(infolabel);
|
|
|
|
let infolink = new Gtk.LinkButton({
|
|
label: _('Help'),
|
|
uri: 'https://github.com/andyholmes/gnome-shell-extension-gsconnect/wiki/Help#remote-keyboard-not-active'
|
|
});
|
|
bar.get_action_area().add(infolink);
|
|
|
|
// Content
|
|
let layout = new Gtk.Grid({
|
|
column_spacing: 6,
|
|
margin: 6
|
|
});
|
|
content.add(layout);
|
|
|
|
// Modifier Buttons
|
|
this.shift_label = new Gtk.ShortcutLabel({
|
|
accelerator: Gtk.accelerator_name(0, Gdk.ModifierType.SHIFT_MASK),
|
|
halign: Gtk.Align.END,
|
|
valign: Gtk.Align.START,
|
|
sensitive: false
|
|
});
|
|
layout.attach(this.shift_label, 0, 0, 1, 1);
|
|
|
|
this.ctrl_label = new Gtk.ShortcutLabel({
|
|
accelerator: Gtk.accelerator_name(0, Gdk.ModifierType.CONTROL_MASK),
|
|
halign: Gtk.Align.END,
|
|
valign: Gtk.Align.START,
|
|
sensitive: false
|
|
});
|
|
layout.attach(this.ctrl_label, 0, 1, 1, 1);
|
|
|
|
this.alt_label = new Gtk.ShortcutLabel({
|
|
accelerator: Gtk.accelerator_name(0, Gdk.ModifierType.MOD1_MASK),
|
|
halign: Gtk.Align.END,
|
|
valign: Gtk.Align.START,
|
|
sensitive: false
|
|
});
|
|
layout.attach(this.alt_label, 0, 2, 1, 1);
|
|
|
|
this.super_label = new Gtk.ShortcutLabel({
|
|
accelerator: Gtk.accelerator_name(0, Gdk.ModifierType.SUPER_MASK),
|
|
halign: Gtk.Align.END,
|
|
valign: Gtk.Align.START,
|
|
sensitive: false
|
|
});
|
|
layout.attach(this.super_label, 0, 3, 1, 1);
|
|
|
|
// Text Input
|
|
let scroll = new Gtk.ScrolledWindow({
|
|
hscrollbar_policy: Gtk.PolicyType.NEVER,
|
|
shadow_type: Gtk.ShadowType.IN
|
|
});
|
|
layout.attach(scroll, 1, 0, 1, 4);
|
|
|
|
this.text = new Gtk.TextView({
|
|
border_width: 6,
|
|
hexpand: true,
|
|
vexpand: true,
|
|
visible: true
|
|
});
|
|
scroll.add(this.text);
|
|
|
|
this.infobar.connect('notify::reveal-child', this._onState.bind(this));
|
|
this.plugin.bind_property('state', this.infobar, 'reveal-child', 6);
|
|
|
|
this.show_all();
|
|
}
|
|
|
|
vfunc_delete_event(event) {
|
|
this._ungrab();
|
|
return this.hide_on_delete();
|
|
}
|
|
|
|
vfunc_key_release_event(event) {
|
|
if (!this.plugin.state) {
|
|
return true;
|
|
}
|
|
|
|
let keyvalLower = Gdk.keyval_to_lower(event.keyval);
|
|
let realMask = event.state & Gtk.accelerator_get_default_mod_mask();
|
|
|
|
this.alt_label.sensitive = !isAlt(keyvalLower) && (realMask & Gdk.ModifierType.MOD1_MASK);
|
|
this.ctrl_label.sensitive = !isCtrl(keyvalLower) && (realMask & Gdk.ModifierType.CONTROL_MASK);
|
|
this.shift_label.sensitive = !isShift(keyvalLower) && (realMask & Gdk.ModifierType.SHIFT_MASK);
|
|
this.super_label.sensitive = !isSuper(keyvalLower) && (realMask & Gdk.ModifierType.SUPER_MASK);
|
|
|
|
return super.vfunc_key_release_event(event);
|
|
}
|
|
|
|
vfunc_key_press_event(event) {
|
|
if (!this.plugin.state) {
|
|
return true;
|
|
}
|
|
|
|
let keyvalLower = Gdk.keyval_to_lower(event.keyval);
|
|
let realMask = event.state & Gtk.accelerator_get_default_mod_mask();
|
|
|
|
this.alt_label.sensitive = isAlt(keyvalLower) || (realMask & Gdk.ModifierType.MOD1_MASK);
|
|
this.ctrl_label.sensitive = isCtrl(keyvalLower) || (realMask & Gdk.ModifierType.CONTROL_MASK);
|
|
this.shift_label.sensitive = isShift(keyvalLower) || (realMask & Gdk.ModifierType.SHIFT_MASK);
|
|
this.super_label.sensitive = isSuper(keyvalLower) || (realMask & Gdk.ModifierType.SUPER_MASK);
|
|
|
|
// Wait for a real key before sending
|
|
if (MOD_KEYS.includes(keyvalLower)) {
|
|
return false;
|
|
}
|
|
|
|
// Normalize Tab
|
|
if (keyvalLower === Gdk.KEY_ISO_Left_Tab) {
|
|
keyvalLower = Gdk.KEY_Tab;
|
|
}
|
|
|
|
// Put shift back if it changed the case of the key, not otherwise.
|
|
if (keyvalLower !== event.keyval) {
|
|
realMask |= Gdk.ModifierType.SHIFT_MASK;
|
|
}
|
|
|
|
// HACK: we don't want to use SysRq as a keybinding (but we do want
|
|
// Alt+Print), so we avoid translation from Alt+Print to SysRq
|
|
if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0) {
|
|
keyvalLower = Gdk.KEY_Print;
|
|
}
|
|
|
|
// CapsLock isn't supported as a keybinding modifier, so keep it from
|
|
// confusing us
|
|
realMask &= ~Gdk.ModifierType.LOCK_MASK;
|
|
|
|
if (keyvalLower !== 0) {
|
|
debug(`keyval: ${event.keyval}, mask: ${realMask}`);
|
|
|
|
let request = {
|
|
alt: !!(realMask & Gdk.ModifierType.MOD1_MASK),
|
|
ctrl: !!(realMask & Gdk.ModifierType.CONTROL_MASK),
|
|
shift: !!(realMask & Gdk.ModifierType.SHIFT_MASK),
|
|
super: !!(realMask & Gdk.ModifierType.SUPER_MASK),
|
|
sendAck: true
|
|
};
|
|
|
|
// specialKey
|
|
if (ReverseKeyMap.has(event.keyval)) {
|
|
request.specialKey = ReverseKeyMap.get(event.keyval);
|
|
|
|
// key
|
|
} else {
|
|
let codePoint = Gdk.keyval_to_unicode(event.keyval);
|
|
request.key = String.fromCodePoint(codePoint);
|
|
}
|
|
|
|
this.device.sendPacket({
|
|
type: 'kdeconnect.mousepad.request',
|
|
body: request
|
|
});
|
|
|
|
// Pass these key combinations rather than using the echo reply
|
|
if (request.alt || request.ctrl || request.super) {
|
|
return super.vfunc_key_press_event(event);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
vfunc_window_state_event(event) {
|
|
if (this.plugin.state && !!(event.new_window_state & Gdk.WindowState.FOCUSED)) {
|
|
this._grab();
|
|
} else {
|
|
this._ungrab();
|
|
}
|
|
|
|
return super.vfunc_window_state_event(event);
|
|
}
|
|
|
|
_onState(widget) {
|
|
if (this.plugin.state && this.is_active) {
|
|
this._grab();
|
|
} else {
|
|
this._ungrab();
|
|
}
|
|
}
|
|
|
|
_grab() {
|
|
if (!this.visible || this._device) return;
|
|
|
|
let seat = Gdk.Display.get_default().get_default_seat();
|
|
let status = seat.grab(
|
|
this.get_window(),
|
|
Gdk.SeatCapabilities.KEYBOARD,
|
|
false,
|
|
null,
|
|
null,
|
|
null
|
|
);
|
|
|
|
if (status !== Gdk.GrabStatus.SUCCESS) {
|
|
logError(new Error('Grabbing keyboard failed'));
|
|
return;
|
|
}
|
|
|
|
this._device = seat.get_keyboard();
|
|
this.grab_add();
|
|
this.text.has_focus = true;
|
|
}
|
|
|
|
_ungrab() {
|
|
if (this._device) {
|
|
this._device.get_seat().ungrab();
|
|
this._device = null;
|
|
this.grab_remove();
|
|
}
|
|
|
|
this.text.buffer.text = '';
|
|
}
|
|
});
|
|
|