(follow-mode): Don't run hooks twice. Use `when'.
[bpt/emacs.git] / lisp / window.el
index 4d02390..26d1bdc 100644 (file)
@@ -1,7 +1,7 @@
 ;;; window.el --- GNU Emacs window commands aside from those written in C
 
 ;; Copyright (C) 1985, 1989, 1992, 1993, 1994, 2000, 2001, 2002,
-;;   2003, 2004, 2005, 2006 Free Software Foundation, Inc.
+;;   2003, 2004, 2005, 2006, 2007 Free Software Foundation, Inc.
 
 ;; Maintainer: FSF
 ;; Keywords: internal
@@ -10,7 +10,7 @@
 
 ;; GNU Emacs 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 2, or (at your option)
+;; the Free Software Foundation; either version 3, or (at your option)
 ;; any later version.
 
 ;; GNU Emacs is distributed in the hope that it will be useful,
@@ -57,15 +57,15 @@ BODY remains selected."
         ;; select-window changes frame-selected-window for whatever
         ;; frame that window is in.
         (save-selected-window-alist
-         (mapcar (lambda (frame) (list frame (frame-selected-window frame)))
+         (mapcar (lambda (frame) (cons frame (frame-selected-window frame)))
                  (frame-list))))
      (save-current-buffer
        (unwind-protect
           (progn ,@body)
         (dolist (elt save-selected-window-alist)
           (and (frame-live-p (car elt))
-               (window-live-p (cadr elt))
-               (set-frame-selected-window (car elt) (cadr elt))))
+               (window-live-p (cdr elt))
+               (set-frame-selected-window (car elt) (cdr elt))))
         (if (window-live-p save-selected-window-window)
             (select-window save-selected-window-window))))))
 
@@ -120,7 +120,7 @@ bars (top, bottom, or nil)."
 PROC is called with a window as argument.
 
 Optional second arg MINIBUF t means count the minibuffer window even
-if not active.  MINIBUF nil or omitted means count the minibuffer iff
+if not active.  MINIBUF nil or omitted means count the minibuffer only if
 it is active.  MINIBUF neither t nor nil means not to count the
 minibuffer even if it is active.
 
@@ -165,7 +165,7 @@ value is returned.  If no window satisfies PREDICATE, DEFAULT is
 returned.
 
 Optional second arg MINIBUF t means count the minibuffer window even
-if not active.  MINIBUF nil or omitted means count the minibuffer iff
+if not active.  MINIBUF nil or omitted means count the minibuffer only if
 it is active.  MINIBUF neither t nor nil means not to count the
 minibuffer even if it is active.
 
