]> git.eshelyaron.com Git - emacs.git/commitdiff
New focus management interface
authorDaniel Colascione <dancol@dancol.org>
Mon, 11 Jun 2018 21:58:09 +0000 (14:58 -0700)
committerDaniel Colascione <dancol@dancol.org>
Mon, 11 Jun 2018 23:10:34 +0000 (16:10 -0700)
focus-in-hook and focus-out-hook don't accurately reflect actual
user-visible focus states.  Add a new focus interface and mark the old
one obsolete.

* doc/lispref/frames.texi (Input Focus): Document new focus
functions.  Remove references to the now-obsolete focus hooks.

* lisp/frame.el (frame-focus-state): New function.
(after-focus-change-function): New variable.
(focus-in-hook, focus-out-hook): Move to lisp from C;
mark obsolete.

* lisp/term/xterm.el (xterm-translate-focus-in)
(xterm-translate-focus-out): Track tty focus in `tty-focus-state'
terminal parameter; call `after-focus-change-function'.
(xterm--suspend-tty-function): New function.

* src/frame.c (Fhandle_switch_frame): Update docstring; don't call
focus hooks.
(focus-in-hook, focus-out-hook): Remove: moved to lisp.
(syms_of_frame): Remove unread_switch_frame; add
Vunread_switch_frame.

