461 lines
13 KiB
JavaScript
461 lines
13 KiB
JavaScript
|
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||
|
//
|
||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
||
|
import GLib from 'gi://GLib';
|
||
|
import Gdk from 'gi://Gdk';
|
||
|
import GObject from 'gi://GObject';
|
||
|
import Gtk from 'gi://Gtk';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* 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);
|
||
|
|
||
|
|
||
|
export const InputDialog = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectMousepadInputDialog',
|
||
|
Properties: {
|
||
|
'device': GObject.ParamSpec.object(
|
||
|
'device',
|
||
|
'Device',
|
||
|
'The device associated with this window',
|
||
|
GObject.ParamFlags.READWRITE,
|
||
|
GObject.Object
|
||
|
),
|
||
|
'plugin': GObject.ParamSpec.object(
|
||
|
'plugin',
|
||
|
'Plugin',
|
||
|
'The mousepad plugin associated with this window',
|
||
|
GObject.ParamFlags.READWRITE,
|
||
|
GObject.Object
|
||
|
),
|
||
|
},
|
||
|
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/mousepad-input-dialog.ui',
|
||
|
Children: [
|
||
|
'infobar', 'infobar-label',
|
||
|
'touchpad-eventbox', 'mouse-left-button', 'mouse-middle-button', 'mouse-right-button',
|
||
|
'touchpad-drag', 'touchpad-long-press',
|
||
|
'shift-label', 'ctrl-label', 'alt-label', 'super-label', 'entry',
|
||
|
],
|
||
|
}, class InputDialog extends Gtk.Dialog {
|
||
|
|
||
|
_init(params) {
|
||
|
super._init(Object.assign({
|
||
|
use_header_bar: true,
|
||
|
}, params));
|
||
|
|
||
|
const headerbar = this.get_titlebar();
|
||
|
headerbar.title = _('Remote Input');
|
||
|
headerbar.subtitle = this.device.name;
|
||
|
|
||
|
// Main Box
|
||
|
const content = this.get_content_area();
|
||
|
content.border_width = 0;
|
||
|
|
||
|
// TRANSLATORS: Displayed when the remote keyboard is not ready to accept input
|
||
|
this.infobar_label.label = _('Remote keyboard on %s is not active').format(this.device.name);
|
||
|
|
||
|
// Text Input
|
||
|
this.entry.buffer.connect(
|
||
|
'insert-text',
|
||
|
this._onInsertText.bind(this)
|
||
|
);
|
||
|
|
||
|
this.infobar.connect('notify::reveal-child', this._onState.bind(this));
|
||
|
this.plugin.bind_property('state', this.infobar, 'reveal-child', 6);
|
||
|
|
||
|
// Mouse Pad
|
||
|
this._resetTouchpadMotion();
|
||
|
this.touchpad_motion_timeout_id = 0;
|
||
|
this.touchpad_holding = false;
|
||
|
|
||
|
// Scroll Input
|
||
|
this.add_events(Gdk.EventMask.SCROLL_MASK);
|
||
|
|
||
|
this.show_all();
|
||
|
}
|
||
|
|
||
|
vfunc_delete_event(event) {
|
||
|
this._ungrab();
|
||
|
return this.hide_on_delete();
|
||
|
}
|
||
|
|
||
|
vfunc_grab_broken_event(event) {
|
||
|
if (event.keyboard)
|
||
|
this._ungrab();
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
vfunc_key_release_event(event) {
|
||
|
if (!this.plugin.state)
|
||
|
debug('ignoring remote keyboard state');
|
||
|
|
||
|
const keyvalLower = Gdk.keyval_to_lower(event.keyval);
|
||
|
const 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)
|
||
|
debug('ignoring remote keyboard state');
|
||
|
|
||
|
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)
|
||
|
return false;
|
||
|
|
||
|
debug(`keyval: ${event.keyval}, mask: ${realMask}`);
|
||
|
|
||
|
const 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 {
|
||
|
const 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_scroll_event(event) {
|
||
|
if (event.delta_x === 0 && event.delta_y === 0)
|
||
|
return true;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
scroll: true,
|
||
|
dx: event.delta_x * 200,
|
||
|
dy: event.delta_y * 200,
|
||
|
},
|
||
|
});
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
vfunc_window_state_event(event) {
|
||
|
if (!this.plugin.state)
|
||
|
debug('ignoring remote keyboard state');
|
||
|
|
||
|
if (event.new_window_state & Gdk.WindowState.FOCUSED)
|
||
|
this._grab();
|
||
|
else
|
||
|
this._ungrab();
|
||
|
|
||
|
return super.vfunc_window_state_event(event);
|
||
|
}
|
||
|
|
||
|
_onInsertText(buffer, location, text, len) {
|
||
|
if (this._isAck)
|
||
|
return;
|
||
|
|
||
|
debug(`insert-text: ${text} (chars ${[...text].length})`);
|
||
|
|
||
|
for (const char of [...text]) {
|
||
|
if (!char)
|
||
|
continue;
|
||
|
|
||
|
// TODO: modifiers?
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
alt: false,
|
||
|
ctrl: false,
|
||
|
shift: false,
|
||
|
super: false,
|
||
|
sendAck: false,
|
||
|
key: char,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_onState(widget) {
|
||
|
if (!this.plugin.state)
|
||
|
debug('ignoring remote keyboard state');
|
||
|
|
||
|
if (this.is_active)
|
||
|
this._grab();
|
||
|
else
|
||
|
this._ungrab();
|
||
|
}
|
||
|
|
||
|
_grab() {
|
||
|
if (!this.visible || this._keyboard)
|
||
|
return;
|
||
|
|
||
|
const seat = Gdk.Display.get_default().get_default_seat();
|
||
|
const 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._keyboard = seat.get_keyboard();
|
||
|
this.grab_add();
|
||
|
this.entry.has_focus = true;
|
||
|
}
|
||
|
|
||
|
_ungrab() {
|
||
|
if (this._keyboard) {
|
||
|
this._keyboard.get_seat().ungrab();
|
||
|
this._keyboard = null;
|
||
|
this.grab_remove();
|
||
|
}
|
||
|
|
||
|
this.entry.buffer.text = '';
|
||
|
}
|
||
|
|
||
|
_resetTouchpadMotion() {
|
||
|
this.touchpad_motion_prev_x = 0;
|
||
|
this.touchpad_motion_prev_y = 0;
|
||
|
this.touchpad_motion_x = 0;
|
||
|
this.touchpad_motion_y = 0;
|
||
|
}
|
||
|
|
||
|
_onMouseLeftButtonClicked(button) {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
singleclick: true,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_onMouseMiddleButtonClicked(button) {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
middleclick: true,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_onMouseRightButtonClicked(button) {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
rightclick: true,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_onTouchpadDragBegin(gesture) {
|
||
|
this._resetTouchpadMotion();
|
||
|
|
||
|
this.touchpad_motion_timeout_id =
|
||
|
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10,
|
||
|
this._onTouchpadMotionTimeout.bind(this));
|
||
|
}
|
||
|
|
||
|
_onTouchpadDragUpdate(gesture, offset_x, offset_y) {
|
||
|
this.touchpad_motion_x = offset_x;
|
||
|
this.touchpad_motion_y = offset_y;
|
||
|
}
|
||
|
|
||
|
_onTouchpadDragEnd(gesture) {
|
||
|
this._resetTouchpadMotion();
|
||
|
|
||
|
GLib.Source.remove(this.touchpad_motion_timeout_id);
|
||
|
this.touchpad_motion_timeout_id = 0;
|
||
|
}
|
||
|
|
||
|
_onTouchpadLongPressCancelled(gesture) {
|
||
|
const gesture_button = gesture.get_current_button();
|
||
|
|
||
|
// Check user dragged less than certain distances.
|
||
|
const is_click =
|
||
|
(Math.abs(this.touchpad_motion_x) < 4) &&
|
||
|
(Math.abs(this.touchpad_motion_y) < 4);
|
||
|
|
||
|
if (is_click) {
|
||
|
const click_body = {};
|
||
|
switch (gesture_button) {
|
||
|
case 1:
|
||
|
click_body.singleclick = true;
|
||
|
break;
|
||
|
|
||
|
case 2:
|
||
|
click_body.middleclick = true;
|
||
|
break;
|
||
|
|
||
|
case 3:
|
||
|
click_body.rightclick = true;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: click_body,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_onTouchpadLongPressPressed(gesture) {
|
||
|
const gesture_button = gesture.get_current_button();
|
||
|
|
||
|
if (gesture_button !== 1) {
|
||
|
debug('Long press on other type of buttons are not handled.');
|
||
|
} else {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
singlehold: true,
|
||
|
},
|
||
|
});
|
||
|
this.touchpad_holding = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_onTouchpadLongPressEnd(gesture) {
|
||
|
if (this.touchpad_holding) {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
singlerelease: true,
|
||
|
},
|
||
|
});
|
||
|
this.touchpad_holding = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_onTouchpadMotionTimeout() {
|
||
|
const diff_x = this.touchpad_motion_x - this.touchpad_motion_prev_x;
|
||
|
const diff_y = this.touchpad_motion_y - this.touchpad_motion_prev_y;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mousepad.request',
|
||
|
body: {
|
||
|
dx: diff_x,
|
||
|
dy: diff_y,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
this.touchpad_motion_prev_x = this.touchpad_motion_x;
|
||
|
this.touchpad_motion_prev_y = this.touchpad_motion_y;
|
||
|
return true;
|
||
|
}
|
||
|
});
|