// 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; } });