* src/keyboard.c:
(Finternal_handle_focus_in): New function.
(make_lispy_event): Always report focus events to lisp; don't
translate them to switch events sometimes.  Lisp can take care of
creating synthetic switch-frame events via
`internal-handle-focus-in'.

* src/w32term.c (x_focus_changed): Remove switch-avoidance logic:
just directly report focus changes to lisp.

* src/xterm.c (x_focus_changed): Remove switch-avoidance logic:
just directly report focus changes to lisp.

doc/lispref/frames.texi
etc/NEWS
lisp/frame.el
lisp/term/xterm.el
src/frame.c
src/keyboard.c
src/w32term.c
src/xterm.c

index 459f05cb1c9f3d21df5eba4e2d4d12a1218f24ed..3a97ec0138472d968a1aa564a0c52dbe7e771ab9 100644 (file)
@@ -2702,14 +2702,22 @@ This function returns the selected frame.
 Some window systems and window managers direct keyboard input to the
 window object that the mouse is in; others require explicit clicks or
 commands to @dfn{shift the focus} to various window objects.  Either
-way, Emacs automatically keeps track of which frame has the focus.  To
+way, Emacs automatically keeps track of which frames have focus.  To
 explicitly switch to a different frame from a Lisp function, call
 @code{select-frame-set-input-focus}.
 
-Lisp programs can also switch frames temporarily by calling the
-function @code{select-frame}.  This does not alter the window system's
-concept of focus; rather, it escapes from the window manager's control
-until that control is somehow reasserted.
+The plural ``frames'' in the previous paragraph is deliberate: while
+Emacs itself has only one selected frame, Emacs can have frames on
+many different terminals (recall that a connection to a window system
+counts as a terminal), and each terminal has its own idea of which
+frame has input focus.  When you set the input focus to a frame, you
+set the focus for that frame's terminal, but frames on other terminals
+may still remain focused.
+
+Lisp programs can switch frames temporarily by calling the function
+@code{select-frame}.  This does not alter the window system's concept
+of focus; rather, it escapes from the window manager's control until
+that control is somehow reasserted.
 
 When using a text terminal, only one frame can be displayed at a time
 on the terminal, so after a call to @code{select-frame}, the next
@@ -2720,11 +2728,11 @@ before the buffer name (@pxref{Mode Line Variables}).
 
 @defun select-frame-set-input-focus frame &optional norecord
 This function selects @var{frame}, raises it (should it happen to be
-obscured by other frames) and tries to give it the X server's focus.
-On a text terminal, the next redisplay displays the new frame on the
-entire terminal screen.  The optional argument @var{norecord} has the
-same meaning as for @code{select-frame} (see below).  The return value
-of this function is not significant.
+obscured by other frames) and tries to give it the window system's
+focus.  On a text terminal, the next redisplay displays the new frame
+on the entire terminal screen.  The optional argument @var{norecord}
+has the same meaning as for @code{select-frame} (see below).
+The return value of this function is not significant.
 @end defun
 
 Ideally, the function described next should focus a frame without also
@@ -2772,17 +2780,31 @@ could switch to a different terminal without switching back when
 you're done.
 @end deffn
 
-Emacs cooperates with the window system by arranging to select frames as
-the server and window manager request.  It does so by generating a
-special kind of input event, called a @dfn{focus} event, when
-appropriate.  The command loop handles a focus event by calling
-@code{handle-switch-frame}.  @xref{Focus Events}.
+Emacs cooperates with the window system by arranging to select frames
+as the server and window manager request.  When a window system
+informs Emacs that one of its frames has been selected, Emacs
+internally generates a @dfn{focus-in} event.  Focus events are
+normally handled by @code{handle-focus-in}.
+
+@deffn Command handle-focus-in event
+This function handles focus-in events from window systems and
+terminals that support explicit focus notifications.  It updates the
+per-frame focus flags that @code{frame-focus-state} queries and calls
+@code{after-focus-change-function}.  In addition, it generates a
+@code{switch-frame} event in order to switch the Emacs notion of the
+selected frame to the frame most recently focused in some terminal.
+It's important to note that this switching of the Emacs selected frame
+to the most recently focused frame does not mean that other frames do
+not continue to have the focus in their respective terminals.  Do not
+invoke this function yourself: instead, attach logic to
+@code{after-focus-change-function}.
+@end deffn
 
 @deffn Command handle-switch-frame frame
-This function handles a focus event by selecting frame @var{frame}.
-
-Focus events normally do their job by invoking this command.
-Don't call it for any other reason.
+This function handles a switch-frame event, which Emacs generates for
+itself upon focus notification or under various other circumstances
+involving an input event arriving at a different frame from the last
+event.  Do not invoke this function yourself.
 @end deffn
 
 @defun redirect-frame-focus frame &optional focus-frame
@@ -2816,14 +2838,42 @@ The redirection lasts until @code{redirect-frame-focus} is called to
 change it.
 @end defun
 
-@defvar focus-in-hook
-This is a normal hook run when an Emacs frame gains input focus.  The
-frame gaining focus is selected when this hook is run.
-@end defvar
+@defun frame-focus-state frame
+This function retrieves the last known focus state of @var{frame}.
+
+It returns @code{nil} if the frame is known not to be focused,
+@code{t} if the frame is known to be focused, or @code{unknown} if
+Emacs does not know the focus state of the frame.  (You may see this
+last state in TTY frames running on terminals that do not support
+explicit focus notifications.)
+@end defun
 
-@defvar focus-out-hook
-This is a normal hook run when an Emacs frame has lost input focus and
-no other Emacs frame has gained input focus instead.
+@defvar after-focus-change-function
+This function is an extension point that code can use to receive a
+notification that focus has changed.
+
+This function is called with no arguments when Emacs notices that the
+set of focused frames may have changed.  Code wanting to do something
+when frame focus changes should use @code{add-function} to add a
+function to this one, and in this added function, re-scan the set of
+focused frames, calling @code{frame-focus-state} to retrieve the last
+known focus state of each frame.  Focus events are delivered
+asynchronously, and frame input focus according to an external system
+may not correspond to the notion of the Emacs selected frame.
+Multiple frames may appear to have input focus simultaneously due to
+focus event delivery differences, the presence of multiple Emacs
+terminals, and other factors, and code should be robust in the face of
+this situation.
+
+Depending on window system, focus events may also be delivered
+repeatedly and with different focus states before settling to the
+expected values.  Code relying on focus notifications should
+``debounce'' any user-visible updates arising from focus changes,
+perhaps by deferring work until redisplay.
+
+This function may be called in arbitrary contexts, including from
+inside @code{read-event}, so take the same care as you might when
+writing a process filter.
 @end defvar
 
 @defopt focus-follows-mouse
index 4ea3440754724d4ac7d8b271d505453fa867687e..7b14b9f89603fd62102fb623f229e21d0d08ba7d 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -582,6 +582,12 @@ manual for more details.
 \f
 * Lisp Changes in Emacs 27.1
 
++++
+** New focus state inspection interface: `focus-in-hook' and
+   `focus-out-hook' are marked obsolete.  Instead, attach to
+   `after-focus-change-function' using `add-function' and inspect the
+   focus state of each frame using `frame-focus-state'.
+
 +++
 ** Emacs now requests and recognizes focus-change notifications from
    terminals that support the feature, meaning that `focus-in-hook'
index c3daff444062d126a6b11161a413b74ef09a18e8..2a2391e8a530de59305c3ac4a86fd3b77facfa56 100644 (file)
@@ -129,22 +129,104 @@ appended when the minibuffer frame is created."
       ;; Gildea@x.org says it is ok to ask questions before terminating.
       (save-buffers-kill-emacs))))
 
