;;; centered-cursor-mode.el --- cursor stays vertically centered ;; Copyright (C) 2007 André Riemann ;; Author: André Riemann ;; Maintainer: André Riemann ;; Created: 2007-09-14 ;; Keywords: convenience ;; URL: http://www.emacswiki.org/cgi-bin/wiki/centered-cursor-mode.el ;; Compatibility: only tested with GNU Emacs 23.0 ;; Version: 0.5.2 ;; Last-Updated: 2009-08-31 ;; This file 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) ;; any later version. ;; This file is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to the Free ;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, ;; MA 02110-1301, USA. ;;; Commentary: ;; Makes the cursor stay vertically in a defined position (usually ;; centered). The vertical position can be altered, see key definition ;; below. ;; To load put that in .emacs: ;; (require 'centered-cursor-mode) ;; To activate do: ;; M-x centered-cursor-mode ;; for buffer local or ;; M-x global-centered-cursor-mode ;; for global minor mode. ;; Also possible: put that in .emacs ;; (and ;; (require 'centered-cursor-mode) ;; (global-centered-cursor-mode +1)) ;; to always have centered-cursor-mode on in all buffers. ;;; TODO: ;; - the code is a mess ;; - ccm-vpos-inverted doesn't work with ccm-vpos == 0, because first ;; position from top is 0 and from bottom -1 ;; - interactive first start isn't animated when calling global-... ;; because it starts the modes for each buffer and interactive-p fails ;; for that ;; - more bugs? ;;; Change Log: ;; 2009-08-31 andre-r ;; * replaced window-body-height with window-text-height ;; (partially visible lines are not counted in window-text-height) ;; * bug fixed in ccm-vpos-recenter ;; (some parentheses where wrong after the last update) ;; 2009-02-23 andre-r ;; * some simplifications ;; 2009-02-22 andre-r ;; * some tips from Drew Adams: ;; - new local variable coding:utf-8 ;; - made recenter-sequence a defvar ;; - added groups scrolling and convenience ;; - replaced mouse-4 and mouse-5 with ;; mouse-wheel-up-event and mouse-wheel-down-event ;; - added scroll-bar-toolkit-scroll to ccm-ignored-commands ;; - made ccm-ignored-commands customisable ;; * removed a bug where it didn't work with more than one window ;; displaying the same buffer ;; * added function for page up and down scrolling ;; (standard ones didn't work well with this mode) ;; * made the animation delay customisable ;; * made the initial vertical position customisable ;; * made the behaviour at the end of the file customisable ;; 2008-02-02 andre-r ;; * fixed bug that led to wrong-type-argument ;; when opening a new buffer ;; * some other minor stuff ;; 2007-09-24 andre-r ;; * added global minor mode ;; 2007-09-21 andre-r ;; * not recentering at end of buffer ;; * defvar animate-first-start-p ;; 2007-09-14 andre-r ;; * inital release ;; This file is *NOT* part of GNU Emacs. ;;; Code: (defgroup centered-cursor nil "Makes the cursor stay vertically in a defined position (usually centered). Instead the cursor the text moves around the cursor." :group 'scrolling :group 'convenience :link '(emacs-library-link :tag "Source Lisp File" "centered-cursor-mode.el") :link '(url-link "http://www.emacswiki.org/cgi-bin/wiki/centered-cursor-mode.el")) (defcustom ccm-step-size 2 "Step size when animated recentering." :group 'centered-cursor :tag "Animation step size" :type 'integer) (defcustom ccm-step-delay 0.02 "Delay between animation steps. If you want a different animation speed." :group 'centered-cursor :tag "Animation step delay" :type 'number) (defcustom ccm-ignored-commands '(mouse-drag-region mouse-set-point widget-button-click scroll-bar-toolkit-scroll) "After these commands recentering is ignored. This is to prevent unintentional jumping (especially when mouse clicking). Following commands (except the ignored ones) will cause an animated recentering to give a feedback and not just jumping to the center." :group 'centered-cursor :tag "Ignored commands" :type '(repeat (symbol :tag "Command"))) (defcustom ccm-vpos-init '(round (window-text-height) 2) "This is the screen line position where the cursor initially stays." :group 'centered-cursor :tag "Vertical cursor position" :type '(choice (const :tag "Center" (round (window-text-height) 2)) (const :tag "Golden ratio" (round (* 21 (window-text-height)) 34)) (integer :tag "Lines from top" :value 10))) (make-variable-buffer-local 'ccm-vpos-init) (defcustom ccm-vpos-inverted 1 "Inverted vertical cursor position. Defines if the initial vertical position `ccm-vpos-init' is measured from the bottom instead from the top." :group 'centered-cursor :tag "Inverted cursor position" :type '(choice (const :tag "Inverted" -1) (const :tag "Not inverted" 1))) (make-variable-buffer-local 'ccm-vpos-inverted) (defcustom ccm-recenter-at-end-of-file nil "Recenter at the end of the file. If non-nil the end of the file is recentered. If nil the end of the file stays at the end of the window." :group 'centered-cursor :tag "Recenter at EOF" :type '(choice (const :tag "Don't recenter at the end of the file" nil) (const :tag "Recenter at the end of the file" t))) (make-variable-buffer-local 'ccm-recenter-end-of-file) (defvar ccm-vpos nil "This is the screen line position where the cursor stays.") (make-variable-buffer-local 'ccm-vpos) (defvar animate-first-start-p nil "Whether or not to animate at first start. It is set to nil, if centered-cursor-mode is called non-interactively.") (make-variable-buffer-local 'animate-first-start-p) (defvar recenter-sequence nil "Before animated recentering a list is generated first with positions to successively recenter to") (make-variable-buffer-local 'recenter-sequence) (defvar ccm-map (let ((ccm-map (make-sparse-keymap))) (define-key ccm-map [(control meta -)] 'ccm-vpos-up) (define-key ccm-map [(control meta +)] 'ccm-vpos-down) (define-key ccm-map [(control meta =)] 'ccm-vpos-down) (define-key ccm-map [(control meta ?0)] 'ccm-vpos-recenter) (when (bound-and-true-p mouse-wheel-mode) (mapc (lambda (key) (define-key ccm-map key 'ccm-mwheel-scroll)) (list (vector mouse-wheel-up-event) (vector mouse-wheel-down-event) (vector (list 'control mouse-wheel-up-event)) (vector (list 'control mouse-wheel-down-event)) (vector (list 'shift mouse-wheel-up-event)) (vector (list 'shift mouse-wheel-down-event))))) (define-key ccm-map [(meta v)] 'ccm-scroll-down) (define-key ccm-map [(control v)] 'ccm-scroll-up) (define-key ccm-map [prior] 'ccm-scroll-down) (define-key ccm-map [next] 'ccm-scroll-up) ccm-map) "Keymap used in centered-cursor-mode.") (defun ccm-mwheel-scroll (event) "Very similar to `mwheel-scroll', but does not use `scroll-down' and `scroll-up' but `previous-line' and `next-line', that is, the cursor is moved and thus the text in the window is scrolled due to `recenter'. The customizable variable `mouse-wheel-scroll-amount' is used to determine how much to scroll, where nil instead of a number means the same as in mwheel-scroll, scroll by a near full screen. This command exists, because mwheel-scroll caused strange behaviour with automatic recentering." ;; (interactive (list last-input-event)) (interactive "e") (let* ((mods (delq 'click (delq 'double (delq 'triple (event-modifiers event))))) (amt (assoc mods mouse-wheel-scroll-amount))) ;;(message "%S" mods) (if amt (setq amt (or (cdr amt) (- (window-text-height) next-screen-context-lines))) (let ((list-elt mouse-wheel-scroll-amount)) (while (consp (setq amt (pop list-elt)))))) (if mouse-wheel-follow-mouse (select-window (posn-window (event-start event)))) (let ((button (mwheel-event-button event))) (cond ((eq button mouse-wheel-down-event) (forward-line (- amt))) ;;(princ amt)) ((eq button mouse-wheel-up-event) (forward-line amt)) ;;(princ amt)) (t (error "Bad binding in ccm-mwheel-scroll")))))) (defun ccm-scroll-down (&optional arg) "Replaces `scroll-down' because with scroll-down `centered-cursor-mode' sometimes doesn't reach the top of the buffer. This version actually moves the cursor with `previous-line'. Since with centered-cursor-mode the cursor is in a fixed position the movement appears as page up." (interactive "P") (let ((amt (or arg (- (window-text-height) next-screen-context-lines)))) (forward-line (- amt)))) (defun ccm-scroll-up (&optional arg) "Replaces `scroll-up' to be consistent with `ccm-scroll-down'. This version actually moves the cursor with `previous-line'. Since with centered-cursor-mode the cursor is in a fixed position the movement appears as page up." (interactive "P") (let ((amt (or arg (- (window-text-height) next-screen-context-lines)))) (forward-line amt))) (defun ccm-vpos-down (arg) "Adjust the value of the screen line (where the cursor stays) by arg. Negative values for arg are possible. Just the variable ccm-vpos is set." (interactive "p") (or arg (setq arg 1)) (let ((new-pos (if (< ccm-vpos 0) (- ccm-vpos arg) (+ ccm-vpos arg))) ;; see pos-visible-in-window-p (vpos-max (if (< ccm-vpos 0) -1 (- (window-text-height) 1))) (vpos-min (if (< ccm-vpos 0) (- (window-text-height)) 0))) (setq ccm-vpos (cond ((< new-pos vpos-min) vpos-min) ((> new-pos vpos-max) vpos-max) (t new-pos))))) (defun ccm-vpos-up (arg) "See `ccm-vpos-down'." (interactive "p") (or arg (setq arg 1)) (ccm-vpos-down (- arg))) (defun ccm-vpos-recenter () "Set the value of the screen line (where the cursor stays) in the center. Just the variable ccm-vpos is set." (interactive) (if (equal (current-buffer) (window-buffer (selected-window))) (setq ccm-vpos (* (eval ccm-vpos-init) ccm-vpos-inverted)))) (defun ccm-position-cursor () "Do the actual recentering at the position `ccm-vpos'." (unless (member this-command ccm-ignored-commands) (unless ccm-vpos (ccm-vpos-recenter)) (unless (minibufferp (current-buffer)) (if (equal (current-buffer) (window-buffer (selected-window))) (let* ((current-line (if (< ccm-vpos 0) ;; one-based, from bottom, negative (- (count-lines (point) ;; window-end is sometimes < 0 ;; when opening a help buffer (if (> (window-end) 0) (window-end) 1))) ;; zero-based, from top, positive (+ (count-lines (window-start) (point)) ;; count-lines returns different value in column 0 (if (= (current-column) 0) 0 -1)))) (diff (- ccm-vpos current-line)) (step-size ccm-step-size) (step-delay ccm-step-delay) (vpos-inverted ccm-vpos-inverted) (recenter-at-end-of-file ccm-recenter-at-end-of-file)) (let* ((bottom-vpos (if (< ccm-vpos 0) (- ccm-vpos) (- (window-text-height) ccm-vpos))) (correction (save-excursion (if (or (= (point) (point-max)) (progn (goto-char (point-max)) (zerop (current-column)))) 1 0))) ;; lines from point to end of buffer (bottom-lines (+ (count-lines (point) (point-max)) correction))) ;; only animate if point was moved rather far away ;; before by a mouseclick (see ccm-ignored-commands) ;; or if minor mode is just entered interactively (if (not (and (> (abs diff) 4) (or (member last-command ccm-ignored-commands) animate-first-start-p))) (recenter (if (and (< bottom-lines bottom-vpos) (not recenter-at-end-of-file)) ;; if near the bottom, recenter in the ;; negative screen line that equals the ;; bottom buffer line, i.e. if we are in ;; the second last line (-2) of the ;; buffer, the cursor will be recentered ;; in -2 (- bottom-lines) ccm-vpos)) (setq animate-first-start-p nil) ;; first build a list with positions to successively recenter to (setq recenter-sequence ;; reverse: because we build the list not FROM -> TO but ;; TO -> FROM because if step size in number-sequence is ;; bigger than one, TO might not included, that means the ;; ccm-vpos would not be reached ;; cdr: don't recenter the current-line (if (and (< bottom-lines bottom-vpos) (not recenter-at-end-of-file)) ;; this one is for animation near the bottom (cdr (reverse (number-sequence (- bottom-lines) (if (< ccm-vpos 0) current-line (- (- (window-text-height) current-line))) (* (/ diff (abs diff)) (- step-size))))) (cdr (reverse (number-sequence ccm-vpos current-line (* (/ diff (abs diff)) (- step-size))))))) ;; (message "%d %d %d (%d): %S" current-line ccm-vpos bottom-lines diff recenter-sequence) (while recenter-sequence ;; actual animation (recenter (pop recenter-sequence)) (if (car recenter-sequence) (sit-for step-delay t)))))))))) (defun ccm-first-start (animate) "Called from centered-cursor-mode. Animate at first start, if centered-cursor-mode is called interactively." (let ((animate-first-start-p animate)) (ccm-vpos-recenter) (ccm-position-cursor))) ;;(defalias 'ccm 'centered-cursor-mode) (define-minor-mode centered-cursor-mode "Makes the cursor stay vertically in a defined position (usually centered)." :init-value nil ;; :lighter nil :lighter " ¢" :keymap ccm-map (cond (centered-cursor-mode (ccm-first-start (interactive-p)) (add-hook 'post-command-hook 'ccm-position-cursor t t) (add-hook 'window-configuration-change-hook 'ccm-vpos-recenter t t)) (t (remove-hook 'post-command-hook 'ccm-position-cursor t) (remove-hook 'window-configuration-change-hook 'ccm-vpos-recenter t)))) (define-global-minor-mode global-centered-cursor-mode centered-cursor-mode centered-cursor-mode) (provide 'centered-cursor-mode) ;;; Help: ;; (info "(elisp)Defining Minor Modes") ;; (info "(elisp)Screen Lines") ;; (info "(elisp)Hooks") ;; (info "(elisp)Customization") ;; (find-function 'mwheel-scroll) ;; Local Variables: ;; coding: utf-8 ;; End: ;;; centered-cursor-mode.el ends here