From fbd9b1da821634f0288327c8924bec9fd9960aa0 Mon Sep 17 00:00:00 2001 From: Cecilio Pardo Date: Mon, 28 Oct 2024 23:57:35 +0100 Subject: [PATCH] Add events for key press and key release on gui systems. And detection of double/triple taps on modifier keys. --- lisp/physkey.el | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ src/keyboard.c | 21 +++++++++++++ src/pgtkterm.c | 34 ++++++++++++++++++++ src/termhooks.h | 1 + src/w32fns.c | 5 +++ src/w32term.c | 38 ++++++++++++++++++++++ src/w32term.h | 3 +- src/xterm.c | 39 +++++++++++++++++++++++ 8 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 lisp/physkey.el diff --git a/lisp/physkey.el b/lisp/physkey.el new file mode 100644 index 00000000000..5dd3f81d685 --- /dev/null +++ b/lisp/physkey.el @@ -0,0 +1,83 @@ +;;; -*- lexical-binding: t -*- +(require 'cl-lib) + +;; User options +(defvar physkey-tap-timeout 1000) +(defvar physkey-tap-count 2) +(defvar physkey-tap-keys + '(lshift rshift lctrl rctrl lalt ralt)) +(defvar physkey-bindings nil) + +(defun physkey-init () + (interactive) + (define-key special-event-map [physical-key] 'physkey-handle) + (setq physkey-bindings nil) + (physkey-bind 'tap 'lshift 'delete-other-windows) + (physkey-bind 'tap 'lctrl 'hyper)) + +;; For example: +;; (physkey-add-binding 'tap 'lshift 'delete-other-windows) +;; Can bind to a command, a function or the symbol 'hyper. +(defun physkey-bind (action key function) + (push (list action key function) physkey-bindings)) + +;; We store the last events here to test for multitap. +(defvar physkey-events nil) + +;; If positive, return key ('lshift, etc) else return nil. +(defun physkey-detect-n-tap (n timeout) + ;; The physical-key event is like this: + ;; (physical-key t lshift 90196265 #) + ;; The second element is t for a key press, nil for a key release + ;; The fourth element is the time in milliseconds + ;; The fifth is the frame, we don't use it yet. + + (let ((key (cl-third last-input-event))) + (if (not (member key physkey-tap-keys)) + ;; Key not in tap list, clear history + (setq physkey-events nil) + ;; Clear it also if the first element is from a different key + (and physkey-events + (not (equal (cl-third (car physkey-events)) key)) + (setq physkey-events nil)) + (push last-input-event physkey-events) + ;; Only care about last 2xN events + (ntake (* 2 n) physkey-events) + ;; If we have: + ;; - Exactly 2 * n events. + ;; - down, up, down, up, ... + ;; - not two much time between first and last + (and (eq (* 2 n) (length physkey-events)) + (cl-every 'eq + (ntake (* 2 n) + (list nil t nil t nil t nil t + nil t nil t nil t nil t)) + (mapcar 'cl-second physkey-events)) + (< (- (cl-fourth (cl-first physkey-events)) + (cl-fourth (car (last physkey-events)))) + timeout) + (progn + (setq physkey-events nil) + key))))) + +(defun physkey-handle () + (interactive) + (let ((tap-key (physkey-detect-n-tap + physkey-tap-count + physkey-tap-timeout))) + (when tap-key + (let ((func (cl-third + (seq-find + (lambda (b) + (and (eq (cl-first b) 'tap) + (eq (cl-second b) tap-key))) + physkey-bindings)))) + (cond + ((commandp func) (call-interactively func)) + ((functionp func) (funcall func)) + ((eq 'hyper func) + (message "H-...") + (let ((r (read-event))) + (setq unread-command-events + (list (event-apply-modifier + r 'hyper 24 "H-")))))))))) diff --git a/src/keyboard.c b/src/keyboard.c index 6d28dca9aeb..4c24e3c8bd3 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4274,6 +4274,7 @@ kbd_buffer_get_event (KBOARD **kbp, case CONFIG_CHANGED_EVENT: case FOCUS_OUT_EVENT: case SELECT_WINDOW_EVENT: + case PHYSICAL_KEY_EVENT: { obj = make_lispy_event (&event->ie); kbd_fetch_ptr = next_kbd_event (event); @@ -7118,6 +7119,14 @@ make_lispy_event (struct input_event *event) case PREEDIT_TEXT_EVENT: return list2 (Qpreedit_text, event->arg); + case PHYSICAL_KEY_EVENT: + return listn (5, + Qphysical_key, + XCAR (event->arg), /* Press or release. */ + XCAR (XCDR (event->arg)), /* The key symbol. */ + make_fixnum (event->timestamp), + event->frame_or_window); + /* The 'kind' field of the event is something we don't recognize. */ default: emacs_abort (); @@ -12931,6 +12940,14 @@ syms_of_keyboard (void) DEFSYM (Qfile_notify, "file-notify"); #endif /* USE_FILE_NOTIFY */ + DEFSYM (Qphysical_key, "physical-key"); + DEFSYM (Qlshift, "lshift"); + DEFSYM (Qrshift, "rshift"); + DEFSYM (Qlctrl, "lctrl"); + DEFSYM (Qrctrl, "rctrl"); + DEFSYM (Qlalt, "lalt"); + DEFSYM (Qralt, "ralt"); + DEFSYM (Qtouch_end, "touch-end"); /* Menu and tool bar item parts. */ @@ -14018,6 +14035,10 @@ keys_of_keyboard (void) "handle-focus-out"); initial_define_lispy_key (Vspecial_event_map, "move-frame", "handle-move-frame"); + initial_define_lispy_key (Vspecial_event_map, "physical-key", + "ignore"); + + } /* Mark the pointers in the kboard objects. diff --git a/src/pgtkterm.c b/src/pgtkterm.c index 079945126e0..f4ade6c9f1c 100644 --- a/src/pgtkterm.c +++ b/src/pgtkterm.c @@ -5201,6 +5201,36 @@ pgtk_enqueue_preedit (struct frame *f, Lisp_Object preedit) evq_enqueue (&inev); } +static void +pgtk_maybe_send_physical_key_event (GdkEvent *event) +{ + Lisp_Object key; + switch (event->key.keyval) + { + case GDK_KEY_Shift_L: key = Qlshift; break; + case GDK_KEY_Shift_R: key = Qrshift; break; + case GDK_KEY_Control_L: key = Qlctrl; break; + case GDK_KEY_Control_R: key = Qrctrl; break; + case GDK_KEY_Alt_L: key = Qlalt; break; + case GDK_KEY_Alt_R: key = Qralt; break; + default: + return; + } + bool keypress = event->key.type == GDK_KEY_PRESS; + struct frame *f = pgtk_any_window_to_frame (event->key.window); + if (!f) + return; + + union buffered_input_event inev; + + EVENT_INIT (inev.ie); + XSETFRAME (inev.ie.frame_or_window, f); + inev.ie.kind = PHYSICAL_KEY_EVENT; + inev.ie.timestamp = event->key.time; + inev.ie.arg = list2 (keypress ? Qt : Qnil, key); + evq_enqueue (&inev); +} + static gboolean key_press_event (GtkWidget *widget, GdkEvent *event, gpointer *user_data) { @@ -5210,6 +5240,8 @@ key_press_event (GtkWidget *widget, GdkEvent *event, gpointer *user_data) struct frame *f; struct pgtk_display_info *dpyinfo; + pgtk_maybe_send_physical_key_event(event); + f = pgtk_any_window_to_frame (gtk_widget_get_window (widget)); EVENT_INIT (inev.ie); hlinfo = MOUSE_HL_INFO (f); @@ -5454,6 +5486,8 @@ key_release_event (GtkWidget *widget, GdkDisplay *display; struct pgtk_display_info *dpyinfo; + pgtk_maybe_send_physical_key_event(event); + display = gtk_widget_get_display (widget); dpyinfo = pgtk_display_info_for_display (display); diff --git a/src/termhooks.h b/src/termhooks.h index d6a9300bac9..f03b6126a53 100644 --- a/src/termhooks.h +++ b/src/termhooks.h @@ -347,6 +347,7 @@ #define EMACS_TERMHOOKS_H /* In a NOTIFICATION_EVENT, .arg is a lambda to evaluate. */ , NOTIFICATION_EVENT #endif /* HAVE_ANDROID */ + , PHYSICAL_KEY_EVENT }; /* Bit width of an enum event_kind tag at the start of structs and unions. */ diff --git a/src/w32fns.c b/src/w32fns.c index eb42d3b61b2..ef98f2db7e7 100644 --- a/src/w32fns.c +++ b/src/w32fns.c @@ -4650,6 +4650,8 @@ w32_wnd_proc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) case WM_KEYUP: case WM_SYSKEYUP: record_keyup (wParam, lParam); + signal_user_input (); + my_post_msg( &wmsg, hwnd, WM_EMACS_PHYSICAL_KEY, wParam, lParam ); goto dflt; case WM_KEYDOWN: @@ -4676,6 +4678,9 @@ w32_wnd_proc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) if (w32_use_fallback_wm_chars_method) wParam = map_keypad_keys (wParam, (lParam & 0x1000000L) != 0); + signal_user_input (); + my_post_msg( &wmsg, hwnd, WM_EMACS_PHYSICAL_KEY, wParam, lParam ); + windows_translate = 0; switch (wParam) diff --git a/src/w32term.c b/src/w32term.c index 88622700386..c58391d4897 100644 --- a/src/w32term.c +++ b/src/w32term.c @@ -5230,6 +5230,44 @@ w32_read_socket (struct terminal *terminal, } break; + case WM_EMACS_PHYSICAL_KEY: + WORD key_flags = HIWORD (msg.msg.lParam); + BOOL is_wm_keyup = key_flags & KF_UP; + + if (is_wm_keyup || (key_flags & KF_REPEAT) == 0) /* WM_KEYDOWN, not repeating. */ + { + WORD scan_code = LOBYTE (key_flags); + if (key_flags & KF_EXTENDED) + scan_code = MAKEWORD (scan_code, 0xE0); + + UINT translated = MapVirtualKey (scan_code, MAPVK_VSC_TO_VK_EX); + WORD vk = LOWORD (msg.msg.wParam); + if (translated) + vk = LOWORD (translated); + + Lisp_Object key = Qnil; + + switch (vk) + { + case VK_LSHIFT: key = Qlshift; break; + case VK_RSHIFT: key = Qrshift; break; + case VK_LCONTROL: key = Qlctrl; break; + case VK_RCONTROL: key = Qrctrl; break; + case VK_LMENU: key = Qlalt; break; + case VK_RMENU: key = Qralt; break; + } + + if (!NILP (key)) + { + f = w32_window_to_frame (dpyinfo, msg.msg.hwnd); + inev.kind = PHYSICAL_KEY_EVENT; + XSETFRAME (inev.frame_or_window, f); + inev.timestamp = msg.msg.time; + inev.arg = list2 (is_wm_keyup ? Qnil : Qt, key); + } + } + break; + case WM_UNICHAR: case WM_SYSCHAR: case WM_CHAR: diff --git a/src/w32term.h b/src/w32term.h index cad9fcf8cb1..24fdf5421f8 100644 --- a/src/w32term.h +++ b/src/w32term.h @@ -713,7 +713,8 @@ #define WM_EMACS_FILENOTIFY (WM_EMACS_START + 25) #define WM_EMACS_IME_STATUS (WM_EMACS_START + 26) #define WM_EMACS_DRAGOVER (WM_EMACS_START + 27) #define WM_EMACS_DROP (WM_EMACS_START + 28) -#define WM_EMACS_END (WM_EMACS_START + 29) +#define WM_EMACS_PHYSICAL_KEY (WM_EMACS_START + 29) +#define WM_EMACS_END (WM_EMACS_START + 30) #define WND_FONTWIDTH_INDEX (0) #define WND_LINEHEIGHT_INDEX (4) diff --git a/src/xterm.c b/src/xterm.c index 0c20d38b0f7..210c5a9ef50 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -17840,6 +17840,43 @@ #define STORE_KEYSYM_FOR_DEBUG(keysym) ((void)0) static struct x_display_info *next_noop_dpyinfo; +static void +x_maybe_send_physical_key_event (struct x_display_info *dpyinfo, XEvent *event) +{ +#ifdef HAVE_XKB + if (event->type != KeyPress && event->type != KeyRelease) + return; + bool keypress = (event->type == KeyPress); + KeySym keysym = XkbKeycodeToKeysym (dpyinfo->display, event->xkey.keycode, + 0, 0 ); + Lisp_Object key; + switch (keysym) + { + case XK_Shift_L: key = Qlshift; break; + case XK_Shift_R: key = Qrshift; break; + case XK_Control_L: key = Qlctrl; break; + case XK_Control_R: key = Qrctrl; break; + case XK_Alt_L: key = Qlalt; break; + case XK_Alt_R: key = Qralt; break; + default: + return; + } + + struct frame *f = x_any_window_to_frame (dpyinfo, event->xkey.window); + if (!f) + return; + + struct input_event ie; + + EVENT_INIT (ie); + XSETFRAME (ie.frame_or_window, f); + ie.kind = PHYSICAL_KEY_EVENT; + ie.timestamp = event->xkey.time; + ie.arg = list2 (keypress ? Qt : Qnil, key); + kbd_buffer_store_event (&ie); +#endif +} + /* Filter events for the current X input method. DPYINFO is the display this event is for. EVENT is the X event to filter. @@ -17859,6 +17896,8 @@ x_filter_event (struct x_display_info *dpyinfo, XEvent *event) struct frame *f1; + x_maybe_send_physical_key_event (dpyinfo, event); + #if defined HAVE_XINPUT2 && defined USE_GTK bool xinput_event = false; if (dpyinfo->supports_xi2 -- 2.35.1.windows.2