Skip to content

Commit

Permalink
Provide new option `delete-window-set-selected' (Bug#47300)
Browse files Browse the repository at this point in the history
When `delete-window' deletes its frame's selected window, this new
option allows to choose another window as replacement.

* lisp/window.el (get-lru-window, get-mru-window)
(get-largest-window): New optional argument NO-OTHER.
(window-at-pos): New function.
(delete-window-set-selected): New option.
(delete-window): Handle `delete-window-set-selected'.
* src/window.c (Fdelete_window_internal): Set the selected
window of WINDOW's frame to the first window on that frame and
let `delete-window' choose a more suitable window instead.
* doc/lispref/windows.texi (Deleting Windows): Describe new
option `delete-window-set-selected'.
(Cyclic Window Ordering): Describe new NO-OTHER argument for
`get-lru-window', `get-mru-window' and `get-largest-window'.
* etc/NEWS: Mention `delete-window-set-selected' and the NO-OTHER
argument.
  • Loading branch information
Martin Rudalics committed Jun 10, 2021
1 parent dd9385b commit b3dd0ce
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 69 deletions.
47 changes: 28 additions & 19 deletions doc/lispref/windows.texi
Expand Up @@ -1318,6 +1318,23 @@ lieu of the usual action of @code{delete-window}. @xref{Window
Parameters}.
@end deffn

When @code{delete-window} deletes the selected window of its frame, it
has to make another window the new selected window of that frame. The
following option allows to choose which window gets selected instead.

@defopt delete-window-set-selected
This option allows to specify which window should become a frame's
selected window after @code{delete-window} has deleted the previously
selected one.

Possible choices are @code{mru} (the default) to select the most
recently used window on that frame and @code{pos} to choose the window
at the position of point of the previously selected window. If this
option is @code{nil}, it means to choose the frame's first window
instead. Note that a window with a non-@code{nil}
@code{no-other-window} parameter is never chosen.
@end defopt

@deffn Command delete-other-windows &optional window
This function makes @var{window} fill its frame, deleting other
windows as necessary. If @var{window} is omitted or @code{nil}, it
Expand Down Expand Up @@ -2007,7 +2024,7 @@ meaning as for @code{next-window}.
criterion, without selecting it:

@cindex least recently used window
@defun get-lru-window &optional all-frames dedicated not-selected
@defun get-lru-window &optional all-frames dedicated not-selected no-other
This function returns a live window which is heuristically the least
recently used. The optional argument @var{all-frames} has
the same meaning as in @code{next-window}.
Expand All @@ -2018,33 +2035,25 @@ window (@pxref{Dedicated Windows}) is never a candidate unless the
optional argument @var{dedicated} is non-@code{nil}. The selected
window is never returned, unless it is the only candidate. However, if
the optional argument @var{not-selected} is non-@code{nil}, this
function returns @code{nil} in that case.
function returns @code{nil} in that case. The optional argument
@var{no-other}, if non-@code{nil}, means to never return a window whose
@code{no-other-window} parameter is non-@code{nil}.
@end defun

@cindex most recently used window
@defun get-mru-window &optional all-frames dedicated not-selected
@defun get-mru-window &optional all-frames dedicated not-selected no-other
This function is like @code{get-lru-window}, but it returns the most
recently used window instead. The meaning of the arguments is the
same as described for @code{get-lru-window}.
same as for @code{get-lru-window}.
@end defun

@cindex largest window
@defun get-largest-window &optional all-frames dedicated not-selected
@defun get-largest-window &optional all-frames dedicated not-selected no-other
This function returns the window with the largest area (height times
width). The optional argument @var{all-frames} specifies the windows to
search, and has the same meaning as in @code{next-window}.

A minibuffer window is never a candidate. A dedicated window
(@pxref{Dedicated Windows}) is never a candidate unless the optional
argument @var{dedicated} is non-@code{nil}. The selected window is not
a candidate if the optional argument @var{not-selected} is
non-@code{nil}. If the optional argument @var{not-selected} is
non-@code{nil} and the selected window is the only candidate, this
function returns @code{nil}.

