461 lines
13 KiB
JavaScript
Raw Normal View History

2024-07-08 22:46:35 +02:00
// 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;
}
});