Subject: bug#745: pop-to-buffer, frames, and input focus

From: martin rudalics <[email-masked]>
Date: Mon, 25 Aug 2008 15:45:48 +0200

> I've read a little bit in the Xlib Manual[*] and here is what I learned:

I've tried to read that but for the time being I simply believe what you
say here.

 > If we set the focus to the old frame by calling
 > select-frame-set-input-focus in display-buffer we have something like a
 > race condition.  The window manager may decide at the same time that the
 > new frame should have the focus and either Emacs or the WM will win the
 > race.
 >
 > Emacs sets the "input-hint" flag in WM_HINTS when creating new frames.
 > This can be seen with:
 >
 >   shell$ xprop WM_HINTS
 >   WM_HINTS(WM_HINTS):
 >                 Client accepts input or input focus: True
 >                 Initial state is Normal State.
 >                 bitmap id # to use for icon: 0xa00710
 >                 bitmap id # of mask for icon: 0xa00712
 >                 window id # of group leader: 0xa00001
 >
 > If the input-hint flag is set, some window managers will assume that
 > Emacs doesn't switch the input focus by itself and so the WM does it.
 >
 > In theory, Emacs could clear the input-hint flag (to stop the WM from
 > switching focus) and instead listen to WM_TAKE_FOCUS events.  The window
 > manager sends WM_TAKE_FOCUS to Emacs, when the WM thinks that Emacs
 > should now be focused.  Emacs can then decide to set (or not) the focus
 > to the appropriate frame.
 >
 > I don't know how well that would work in practice or if all window
 > managers follow the conventions.  But if the input-hint flag is set,
 > Emacs has to fight with the window manager.  OTOH, some people will
 > prefer that the WM does all the focus switching.  I guess it's not worth
 > fixing situation A.  People with decent window managers will be able to
 > configure their preferred focus behavior anyway.

So when `display-buffer' has called `pop-up-frame-function' we always
anticipate that the new frame is mapped (visible), has got input focus,
and is raised.  We cannot remove input focus from it or lower it because
we would risk some nasty race conditions with the window manager.  The
only reasonable alternative would be to make the new frame Withdrawn and
let `pop-to-buffer' etc. activate it but this seems hardly manageable.

We should document the current behavior to avoid going through these
torments again.

 >>> (If the
 >>> buffer is already visible in some frame, that frame is raised but the
 >>> focus is not moved to that frame.  This is IMO correct.)
 >> I think so.  But what happens in the `pop-to-buffer' case when a _new_
 >> frame gets displayed.  Should we give it input-focus?
 >
 > Yes.  I don't see why not.

And raise it I presume.  IIUC we currently rely on the window-manager to
raise and focus the frame.

 >> I suppose so from
 >> your situation B comments below.  So in all cases where situation A has
 >> Y (that is `display-buffer' fails TDTRT) `pop-to-buffer' DTRT?
 >
 > Yes, right.  In this case pop-up-frames must be t when pop-to-buffer is
 > called.

Is `pop-to-buffer' free of complications when activating a just created
frame?  From your understanding would this be a NOOP for most window
managers?  Note that `x-focus-frame' uses XSetInputFocus together with
x_ewmh_activate_frame.  This mixture seems slightly frightening.

 >> Note in this context
 >> that `select-frame-set-input-focus' always raises the frame.
 >
 > Looks unnecessary.

For Metacity, maybe, because of their "can't raise without focusing"
invariant read backwards.  But would it harm?  IIRC some window-managers
may also move frames between desktops/workspaces when raising them.

 > Emacs also calls XSetInputFocus together with
 > x_ewmh_activate_frame in Fx_focus_frame.  One of those is probably
 > unnecessary.  x_ewmh_activate_frame tells the WM to try to focus the
 > frame, but some WMs will also raise the window at the same time.
 >
 >>> ?  N  Metacity   follow-mouse (mouse)
 >>> ?  N  Metacity   follow-mouse (sloppy)
 >>> ?  N  Metacity   click-to-focus (click)
 >> Does Metacity respect `select-frame-set-input-focus' at all?
 >
 > Yes, this seems to work.  [Though, it also moves the mouse pointer to
 > the upper right corner of the new frame.  Maybe some Metacity
 > idiosyncrasy.]

Does it do so even with `focus-follows-mouse' nil?

 > I didn't know what to look for.  Below I attach two event traces for
[...]
 > One for Sawfish (with focus-window-when-mapped) and one for IceWM.  I
 > traced the events in xterm.c:handle_one_xevent and calls to
 > x_raise_frame.  Maybe you can make some sense out of it.

So far I can't :-(

Anyway: I could imagine something like the (completely untested) snippet
below to handle situation B - modulo whatever we should change in
`select-frame-set-input-focus', `x-focus-frame', ...

(defcustom pop-up-frame-activate nil
   "When non-nil try to explicitly activate popped up frames.
If this is nil leave it to Emacs how to activate the frame.  If
this is t always try to activate a new frame.  Anything else
means activate a frame if and only if it existed before the most
recent call to `display-buffer' ..."
   :type '(choice (const :tag "System Dependent" nil)
		 (const :tag "Existing frames only" 'existing-only)
		 (const :tag "Always" t))
   :group 'frames)

(defvar display-buffer-made-new-frame nil
   "When non-nil `display-buffer' created a new frame.")

(defun pop-to-buffer (buffer-or-name &optional other-window norecord)
   "Select buffer BUFFER-OR-NAME in some window, preferably a different one.
..."
   (let ((buffer
          ;; FIXME: This behavior is carried over from the previous C version
          ;; of pop-to-buffer, but really we should use just
          ;; `get-buffer' here.
          (if (null buffer-or-name) (other-buffer (current-buffer))
            (or (get-buffer buffer-or-name)
                (let ((buf (get-buffer-create buffer-or-name)))
                  (set-buffer-major-mode buf)
                  buf))))
	display-buffer-made-new-frame)
     (set-buffer buffer)
     (select-window (display-buffer buffer other-window) norecord)
     (when (and pop-up-frame-activate
	       (or (not display-buffer-made-new-frame)
		   (eq pop-up-frame-activate t)))
       (select-frame-set-input-focus (window-frame (selected-window))))
     buffer))

;;; In `display-buffer' replace the lines
      ((or pop-up-frames (not frame-to-use))
       ;; We want or need a new frame.
       (window--display-buffer-2
        buffer (frame-selected-window (funcall pop-up-frame-function))))

;;; by the lines
      ((or pop-up-frames (not frame-to-use))
       ;; We want or need a new frame.
       (window--display-buffer-2
        buffer (frame-selected-window (funcall pop-up-frame-function)))
       (setq display-buffer-made-new-frame t))