From b3dd0ce75ba9314eb7a682e5fcf8b4cfbc67655b Mon Sep 17 00:00:00 2001 From: Martin Rudalics Date: Thu, 10 Jun 2021 09:14:21 +0200 Subject: [PATCH] Provide new option `delete-window-set-selected' (Bug#47300) 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. --- doc/lispref/windows.texi | 47 +++++++----- etc/NEWS | 11 +++ lisp/window.el | 157 +++++++++++++++++++++++++++++++-------- src/window.c | 25 +------ 4 files changed, 171 insertions(+), 69 deletions(-) diff --git a/doc/lispref/windows.texi b/doc/lispref/windows.texi index 44656c057a3..bcb492d68f7 100644 --- a/doc/lispref/windows.texi +++ b/doc/lispref/windows.texi @@ -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 @@ -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}. @@ -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 diff --git a/etc/NEWS b/etc/NEWS index 3075fa0cd4e..06c6efd995a 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -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 diff --git a/lisp/window.el b/lisp/window.el index fd1c617d6be..e7551f30c32 100644 --- a/lisp/window.el +++ b/lisp/window.el @@ -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: @@ -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))) @@ -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: @@ -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: @@ -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) @@ -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. @@ -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)) @@ -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)) @@ -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)))) diff --git a/src/window.c b/src/window.c index 2d98ae5f156..db324effcce 100644 --- a/src/window.c +++ b/src/window.c @@ -5148,15 +5148,13 @@ 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 @@ -5164,24 +5162,9 @@ Signal an error when WINDOW is the only window on its frame. */) 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 -- 2.39.5