If there are two candidate windows of the same size, this function
prefers the one that comes first in the cyclic ordering of windows,
starting from the selected window.
width). If there are two candidate windows of the same size, it prefers
the one that comes first in the cyclic ordering of windows, starting
from the selected window. The meaning of the arguments is the same as
for @code{get-lru-window}.
@end defun

@cindex window that satisfies a predicate
Expand Down
11 changes: 11 additions & 0 deletions etc/NEWS
Expand Up @@ -604,6 +604,17 @@ These options include 'windmove-default-keybindings',

** Windows

+++
*** New option 'delete-window-set-selected'.
This allows to choose a frame's selected window after deleting the
previously selected one.

+++
*** New argument NO-OTHER for some window functions.
'get-lru-window', ‘get-mru-window’ and 'get-largest-window' now accept a
new optional argument NO-OTHER which if non-nil avoids returning a
window whose 'no-other-window' parameter is non-nil.

+++
*** New 'display-buffer' function 'display-buffer-use-least-recent-window'.
This is like 'display-buffer-use-some-window', but won't reuse the
Expand Down
157 changes: 128 additions & 29 deletions lisp/window.el
Expand Up @@ -2499,14 +2499,16 @@ and no others."

(defalias 'some-window 'get-window-with-predicate)

(defun get-lru-window (&optional all-frames dedicated not-selected)
(defun get-lru-window (&optional all-frames dedicated not-selected no-other)
"Return the least recently used window on frames specified by ALL-FRAMES.
Return a full-width window if possible. A minibuffer window is
never a candidate. A dedicated window is never a candidate
unless DEDICATED is non-nil, so if all windows are dedicated, the
value is nil. Avoid returning the selected window if possible.
Optional argument NOT-SELECTED non-nil means never return the
selected window.
selected window. Optional argument NO-OTHER non-nil means to
never return a window whose 'no-other-window' parameter is
non-nil.

The following non-nil values of the optional argument ALL-FRAMES
have special meanings:
Expand All @@ -2526,7 +2528,9 @@ selected frame and no others."
(let (best-window best-time second-best-window second-best-time time)
(dolist (window (window-list-1 nil 'nomini all-frames))
(when (and (or dedicated (not (window-dedicated-p window)))
(or (not not-selected) (not (eq window (selected-window)))))
(or (not not-selected) (not (eq window (selected-window))))
(or (not no-other)
(not (window-parameter window 'no-other-window))))
(setq time (window-use-time window))
(if (or (eq window (selected-window))
(not (window-full-width-p window)))
Expand All @@ -2538,12 +2542,14 @@ selected frame and no others."
(setq best-window window)))))
(or best-window second-best-window)))

(defun get-mru-window (&optional all-frames dedicated not-selected)
(defun get-mru-window (&optional all-frames dedicated not-selected no-other)
"Return the most recently used window on frames specified by ALL-FRAMES.
A minibuffer window is never a candidate. A dedicated window is
never a candidate unless DEDICATED is non-nil, so if all windows
are dedicated, the value is nil. Optional argument NOT-SELECTED
non-nil means never return the selected window.
non-nil means never return the selected window. Optional
argument NO-OTHER non-nil means to never return a window whose
'no-other-window' parameter is non-nil.

The following non-nil values of the optional argument ALL-FRAMES
have special meanings:
Expand All @@ -2565,17 +2571,21 @@ selected frame and no others."
(setq time (window-use-time window))
(when (and (or dedicated (not (window-dedicated-p window)))
(or (not not-selected) (not (eq window (selected-window))))
(or (not best-time) (> time best-time)))
(or (not no-other)
(not (window-parameter window 'no-other-window)))
(or (not best-time) (> time best-time)))
(setq best-time time)
(setq best-window window)))
best-window))

(defun get-largest-window (&optional all-frames dedicated not-selected)
(defun get-largest-window (&optional all-frames dedicated not-selected no-other)
"Return the largest window on frames specified by ALL-FRAMES.
A minibuffer window is never a candidate. A dedicated window is
never a candidate unless DEDICATED is non-nil, so if all windows
are dedicated, the value is nil. Optional argument NOT-SELECTED
non-nil means never return the selected window.
non-nil means never return the selected window. Optional
argument NO-OTHER non-nil means to never return a window whose
'no-other-window' parameter is non-nil.

The following non-nil values of the optional argument ALL-FRAMES
have special meanings:
Expand All @@ -2596,7 +2606,9 @@ selected frame and no others."
best-window size)
(dolist (window (window-list-1 nil 'nomini all-frames))
(when (and (or dedicated (not (window-dedicated-p window)))
(or (not not-selected) (not (eq window (selected-window)))))
(or (not not-selected) (not (eq window (selected-window))))
(or (not no-other)
(not (window-parameter window 'no-other-window))))
(setq size (* (window-pixel-height window)
(window-pixel-width window)))
(when (> size best-size)
Expand Down Expand Up @@ -4130,18 +4142,53 @@ frame can be safely deleted."
;; of its frame.
t))))

(defun window--in-subtree-p (window root)
"Return t if WINDOW is either ROOT or a member of ROOT's subtree."
(or (eq window root)
(let ((parent (window-parent window)))
(catch 'done
(while parent
(if (eq parent root)
(throw 'done t)
(setq parent (window-parent parent))))))))
(defun window-at-pos (x y &optional frame no-other)
"Return live window at coordinates X, Y on specified FRAME.
X and Y are counted in pixels from an origin at 0, 0 of FRAME's
native frame. A coordinate on an edge shared by two windows is
attributed to the window on the right (or below). Return nil if
no such window can be found.

Optional argument FRAME must specify a live frame and defaults to
the selected one. Optional argument NO-OTHER non-nil means to
not return a window with a non-nil 'no-other-window' parameter."
(setq frame (window-normalize-frame frame))
(let* ((root-edges (window-edges (frame-root-window frame) nil nil t))
(root-left (nth 2 root-edges))
(root-bottom (nth 3 root-edges)))
(catch 'window
(walk-window-tree
(lambda (window)
(let ((edges (window-edges window nil nil t)))
(when (and (>= x (nth 0 edges))
(or (< x (nth 2 edges)) (= x root-left))
(>= y (nth 1 edges))
(or (< y (nth 3 edges)) (= y root-bottom)))
(if (and no-other (window-parameter window 'no-other-window))
(throw 'window nil)
(throw 'window window)))))
frame))))

(defcustom delete-window-set-selected 'mru
"How to choose a frame's selected window after window deletion.
When a frame's selected window gets deleted, Emacs has to choose
another live window on that frame to serve as its selected
window. This option allows to control which window gets selected
instead.

The possible choices are 'mru' (the default) to select the most
recently used window on that frame and 'pos' to choose the window
at the position of point of the previously selected window. If
this is nil, choose the frame's first window instead. A window
with a non-nil 'no-other-window' parameter is never chosen."
:type '(choice (const :tag "Most recently used" mru)
(const :tag "At position of deleted" pos)
(const :tag "Frame's first " nil))
:group 'windows
:version "28.1")

(defun delete-window (&optional window)
"Delete WINDOW.
"Delete specified WINDOW.
WINDOW must be a valid window and defaults to the selected one.
Return nil.

Expand All @@ -4156,7 +4203,11 @@ Otherwise, if WINDOW is part of an atomic window, call
`delete-window' with the root of the atomic window as its
argument. Signal an error if WINDOW is either the only window on
its frame, the last non-side window, or part of an atomic window
that is its frame's root window."
that is its frame's root window.

If WINDOW is the selected window on its frame, choose some other
window as that frame's selected window according to the value of
the option `delete-window-set-selected'."
(interactive)
(setq window (window-normalize-window window))
(let* ((frame (window-frame window))
Expand Down Expand Up @@ -4191,11 +4242,11 @@ that is its frame's root window."
(window-combination-resize
(or window-combination-resize
(window-parameter parent 'window-side)))
(frame-selected
(window--in-subtree-p (frame-selected-window frame) window))
(frame-selected-window (frame-selected-window frame))
;; Emacs 23 preferably gives WINDOW's space to its left
;; sibling.
(sibling (or (window-left window) (window-right window))))
(sibling (or (window-left window) (window-right window)))
frame-selected-window-edges frame-selected-window-pos)
(window--resize-reset frame horizontal)
(cond
((and (not (eq window-combination-resize t))
Expand All @@ -4211,15 +4262,63 @@ that is its frame's root window."
(t
;; Can't do without resizing fixed-size windows.
(window--resize-siblings window (- size) horizontal t)))

(when (eq delete-window-set-selected 'pos)
;; Remember edges and position of point of the selected window
;; of WINDOW'S frame.
(setq frame-selected-window-edges
(window-edges frame-selected-window nil nil t))
(setq frame-selected-window-pos
(nth 2 (posn-at-point nil frame-selected-window))))

;; Actually delete WINDOW.
(delete-window-internal window)
(window--pixel-to-total frame horizontal)
(when (and frame-selected
(window-parameter
(frame-selected-window frame) 'no-other-window))
;; `delete-window-internal' has selected a window that should
;; not be selected, fix this here.
(other-window -1 frame))

;; If we deleted the selected window of WINDOW's frame, choose
;; another one based on `delete-window-set-selected'. Note
;; that both `window-at-pos' and `get-mru-window' may fail to
;; produce a suitable window in which case we will fall back on
;; its frame's first window, chosen by `delete-window-internal'.
(cond
((window-live-p frame-selected-window))
((and frame-selected-window-pos
;; We have a recorded position of point of the previously
;; selected window. Try to find the window that is now
;; at that position.
(let ((new-frame-selected-window
(window-at-pos
(+ (nth 0 frame-selected-window-edges)
(car frame-selected-window-pos))
(+ (nth 1 frame-selected-window-edges)
(cdr frame-selected-window-pos))
frame t)))
(and new-frame-selected-window
;; Select window at WINDOW's position at point.
(set-frame-selected-window
frame new-frame-selected-window)))))
((and (eq delete-window-set-selected 'mru)
;; Try to use the most recently used window.
(let ((mru-window (get-mru-window frame nil nil t)))
(and mru-window
(set-frame-selected-window frame mru-window)))))
((and (window-parameter
(frame-selected-window frame) 'no-other-window)
;; If `delete-window-internal' selected a window with a
;; non-nil 'no-other-window' parameter as its frame's
;; selected window, try to choose another one.
(catch 'found
(walk-window-tree
(lambda (other)
(unless (window-parameter other 'no-other-window)
(set-frame-selected-window frame other)
(throw 'found t)))
frame))))
(t
;; Record the window chosen by `delete-window-internal'.
(set-frame-selected-window
frame (frame-selected-window frame))))

(window--check frame)
;; Always return nil.
nil))))
Expand Down
25 changes: 4 additions & 21 deletions src/window.c
Expand Up @@ -5148,40 +5148,23 @@ Signal an error when WINDOW is the only window on its frame. */)
adjust_frame_glyphs (f);

if (!WINDOW_LIVE_P (FRAME_SELECTED_WINDOW (f)))
/* We deleted the frame's selected window. */
/* We apparently deleted the frame's selected window; use the
frame's first window as substitute but don't record it yet.
`delete-window' may have something better up its sleeves. */
{
/* Use the frame's first window as fallback ... */
Lisp_Object new_selected_window = Fframe_first_window (frame);
/* ... but preferably use its most recently used window. */
Lisp_Object mru_window;

/* `get-mru-window' might fail for some reason so play it safe
- promote the first window _without recording it_ first. */
if (EQ (FRAME_SELECTED_WINDOW (f), selected_window))
Fselect_window (new_selected_window, Qt);
else
/* Do not clear f->select_mini_window_flag here. If the
last selected window on F was an active minibuffer, we
want to return to it on a later Fselect_frame. */
fset_selected_window (f, new_selected_window);

unblock_input ();

/* Now look whether `get-mru-window' gets us something. */
mru_window = call1 (Qget_mru_window, frame);
if (WINDOW_LIVE_P (mru_window)
&& EQ (XWINDOW (mru_window)->frame, frame))
new_selected_window = mru_window;

/* If all ended up well, we now promote the mru window. */
if (EQ (FRAME_SELECTED_WINDOW (f), selected_window))
Fselect_window (new_selected_window, Qnil);
else
fset_selected_window (f, new_selected_window);
}
else
unblock_input ();

unblock_input ();
FRAME_WINDOW_CHANGE (f) = true;
}
else
Expand Down

0 comments on commit b3dd0ce

Please sign in to comment.