;;; exwm-mff.el --- Mouse Follows Focus -*- lexical-binding: t; -*- ;; Copyright (C) 2019, 2020, 2021 Ian Eure ;; Author: Ian Eure ;; URL: https://github.com/ieure/exwm-mff ;; Version: 1.2.1 ;; Package-Requires: ((emacs "25.1")) ;; Keywords: unix ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Mouse Follows Focus ;; =================== ;; ;; Traditional window managers are mouse-centric: the window to receive ;; input is usually selected with the pointing device. ;; ;; Emacs is keybord-centric: the window to receive key input is usually ;; selected with the keyboard. When you use the keyboard to focus a ;; window, the spatial relationship between pointer and active window is ;; broken -- the pointer can be anywhere on the screen, instead of over ;; the active window, which can make it hard to find. ;; ;; The same problem also exists in traditional windowing systems when ;; you use the keyboard to switch windows, e.g. with Alt-Tab. ;; ;; Because Emacs’ model is inverted, this suggests that the correct ;; behavior is also the inverse -- instead of using the mouse to ;; select a window to receive keyboard input, the keyboard should be ;; used to select the window to receive mouse input. ;; ;; `EXWM-MFF-MODE' is a global minor mode which does exactly this. ;; When the selected window in Emacs changes, the mouse pointer is ;; moved to its center, unless the pointer is already somewhere inside ;; the window’s bounds. While it's especially helpful for for EXWM ;; users, it works for any Emacs window in a graphical session. ;; ;; This package also offers the `EXWM-MFF-WARP-TO-SELECTED' command, ;; which allows you to summon the pointer with a hotkey. Unlike the ;; minor mode, summoning is unconditional, and will place the pointer in ;; the center of the window even if it already resides within its bounds ;; -- a handy feature if you’ve lost your pointer, even if you’re using ;; the minor mode. ;; ;; ;; Limitations ;; ~~~~~~~~~~~ ;; ;; None known at this time. ;;; Code: (require 'subr-x) (require 'cl-macs) (defcustom exwm-mff-ignore-if nil "List of predicate functions for windows to ignore. Predicates accept one argument, WINDOW, and return non-NIL if automatic pointer warping should be suppressed." :type 'hook :group 'exwm-mff) (defconst exwm-mff--debug-buffer " *exwm-mff-debug*" "Name of the buffer exwm-mff will write debug messages into.") (defvar exwm-mff--debug 0 "Whether (and how) to debug exwm-mff. 0 = don't debug. 1 = log messages to *exwm-mff-debug*. 2 = log messages to *exwm-mff-debug* and the echo area.") (defvar exwm-mff--last-window nil "The last selected window.") (defun exwm-mff--guard () "Raise an error unless this is a graphic session with mouse support." (unless (and (display-graphic-p) (display-mouse-p)) (error "EXWM-MFF-MODE doesn't work on non-graphic or non-mouse sessions"))) (defun exwm-mff--contains-pointer? (window) "Return non-NIL when the mouse pointer is within FRAME and WINDOW." (cl-destructuring-bind ((mouse-x . mouse-y) (left top right bottom)) (list (mouse-absolute-pixel-position) (window-absolute-pixel-edges window)) (and (<= left mouse-x right) (<= top mouse-y bottom)))) (defun exwm-mff--debug (string &rest objects) "Log debug message STRING, using OBJECTS to format it." (let ((debug-level (or exwm-mff--debug 0))) (when (> debug-level 0) (let ((str (apply #'format (concat "[%s] " string) (cons (current-time-string) objects)))) (when (>= debug-level 1) (with-current-buffer (get-buffer-create exwm-mff--debug-buffer) (goto-char (point-max)) (insert (concat str "\n"))) (when (>= debug-level 2) (message str))))))) (defun exwm-mff-show-debug () "Enable exwm-mff debugging, and show the buffer with debug logs." (interactive) (setq exwm-mff--debug 1) (pop-to-buffer (get-buffer-create exwm-mff--debug-buffer))) (defun exwm-mff--window-center (window) "Return a list of (x y) coordinates of the center of WINDOW in FRAME." (cl-destructuring-bind (left top right bottom) (window-pixel-edges window) (list (+ left (/ (- right left) 2)) (+ top (/ (- bottom top) 2))))) (defun exwm-mff-warp-to (frame window) "Place the pointer in the center of WINDOW in FRAME." (apply #'set-mouse-pixel-position frame (exwm-mff--window-center window))) ;;;###autoload (defun exwm-mff-warp-to-selected () "Place the pointer in the center of the selected window." (interactive) (exwm-mff--guard) (exwm-mff-warp-to (selected-frame) (selected-window))) (defun exwm-mff--explain (selected-window same-window? contains-pointer? mini? ignored?) "Use SELECTED-WINDOW, SAME-WINDOW?, CONTAINS-POINTER?, MINI? and IGNORED? to return an explanation of focusing behavior." (cond (same-window? "selected window hasn't changed") (contains-pointer? "already contains pointer") (mini? "is minibuffer") (ignored? "one or more functions in `exwm-mff-ignore-if' matches") (t (format "doesn't contain pointer (in %s)" selected-window)))) (defun exwm-mff-hook (sw &optional norecord) "EXWM-MFF-MODE hook. This is after-advice placed on SELECT-WINDOW. It moves the pointer to SW (the currently selected window), if NORECORD is nil, and if it's not already in it." (unless norecord (if-let ((same-window? (eq sw exwm-mff--last-window))) ;; The selected window is unchanged, we don't need to check ;; anything else. (exwm-mff--debug "nop-> %s" (exwm-mff--explain sw same-window? nil nil nil)) (let* ((sf (window-frame sw)) (contains-pointer? (exwm-mff--contains-pointer? sw)) (mini? (minibufferp (window-buffer sw))) (ignore? (run-hook-with-args-until-success 'exwm-mff-ignore-if sw))) (if (or same-window? contains-pointer? mini? ignore?) (exwm-mff--debug "nop-> %s::%s (%s)" sf sw (exwm-mff--explain sw nil contains-pointer? mini? ignore?)) (exwm-mff--debug "warp-> %s::%s (%s)" sf sw (exwm-mff--explain sw nil contains-pointer? mini? ignore?)) (exwm-mff-warp-to sf (setq exwm-mff--last-window sw))))))) (defgroup exwm-mff nil "Mouse-Follows-Focus mode for EXWM." :group 'exwm) ;;;###autoload (define-minor-mode exwm-mff-mode "Mouse follows focus mode for EXWM." :global t :require 'exwm-mff :group 'exwm-mff (exwm-mff--guard) (if exwm-mff-mode (advice-add 'select-window :after #'exwm-mff-hook) (advice-remove 'select-window #'exwm-mff-hook))) (provide 'exwm-mff) ;;; exwm-mff.el ends here