@@ -246,9 +246,10 @@ then the smallest tree containing that window is returned."
                 (windowp window-or-frame))
       (error "Not a frame or window: %s" window-or-frame)))
   (let ((subtree (bw-find-tree-sub window-or-frame)))
-    (if (integerp subtree)
-        nil
-      (bw-get-tree-1 subtree))))
+    (when subtree
+      (if (integerp subtree)
+         nil
+       (bw-get-tree-1 subtree)))))
 
 (defun bw-get-tree-1 (split)
   (if (windowp split)
@@ -395,11 +396,15 @@ subtree is balanced."
 (defun bw-adjust-window (window delta horizontal)
   "Wrapper around `adjust-window-trailing-edge' with error checking.
 Arguments WINDOW, DELTA and HORIZONTAL are passed on to that function."
-  (condition-case err
-      (adjust-window-trailing-edge window delta horizontal)
-    (error
-     ;;(message "adjust: %s" (error-message-string err))
-     )))
+  ;; `adjust-window-trailing-edge' may fail if delta is too large.
+  (while (>= (abs delta) 1)
+    (condition-case err
+        (progn
+          (adjust-window-trailing-edge window delta horizontal)
+          (setq delta 0))
+      (error
+       ;;(message "adjust: %s" (error-message-string err))
+       (setq delta (/ delta 2))))))
 
 (defun bw-balance-sub (wt w h)
   (setq wt (bw-refresh-edges wt))
@@ -422,6 +427,99 @@ Arguments WINDOW, DELTA and HORIZONTAL are passed on to that function."
       (dolist (c childs)
           (bw-balance-sub c cw ch)))))
 
+;;; A different solution to balance-windows
+
+(defun window-fixed-size-p (&optional window direction)
+  "Non-nil if WINDOW cannot be resized in DIRECTION.
+DIRECTION can be nil (i.e. any), `height' or `width'."
+  (with-current-buffer (window-buffer window)
+    (let ((fixed (and (boundp 'window-size-fixed) window-size-fixed)))
+      (when fixed
+       (not (and direction
+                 (member (cons direction window-size-fixed)
+                         '((height . width) (width . height)))))))))
+
+(defvar window-area-factor 1
+  "Factor by which the window area should be over-estimated.
+This is used by `balance-windows-area'.
+Changing this globally has no effect.")
+
+(defun balance-windows-area ()
+  "Make all visible windows the same area (approximately).
+See also `window-area-factor' to change the relative size of specific buffers."
+  (interactive)
+  (let* ((unchanged 0) (carry 0) (round 0)
+         ;; Remove fixed-size windows.
+         (wins (delq nil (mapcar (lambda (win)
+                                   (if (not (window-fixed-size-p win)) win))
+                                 (window-list nil 'nomini))))
+         (changelog nil)
+         next)
+    ;; Resizing a window changes the size of surrounding windows in complex
+    ;; ways, so it's difficult to balance them all.  The introduction of
+    ;; `adjust-window-trailing-edge' made it a bit easier, but it is still
+    ;; very difficult to do.  `balance-window' above takes an off-line
+    ;; approach: get the whole window tree, then balance it, then try to
+    ;; adjust the windows so they fit the result.
+    ;; Here, instead, we take a "local optimization" approach, where we just
+    ;; go through all the windows several times until nothing needs to be
+    ;; changed.  The main problem with this approach is that it's difficult
+    ;; to make sure it terminates, so we use some heuristic to try and break
+    ;; off infinite loops.
+    ;; After a round without any change, we allow a second, to give a chance
+    ;; to the carry to propagate a minor imbalance from the end back to
+    ;; the beginning.
+    (while (< unchanged 2)
+      ;; (message "New round")
+      (setq unchanged (1+ unchanged) round (1+ round))
+      (dolist (win wins)
+        (setq next win)
+        (while (progn (setq next (next-window next))
+                      (window-fixed-size-p next)))
+        ;; (assert (eq next (or (cadr (member win wins)) (car wins))))
+        (let* ((horiz
+                (< (car (window-edges win)) (car (window-edges next))))
+               (areadiff (/ (- (* (window-height next) (window-width next)
+                                  (buffer-local-value 'window-area-factor
+                                                      (window-buffer next)))
+                               (* (window-height win) (window-width win)
+                                  (buffer-local-value 'window-area-factor
+                                                      (window-buffer win))))
+                            (max (buffer-local-value 'window-area-factor
+                                                     (window-buffer win))
+                                 (buffer-local-value 'window-area-factor
+                                                     (window-buffer next)))))
+               (edgesize (if horiz
+                             (+ (window-height win) (window-height next))
+                           (+ (window-width win) (window-width next))))
+               (diff (/ areadiff edgesize)))
+          (when (zerop diff)
+            ;; Maybe diff is actually closer to 1 than to 0.
+            (setq diff (/ (* 3 areadiff) (* 2 edgesize))))
+          (when (and (zerop diff) (not (zerop areadiff)))
+            (setq diff (/ (+ areadiff carry) edgesize))
+            ;; Change things smoothly.
+            (if (or (> diff 1) (< diff -1)) (setq diff (/ diff 2))))
+          (if (zerop diff)
+              ;; Make sure negligible differences don't accumulate to
+              ;; become significant.
+              (setq carry (+ carry areadiff))
+            (bw-adjust-window win diff horiz)
+            ;; (sit-for 0.5)
+            (let ((change (cons win (window-edges win))))
+              ;; If the same change has been seen already for this window,
+              ;; we're most likely in an endless loop, so don't count it as
+              ;; a change.
+              (unless (member change changelog)
+                (push change changelog)
+                (setq unchanged 0 carry 0)))))))
+    ;; We've now basically balanced all the windows.
+    ;; But there may be some minor off-by-one imbalance left over,
+    ;; so let's do some fine tuning.
+    ;; (bw-finetune wins)
+    ;; (message "Done in %d rounds" round)
+    ))
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 \f
 ;; I think this should be the default; I think people will prefer it--rms.
@@ -532,7 +630,11 @@ the height exactly, but attempts to be conservative, by allocating more
 lines than are actually needed in the case where some error may be present."
   (let ((delta (- height (window-text-height window))))
     (unless (zerop delta)
-      (let ((window-min-height 1))
+      ;; Setting window-min-height to a value like 1 can lead to very
+      ;; bizarre displays because it also allows Emacs to make *other*
+      ;; windows 1-line tall, which means that there's no more space for
+      ;; the modeline.
+      (let ((window-min-height (min 2 height))) ;One text line plus a modeline.
        (if (and window (not (eq window (selected-window))))
            (save-selected-window
              (select-window window)
@@ -597,7 +699,7 @@ in some window."
         (1+ (vertical-motion (buffer-size) window))))))
 
 (defun fit-window-to-buffer (&optional window max-height min-height)
-  "Make WINDOW the right size to display its contents exactly.
+  "Make WINDOW the right height to display its contents exactly.
 If WINDOW is omitted or nil, it defaults to the selected window.
 If the optional argument MAX-HEIGHT is supplied, it is the maximum height
   the window is allowed to be, defaulting to the frame height.
@@ -640,10 +742,7 @@ header-line."
          ;; desired-height lines, constrained by MIN-HEIGHT and MAX-HEIGHT.
          (- (max (min desired-height max-height)
                  (or min-height window-min-height))
-            window-height))
-        ;; We do our own height checking, so avoid any restrictions due to
-        ;; window-min-height.
-        (window-min-height 1))
+            window-height)))
 
     ;; Don't try to redisplay with the cursor at the end
     ;; on its own line--that would force a scroll and spoil things.
@@ -714,17 +813,25 @@ or if the window is the only window of its frame."
   "Kill the current buffer and delete the selected window."
   (interactive)
   (let ((window-to-delete (selected-window))
+       (buffer-to-kill (current-buffer))
        (delete-window-hook (lambda ()
                              (condition-case nil
                                  (delete-window)
                                (error nil)))))
-    (add-hook 'kill-buffer-hook delete-window-hook t t)
-    (if (kill-buffer (current-buffer))
-       ;; If `delete-window' failed before, we rerun it to regenerate
-       ;; the error so it can be seen in the minibuffer.
-       (when (eq (selected-window) window-to-delete)
-         (delete-window))
-      (remove-hook 'kill-buffer-hook delete-window-hook t))))
+    (unwind-protect
+       (progn
+         (add-hook 'kill-buffer-hook delete-window-hook t t)
+         (if (kill-buffer (current-buffer))
+             ;; If `delete-window' failed before, we rerun it to regenerate
+             ;; the error so it can be seen in the echo area.
+             (when (eq (selected-window) window-to-delete)
+               (delete-window))))
+      ;; If the buffer is not dead for some reason (probably because
+      ;; of a `quit' signal), remove the hook again.
+      (condition-case nil
+         (with-current-buffer buffer-to-kill
+           (remove-hook 'kill-buffer-hook delete-window-hook t))
+       (error nil)))))
 
 (defun quit-window (&optional kill window)
   "Quit the current buffer.  Bury it, and maybe delete the selected frame.
@@ -772,21 +879,144 @@ and the buffer that is killed or buried is the one in that window."
     ;; Maybe get rid of the window.
     (and window (not window-handled) (not window-solitary)
         (delete-window window))))
+\f
+(defvar mouse-autoselect-window-timer nil
+  "Timer used by delayed window autoselection.")
+
+(defvar mouse-autoselect-window-position nil
+  "Last mouse position recorded by delayed window autoselection.")
+
+(defvar mouse-autoselect-window-window nil
+  "Last window recorded by delayed window autoselection.")
+
+(defvar mouse-autoselect-window-state nil
+  "When non-nil, special state of delayed window autoselection.
+Possible values are `suspend' \(suspend autoselection after a menu or
+scrollbar interaction\) and `select' \(the next invocation of
+'handle-select-window' shall select the window immediately\).")
+
+(defun mouse-autoselect-window-cancel (&optional force)
+  "Cancel delayed window autoselection.
+Optional argument FORCE means cancel unconditionally."
+  (unless (and (not force)
+              ;; Don't cancel while the user drags a scroll bar.
+              (eq this-command 'scroll-bar-toolkit-scroll)
+              (memq (nth 4 (event-end last-input-event))
+                    '(handle end-scroll)))
+    (setq mouse-autoselect-window-state nil)
+    (when (timerp mouse-autoselect-window-timer)
+      (cancel-timer mouse-autoselect-window-timer))
+    (remove-hook 'pre-command-hook 'mouse-autoselect-window-cancel)))
+
+(defun mouse-autoselect-window-start (mouse-position &optional window suspend)
+  "Start delayed window autoselection.
+MOUSE-POSITION is the last position where the mouse was seen as returned
+by `mouse-position'.  Optional argument WINDOW non-nil denotes the
+window where the mouse was seen.  Optional argument SUSPEND non-nil
+means suspend autoselection."
+  ;; Record values for MOUSE-POSITION, WINDOW, and SUSPEND.
+  (setq mouse-autoselect-window-position mouse-position)
+  (when window (setq mouse-autoselect-window-window window))
+  (setq mouse-autoselect-window-state (when suspend 'suspend))
+  ;; Install timer which runs `mouse-autoselect-window-select' after
+  ;; `mouse-autoselect-window' seconds.
+  (setq mouse-autoselect-window-timer
+       (run-at-time
+        (abs mouse-autoselect-window) nil 'mouse-autoselect-window-select)))
+
+(defun mouse-autoselect-window-select ()
+  "Select window with delayed window autoselection.
+If the mouse position has stabilized in a non-selected window, select
+that window.  The minibuffer window is selected only if the minibuffer is
+active.  This function is run by `mouse-autoselect-window-timer'."
+  (condition-case nil
+      (let* ((mouse-position (mouse-position))
+            (window
+             (condition-case nil
+                 (window-at (cadr mouse-position) (cddr mouse-position)
+                            (car mouse-position))
+               (error nil))))
+       (cond
+        ((or (menu-or-popup-active-p)
+             (and window
+                  (not (coordinates-in-window-p (cdr mouse-position) window))))
+         ;; A menu / popup dialog is active or the mouse is on the scroll-bar
+         ;; of WINDOW, temporarily suspend delayed autoselection.
+         (mouse-autoselect-window-start mouse-position nil t))
+        ((eq mouse-autoselect-window-state 'suspend)
+         ;; Delayed autoselection was temporarily suspended, reenable it.
+         (mouse-autoselect-window-start mouse-position))
+        ((and window (not (eq window (selected-window)))
+              (or (not (numberp mouse-autoselect-window))
+                  (and (> mouse-autoselect-window 0)
+                       ;; If `mouse-autoselect-window' is positive, select
+                       ;; window if the window is the same as before.
+                       (eq window mouse-autoselect-window-window))
+                  ;; Otherwise select window if the mouse is at the same
+                  ;; position as before.  Observe that the first test after
+                  ;; starting autoselection usually fails since the value of
+                  ;; `mouse-autoselect-window-position' recorded there is the
+                  ;; position where the mouse has entered the new window and
+                  ;; not necessarily where the mouse has stopped moving.
+                  (equal mouse-position mouse-autoselect-window-position))
+              ;; The minibuffer is a candidate window if it's active.
+              (or (not (window-minibuffer-p window))
+                  (eq window (active-minibuffer-window))))
+         ;; Mouse position has stabilized in non-selected window: Cancel
+         ;; delayed autoselection and try to select that window.
+         (mouse-autoselect-window-cancel t)
+         ;; Select window where mouse appears unless the selected window is the
+         ;; minibuffer.  Use `unread-command-events' in order to execute pre-
+         ;; and post-command hooks and trigger idle timers.  To avoid delaying
+         ;; autoselection again, set `mouse-autoselect-window-state'."
+         (unless (window-minibuffer-p (selected-window))
+           (setq mouse-autoselect-window-state 'select)
+           (setq unread-command-events
+                 (cons (list 'select-window (list window))
+                       unread-command-events))))
+        ((or (and window (eq window (selected-window)))
+             (not (numberp mouse-autoselect-window))
+             (equal mouse-position mouse-autoselect-window-position))
+         ;; Mouse position has either stabilized in the selected window or at
+         ;; `mouse-autoselect-window-position': Cancel delayed autoselection.
+         (mouse-autoselect-window-cancel t))
+        (t
+         ;; Mouse position has not stabilized yet, resume delayed
+         ;; autoselection.
+         (mouse-autoselect-window-start mouse-position window))))
+    (error nil)))
 
 (defun handle-select-window (event)
   "Handle select-window events."
   (interactive "e")
   (let ((window (posn-window (event-start event))))
-    (if (and (window-live-p window)
-            ;; Don't switch if we're currently in the minibuffer.
-            ;; This tries to work around problems where the minibuffer gets
-            ;; unselected unexpectedly, and where you then have to move
-            ;; your mouse all the way down to the minibuffer to select it.
-            (not (window-minibuffer-p (selected-window)))
-            ;; Don't switch to a minibuffer window unless it's active.
-            (or (not (window-minibuffer-p window))
-                (minibuffer-window-active-p window)))
-       (select-window window))))
+    (when (and (window-live-p window)
+              ;; Don't switch if we're currently in the minibuffer.
+              ;; This tries to work around problems where the minibuffer gets
+              ;; unselected unexpectedly, and where you then have to move
+              ;; your mouse all the way down to the minibuffer to select it.
+              (not (window-minibuffer-p (selected-window)))
+              ;; Don't switch to a minibuffer window unless it's active.
+              (or (not (window-minibuffer-p window))
+                  (minibuffer-window-active-p window)))
+      (unless (and (numberp mouse-autoselect-window)
+                  (not (zerop mouse-autoselect-window))
+                  (not (eq mouse-autoselect-window-state 'select))
+                  (progn
+                    ;; Cancel any delayed autoselection.
+                    (mouse-autoselect-window-cancel t)
+                    ;; Start delayed autoselection from current mouse position
+                    ;; and window.
+                    (mouse-autoselect-window-start (mouse-position) window)
+                    ;; Executing a command cancels delayed autoselection.
+                    (add-hook
+                     'pre-command-hook 'mouse-autoselect-window-cancel)))
+       ;; Reset state of delayed autoselection.
+       (setq mouse-autoselect-window-state nil)
+       (when mouse-autoselect-window
+         ;; Run `mouse-leave-buffer-hook' when autoselecting window.
+         (run-hooks 'mouse-leave-buffer-hook))
+       (select-window window)))))
 
 (define-key ctl-x-map "2" 'split-window-vertically)
 (define-key ctl-x-map "3" 'split-window-horizontally)