-(defun handle-focus-in (&optional _event)
+(defun frame-focus-state (&optional frame)
+  "Return FRAME's last known focus state.
+Return nil if the frame is definitely known not be focused, t if
+the frame is known to be focused, and 'unknown if we don't know.  If
+FRAME is nil, query the selected frame."
+  (let* ((frame (or frame (selected-frame)))
+         (tty-top-frame (tty-top-frame frame)))
+    (if (not tty-top-frame)
+        (frame-parameter frame 'last-focus-update)
+      ;; All tty frames are frame-visible-p if the terminal is
+      ;; visible, so check whether the frame is the top tty frame
+      ;; before checking visibility.
+      (cond ((not (eq tty-top-frame frame)) nil)
+            ((not (frame-visible-p frame)) nil)
+            (t (let ((tty-focus-state
+                      (terminal-parameter frame 'tty-focus-state)))
+                 (cond ((eq tty-focus-state 'focused) t)
+                       ((eq tty-focus-state 'defocused) nil)
+                       (t 'unknown))))))))
+
+(defvar after-focus-change-function #'ignore
+  "Function called after frame focus may have changed.
+
+This function is called with no arguments when Emacs notices that
+the set of focused frames may have changed.  Code wanting to do
+something when frame focus changes should use `add-function' to
+add a function to this one, and in this added function, re-scan
+the set of focused frames, calling `frame-focus-state' to
+retrieve the last known focus state of each frame.  Focus events
+are delivered asynchronously, and frame input focus according to
+an external system may not correspond to the notion of the Emacs
+selected frame.  Multiple frames may appear to have input focus
+simultaneously due to focus event delivery differences, the
+presence of multiple Emacs terminals, and other factors, and code
+should be robust in the face of this situation.
+
+Depending on window system, focus events may also be delivered
+repeatedly and with different focus states before settling to the
+expected values.  Code relying on focus notifications should
+\"debounce\" any user-visible updates arising from focus changes,
+perhaps by deferring work until redisplay.
+
+This function may be called in arbitrary contexts, including from
+inside `read-event', so take the same care as you might when
+writing a process filter.")
+
+(defvar focus-in-hook nil
+  "Normal hook run when a frame gains focus.
+The frame gaining focus is selected at the time this hook is run.
+
+This hook is obsolete.  Despite its name, this hook may be run in
+situations other than when a frame obtains input focus: for
+example, we also run this hook when switching the selected frame
+internally to handle certain input events (like mouse wheel
+scrolling) even when the user's notion of input focus
+hasn't changed.
+
+Prefer using `after-focus-change-function'.")
+(make-obsolete-variable
+ 'focus-in-hook "after-focus-change-function" "27.1" 'set)
+
+(defvar focus-out-hook nil
+  "Normal hook run when all frames lost input focus.
+
+This hook is obsolete; see `focus-in-hook'.  Depending on timing,
+this hook may be delivered when a frame does in fact have focus.
+Prefer `after-focus-change-function'.")
+(make-obsolete-variable
+ 'focus-out-hook "after-focus-change-function" "27.1" 'set)
+
+(defun handle-focus-in (event)
   "Handle a focus-in event.
-Focus-in events are usually bound to this function.
-Focus-in events occur when a frame has focus, but a switch-frame event
-is not generated.
-This function runs the hook `focus-in-hook'."
+Focus-in events are bound to this function; do not change this
+binding.  Focus-in events occur when a frame receives focus from
+the window system."
+  ;; N.B. tty focus goes down a different path; see xterm.el.
   (interactive "e")
-  (run-hooks 'focus-in-hook))
-
-(defun handle-focus-out (&optional _event)
+  (unless (eq (car-safe event) 'focus-in)
+    (error "handle-focus-in should handle focus-in events"))
+  (internal-handle-focus-in event)
+  (let ((frame (nth 1 event)))
+    (setf (frame-parameter frame 'last-focus-update) t)
+  (run-hooks 'focus-in-hook)
+  (funcall after-focus-change-function)))
+
+(defun handle-focus-out (event)
   "Handle a focus-out event.
-Focus-out events are usually bound to this function.
-Focus-out events occur when no frame has focus.
-This function runs the hook `focus-out-hook'."
+Focus-out events are bound to this function; do not change this
+binding.  Focus-out events occur when a frame loses focus, but
+that's not the whole story: see `after-focus-change-function'."
+  ;; N.B. tty focus goes down a different path; see xterm.el.
   (interactive "e")
-  (run-hooks 'focus-out-hook))
+  (unless (eq (car event) 'focus-out)
+    (error "handle-focus-out should handle focus-out events"))
+  (let ((frame (nth 1 event)))
+    (setf (frame-parameter frame 'last-focus-update) nil)
+    (run-hooks 'focus-out-hook)
+    (funcall after-focus-change-function)))
 
 (defun handle-move-frame (event)
   "Handle a move-frame event.
index b3b7a21635209cd62cef2a22bcba085b54d8fe98..ce4e18efff8e17eeaa9637da3e7bd3016b2bb648 100644 (file)
@@ -115,13 +115,20 @@ Return the pasted text as a string."
 ;; notifications) instead of read-event (which can't).
 
 (defun xterm-translate-focus-in (_prompt)
-  (handle-focus-in)
+  (setf (terminal-parameter nil 'tty-focus-state) 'focused)
+  (funcall after-focus-change-function)
   [])
 
 (defun xterm-translate-focus-out (_prompt)
-  (handle-focus-out)
+  (setf (terminal-parameter nil 'tty-focus-state) 'defocused)
+  (funcall after-focus-change-function)
   [])
 
+(defun xterm--suspend-tty-function (_tty)
+  ;; We can't know what happens to the tty after we're suspended
+  (setf (terminal-parameter nil 'tty-focus-state) nil)
+  (funcall after-focus-change-function))
+
 ;; Similarly, we want to transparently slurp the entirety of a
 ;; bracketed paste and encapsulate it into a single event.  We used to
 ;; just slurp up the bracketed paste content in the event handler, but
index da82621b8a0631b90eb5439e2ffee56e015530ad..bf0269292d671ffb377b26dc5300d0c29c634b7f 100644 (file)
@@ -1455,23 +1455,15 @@ This function returns FRAME, or nil if FRAME has been deleted.  */)
 DEFUN ("handle-switch-frame", Fhandle_switch_frame, Shandle_switch_frame, 1, 1, "^e",
        doc: /* Handle a switch-frame event EVENT.
 Switch-frame events are usually bound to this function.
-A switch-frame event tells Emacs that the window manager has requested
-that the user's events be directed to the frame mentioned in the event.
-This function selects the selected window of the frame of EVENT.
-
-If EVENT is frame object, handle it as if it were a switch-frame event
-to that frame.  */)
+A switch-frame event is an event Emacs sends itself to
+indicate that input is arriving in a new frame. It does not
+necessarily represent user-visible input focus.  */)
   (Lisp_Object event)
 {
-  Lisp_Object value;
-
   /* Preserve prefix arg that the command loop just cleared.  */
   kset_prefix_arg (current_kboard, Vcurrent_prefix_arg);
   run_hook (Qmouse_leave_buffer_hook);
-  /* `switch-frame' implies a focus in.  */
-  value = do_switch_frame (event, 0, 0, Qnil);
-  call1 (intern ("handle-focus-in"), event);
-  return value;
+  return do_switch_frame (event, 0, 0, Qnil);
 }
 
 DEFUN ("selected-frame", Fselected_frame, Sselected_frame, 0, 0, 0,
@@ -5888,15 +5880,6 @@ when the mouse is over clickable text.  */);
 The pointer becomes visible again when the mouse is moved.  */);
   Vmake_pointer_invisible = Qt;
 
-  DEFVAR_LISP ("focus-in-hook", Vfocus_in_hook,
-               doc: /* Normal hook run when a frame gains input focus.
-The frame gaining focus is selected at the time this hook is run.  */);
-  Vfocus_in_hook = Qnil;
-
-  DEFVAR_LISP ("focus-out-hook", Vfocus_out_hook,
-               doc: /* Normal hook run when all frames lost input focus.  */);
-  Vfocus_out_hook = Qnil;
-
   DEFVAR_LISP ("move-frame-functions", Vmove_frame_functions,
                doc: /* Functions run after a frame was moved.
 The functions are run with one arg, the frame that moved.  */);
index c9e069c86531d6686b912b591d749912411503bd..84acb247228613a19e5609c72927878e3d514896 100644 (file)
@@ -5331,45 +5331,10 @@ make_lispy_event (struct input_event *event)
       }
 
     case FOCUS_IN_EVENT:
-      {
-       /* Notification of a FocusIn event.  The frame receiving the
-          focus is in event->frame_or_window.  Generate a
-          switch-frame event if necessary.  */
-
-        Lisp_Object frame = event->frame_or_window;
-        Lisp_Object focus = FRAME_FOCUS_FRAME (XFRAME (frame));
-        if (FRAMEP (focus))
-          frame = focus;
-        bool switching
-          = (
-#ifdef HAVE_X11
-            ! NILP (event->arg)
-            &&
-#endif
-            !EQ (frame, internal_last_event_frame)
-            && !EQ (frame, selected_frame));
-        internal_last_event_frame = frame;
-
-        return (switching ? make_lispy_switch_frame (frame)
-                : make_lispy_focus_in (frame));
-      }
+        return make_lispy_focus_in (event->frame_or_window);
 
     case FOCUS_OUT_EVENT:
-      {
-#ifdef HAVE_WINDOW_SYSTEM
-
-        Display_Info *di;
-        Lisp_Object frame = event->frame_or_window;
-        bool focused = false;
-
-        for (di = x_display_list; di && ! focused; di = di->next)
-          focused = di->x_highlight_frame != 0;
-
-        return focused ? Qnil
-               : make_lispy_focus_out (frame);
-
-#endif /* HAVE_WINDOW_SYSTEM */
-      }
+        return make_lispy_focus_out (event->frame_or_window);
 
     /* A simple keystroke.  */
     case ASCII_KEYSTROKE_EVENT:
@@ -6637,6 +6602,31 @@ has the same base event type and all the specified modifiers.  */)
     error ("Invalid base event");
 }
 
+DEFUN ("internal-handle-focus-in", Finternal_handle_focus_in,
+       Sinternal_handle_focus_in, 1, 1, 0,
+       doc: /* Internally handle focus-in events, possibly generating
+an artifical switch-frame event.  */)
+     (Lisp_Object event)
+{
+  Lisp_Object frame;
+  if (!EQ (CAR_SAFE (event), Qfocus_in) ||
+      !CONSP (XCDR (event)) ||
+      !FRAMEP ((frame = XCAR (XCDR (event)))))
+    error ("invalid focus-in event");
+
+  /* Conceptually, the concept of window manager focus on a particular
+   frame and the Emacs selected frame shouldn't be related, but for a
+   long time, we automatically switched the selected frame in response
+   to focus events, so let's keep doing that.  */
+  bool switching = (!EQ (frame, internal_last_event_frame)
+                    && !EQ (frame, selected_frame));
+  internal_last_event_frame = frame;
+  if (switching || !NILP (unread_switch_frame))
+    unread_switch_frame = make_lispy_switch_frame (frame);
+
+  return Qnil;
+}
+
 /* Try to recognize SYMBOL as a modifier name.
    Return the modifier flag bit, or 0 if not recognized.  */
 
@@ -11277,6 +11267,7 @@ syms_of_keyboard (void)
   defsubr (&Scurrent_idle_time);
   defsubr (&Sevent_symbol_parse_modifiers);
   defsubr (&Sevent_convert_list);
+  defsubr (&Sinternal_handle_focus_in);
   defsubr (&Sread_key_sequence);
   defsubr (&Sread_key_sequence_vector);
   defsubr (&Srecursive_edit);
index 24950dd25ecb2142ffe631d9f6fc0b8818ac03bf..ff0d2bf5ddb11f869b0e00f2df596688bc82d4f2 100644 (file)
@@ -2886,20 +2886,6 @@ x_focus_changed (int type, int state, struct w32_display_info *dpyinfo,
         {
           x_new_focus_frame (dpyinfo, frame);
           dpyinfo->w32_focus_event_frame = frame;
-
-          /* Don't stop displaying the initial startup message
-             for a switch-frame event we don't need.  */
-          if (NILP (Vterminal_frame)
-              && CONSP (Vframe_list)
-              && !NILP (XCDR (Vframe_list)))
-            {
-              bufp->arg = Qt;
-            }
-          else
-            {
-              bufp->arg = Qnil;
-            }
-
           bufp->kind = FOCUS_IN_EVENT;
           XSETFRAME (bufp->frame_or_window, frame);
         }
index eb299c36759d352f460bef2f8d737941cea06256..decaa33670b2e0cc226da07f900e0376ef0c7496 100644 (file)
@@ -4387,16 +4387,6 @@ x_focus_changed (int type, int state, struct x_display_info *dpyinfo, struct fra
         {
           x_new_focus_frame (dpyinfo, frame);
           dpyinfo->x_focus_event_frame = frame;
-
-          /* Don't stop displaying the initial startup message
-             for a switch-frame event we don't need.  */
-          /* When run as a daemon, Vterminal_frame is always NIL.  */
-          bufp->arg = (((NILP (Vterminal_frame)
-                         || ! FRAME_X_P (XFRAME (Vterminal_frame))
-                         || EQ (Fdaemonp (), Qt))
-                       && CONSP (Vframe_list)
-                       && !NILP (XCDR (Vframe_list)))
-                      ? Qt : Qnil);
           bufp->kind = FOCUS_IN_EVENT;
           XSETFRAME (bufp->frame_or_window, frame);
         }