(Vtruncate_partial_width_windows): Improve docstring.
[bpt/emacs.git] / lisp / doc-view.el
index 7d2988f..3ea4fce 100644 (file)
@@ -1,6 +1,6 @@
 ;;; doc-view.el --- View PDF/PostScript/DVI files in Emacs
 
-;; Copyright (C) 2007, 2008 Free Software Foundation, Inc.
+;; Copyright (C) 2007, 2008, 2009 Free Software Foundation, Inc.
 ;;
 ;; Author: Tassilo Horn <tassilo@member.fsf.org>
 ;; Maintainer: Tassilo Horn <tassilo@member.fsf.org>
@@ -8,10 +8,10 @@
 
 ;; This file is part of GNU Emacs.
 
-;; GNU Emacs is free software; you can redistribute it and/or modify
+;; 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 3, or (at your option)
-;; any later version.
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
 
 ;; GNU Emacs is distributed in the hope that it will be useful,
 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -19,9 +19,7 @@
 ;; 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.
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
 
 ;;; Requirements:
 
@@ -64,7 +62,7 @@
 ;; slice you can use `doc-view-set-slice' (bound to `s s') which will query you
 ;; for the coordinates of the slice's top-left corner and its width and height.
 ;; A much more convenient way to do the same is offered by the command
-;; `doc-view-set-slice-using-mouse' (bound to `s m').  After invokation you
+;; `doc-view-set-slice-using-mouse' (bound to `s m').  After invocation you
 ;; only have to press mouse-1 at the top-left corner and drag it to the
 ;; bottom-right corner of the desired slice.  To reset the slice use
 ;; `doc-view-reset-slice' (bound to `s r').
@@ -76,7 +74,7 @@
 ;; `C-r' you can do the same, but backwards.  To search for a new regexp give a
 ;; prefix arg to one of the search functions, e.g. by typing `C-u C-s'.  The
 ;; searching works by using a plain text representation of the document.  If
-;; that doesn't already exist the first invokation of `doc-view-search' (or
+;; that doesn't already exist the first invocation of `doc-view-search' (or
 ;; `doc-view-search-backward') starts the conversion.  When that finishes and
 ;; you're still viewing the document (i.e. you didn't switch to another buffer)
 ;; you're queried for the regexp then.
@@ -230,7 +228,7 @@ has finished."
   (let ((ol (image-mode-window-get 'overlay winprops)))
     (if ol
         (setq ol (copy-overlay ol))
-      (assert (not (get-char-property (point-min) 'display (car winprops))))
+      (assert (not (get-char-property (point-min) 'display)))
       (setq ol (make-overlay (point-min) (point-max) nil t))
       (overlay-put ol 'doc-view t))
     (overlay-put ol 'window (car winprops))
@@ -278,7 +276,7 @@ Can be `dvi', `pdf', or `ps'.")
 
 (defvar doc-view-mode-map
   (let ((map (make-sparse-keymap)))
-    (suppress-keymap map)
+    (set-keymap-parent map image-mode-map)
     ;; Navigation in the document
     (define-key map (kbd "n")         'doc-view-next-page)
     (define-key map (kbd "p")         'doc-view-previous-page)
@@ -291,13 +289,11 @@ Can be `dvi', `pdf', or `ps'.")
     (define-key map (kbd "M-<")       'doc-view-first-page)
     (define-key map (kbd "M->")       'doc-view-last-page)
     (define-key map [remap goto-line] 'doc-view-goto-page)
-    (define-key map [remap scroll-up] 'image-scroll-up)
-    (define-key map [remap scroll-down] 'image-scroll-down)
+    (define-key map (kbd "RET")       'image-next-line)
     ;; Zoom in/out.
     (define-key map "+"               'doc-view-enlarge)
     (define-key map "-"               'doc-view-shrink)
-    ;; Killing/burying the buffer (and the process)
-    (define-key map (kbd "q")         'bury-buffer)
+    ;; Killing the buffer (and the process)
     (define-key map (kbd "k")         'doc-view-kill-proc-and-buffer)
     (define-key map (kbd "K")         'doc-view-kill-proc)
     ;; Slicing the image
@@ -308,13 +304,6 @@ Can be `dvi', `pdf', or `ps'.")
     (define-key map (kbd "C-s")       'doc-view-search)
     (define-key map (kbd "<find>")    'doc-view-search)
     (define-key map (kbd "C-r")       'doc-view-search-backward)
-    ;; Scrolling
-    (define-key map [remap forward-char]  'image-forward-hscroll)
-    (define-key map [remap backward-char] 'image-backward-hscroll)
-    (define-key map [remap move-end-of-line]       'image-eol)
-    (define-key map [remap move-beginning-of-line] 'image-bol)
-    (define-key map [remap next-line]     'image-next-line)
-    (define-key map [remap previous-line] 'image-previous-line)
     ;; Show the tooltip
     (define-key map (kbd "C-t")       'doc-view-show-tooltip)
     ;; Toggle between text and image display or editing
@@ -358,7 +347,8 @@ Can be `dvi', `pdf', or `ps'.")
 (defun doc-view-goto-page (page)
   "View the page given by PAGE."
   (interactive "nPage: ")
-  (let ((len (length doc-view-current-files)))
+  (let ((len (length doc-view-current-files))
+       (hscroll (window-hscroll)))
     (if (< page 1)
        (setq page 1)
       (when (and (> page len)
@@ -391,6 +381,7 @@ Can be `dvi', `pdf', or `ps'.")
     (let ((file (expand-file-name (format "page-%d.png" page)
                                   (doc-view-current-cache-dir))))
       (doc-view-insert-image file :pointer 'arrow)
+      (set-window-hscroll (selected-window) hscroll)
       (when (and (not (file-exists-p file))
                  doc-view-current-converter-processes)
         ;; The PNG file hasn't been generated yet.
@@ -430,20 +421,26 @@ Can be `dvi', `pdf', or `ps'.")
 (defun doc-view-scroll-up-or-next-page ()
   "Scroll page up if possible, else goto next page."
   (interactive)
-  (when (= (window-vscroll) (image-scroll-up nil))
-    (let ((cur-page (doc-view-current-page)))
+  (let ((hscroll (window-hscroll))
+       (cur-page (doc-view-current-page)))
+    (when (= (window-vscroll) (image-scroll-up nil))
       (doc-view-next-page)
       (when (/= cur-page (doc-view-current-page))
-       (image-scroll-down nil)))))
+       (image-bob)
+       (image-bol 1))
+      (set-window-hscroll (selected-window) hscroll))))
 
 (defun doc-view-scroll-down-or-previous-page ()
   "Scroll page down if possible, else goto previous page."
   (interactive)
-  (when (= (window-vscroll) (image-scroll-down nil))
-    (let ((cur-page (doc-view-current-page)))
+  (let ((hscroll (window-hscroll))
+       (cur-page (doc-view-current-page)))
+    (when (= (window-vscroll) (image-scroll-down nil))
       (doc-view-previous-page)
       (when (/= cur-page (doc-view-current-page))
-       (image-scroll-up nil)))))
+       (image-eob)
+       (image-bol 1))
+      (set-window-hscroll (selected-window) hscroll))))
 
 ;;;; Utility Functions
 
@@ -567,7 +564,10 @@ Should be invoked when the cached images aren't up-to-date."
   "Generic sentinel for doc-view conversion processes."
   (if (not (string-match "finished" event))
       (message "DocView: process %s changed status to %s."
-               (process-name proc) event)
+               (process-name proc)
+              (if (string-match "\\(.+\\)\n?\\'" event)
+                  (match-string 1 event)
+                event))
     (when (buffer-live-p (process-get proc 'buffer))
       (with-current-buffer (process-get proc 'buffer)
         (setq doc-view-current-converter-processes
@@ -640,6 +640,8 @@ Call CALLBACK with no arguments when done."
                  pdf))
    callback))
 
+(declare-function clear-image-cache "image.c" (&optional filter))
+
 (defun doc-view-pdf->png (pdf png pages)
   "Convert a PDF file to PNG asynchronously.
 Start by converting PAGES, and then the rest."
@@ -662,6 +664,8 @@ Start by converting PAGES, and then the rest."
 
 (defun doc-view-pdf->txt (pdf txt callback)
   "Convert PDF to TXT asynchronously and call CALLBACK when finished."
+  (or doc-view-pdftotext-program
+      (error "You need the `pdftotext' program to convert a PDF to text"))
   (doc-view-start-process "pdf->txt" doc-view-pdftotext-program
                           (list "-raw" pdf txt)
                           callback))
@@ -692,6 +696,8 @@ Start by converting PAGES, and then the rest."
 
 (defun doc-view-ps->pdf (ps pdf callback)
   "Convert PS to PDF asynchronously and call CALLBACK when finished."
+  (or doc-view-ps2pdf-program
+      (error "You need the `ps2pdf' program to convert PS to PDF"))
   (doc-view-start-process "ps->pdf" doc-view-ps2pdf-program
                           (list
                            ;; Avoid security problems when rendering files from
@@ -719,8 +725,18 @@ Those files are saved in the directory given by the function
   ;; resets during the redisplay).
   (setq doc-view-pending-cache-flush t)
   (let ((png-file (expand-file-name "page-%d.png"
+                                    (doc-view-current-cache-dir)))
+       (res-file (expand-file-name "resolution.el"
                                     (doc-view-current-cache-dir))))
     (make-directory (doc-view-current-cache-dir) t)
+    ;; Save the used resolution so that it can be restored when
+    ;; reading the cached files.
+    (let ((res doc-view-resolution))
+      (with-temp-buffer
+       (princ res (current-buffer))
+        ;; Don't use write-file, so as to avoid prompts for `require-newline',
+        ;; or for pre-existing buffers with the same name, ...
+       (write-region nil nil res-file nil 'silently)))
     (case doc-view-doc-type
       (dvi
        ;; DVI files have to be converted to PDF before Ghostscript can process
@@ -740,6 +756,8 @@ Those files are saved in the directory given by the function
 
 ;;;; Slicing
 
+(declare-function image-size "image.c" (spec &optional pixels frame))
+
 (defun doc-view-set-slice (x y width height)
   "Set the slice of the images that should be displayed.
 You can use this function to tell doc-view not to display the
@@ -845,16 +863,17 @@ have the page we want to view."
             (sort (directory-files (doc-view-current-cache-dir) t
                                    "page-[0-9]+\\.png" t)
                   'doc-view-sort))
-      (dolist (win (get-buffer-window-list buffer nil t))
-        (let* ((page (doc-view-current-page win))
-               (pagefile (expand-file-name (format "page-%d.png" page)
-                                             (doc-view-current-cache-dir))))
-          (when (or force
-                    (and (not (member pagefile prev-pages))
-                         (member pagefile doc-view-current-files)))
-            (with-selected-window win
-              (assert (eq (current-buffer) buffer))
-              (doc-view-goto-page page))))))))
+      (dolist (win (or (get-buffer-window-list buffer nil t)
+                      (list (selected-window))))
+       (let* ((page (doc-view-current-page win))
+              (pagefile (expand-file-name (format "page-%d.png" page)
+                                          (doc-view-current-cache-dir))))
+         (when (or force
+                   (and (not (member pagefile prev-pages))
+                        (member pagefile doc-view-current-files)))
+           (with-selected-window win
+             (assert (eq (current-buffer) buffer))
+             (doc-view-goto-page page))))))))
 
 (defun doc-view-buffer-message ()
   ;; Only show this message initially, not when refreshing the buffer (in which
@@ -875,6 +894,8 @@ For now these keys are useful:
 `k' : Kill the conversion process and this buffer.
 `K' : Kill the conversion process.\n"))))
 
+(declare-function tooltip-show "tooltip" (text &optional use-echo-area))
+
 (defun doc-view-show-tooltip ()
   (interactive)
   (tooltip-show (doc-view-current-info)))
@@ -1038,6 +1059,16 @@ If BACKWARD is non-nil, jump to the previous match."
        (if (doc-view-already-converted-p)
            (progn
              (message "DocView: using cached files!")
+             ;; Load the saved resolution
+             (let ((res-file (expand-file-name "resolution.el"
+                                               (doc-view-current-cache-dir)))
+                   (res doc-view-resolution))
+               (with-temp-buffer
+                 (when (file-exists-p res-file)
+                   (insert-file-contents res-file)
+                   (setq res (read (current-buffer)))))
+               (when (numberp res)
+                 (set (make-local-variable 'doc-view-resolution) res)))
              (doc-view-display (current-buffer) 'force))
          (doc-view-convert-current-doc))
        (message
@@ -1047,11 +1078,14 @@ If BACKWARD is non-nil, jump to the previous match."
                  "editing or viewing the document."))))
     (message
      "%s"
-     (substitute-command-keys
-      (concat "No image (png) support available or some conversion utility for "
-             (file-name-extension doc-view-buffer-file-name)" files is missing.  "
-             "Type \\[doc-view-toggle-display] to switch to an editing mode or "
-             "\\[doc-view-open-text] to open a buffer showing the doc as text.")))))
+     (concat "No PNG support is available, or some conversion utility for "
+            (file-name-extension doc-view-buffer-file-name)
+            " files is missing."))
+    (if (and (executable-find doc-view-pdftotext-program)
+            (y-or-n-p
+             "Unable to render file.  View extracted text instead? "))
+       (doc-view-open-text)
+      (doc-view-toggle-display))))
 
 (defvar bookmark-make-record-function)
 
@@ -1077,93 +1111,105 @@ If BACKWARD is non-nil, jump to the previous match."
 ;;;###autoload
 (defun doc-view-mode ()
   "Major mode in DocView buffers.
+
+DocView Mode is an Emacs document viewer.  It displays PDF, PS
+and DVI files (as PNG images) in Emacs buffers.
+
 You can use \\<doc-view-mode-map>\\[doc-view-toggle-display] to
 toggle between displaying the document or editing it as text.
 \\{doc-view-mode-map}"
   (interactive)
 
-  (let* ((prev-major-mode (if (eq major-mode 'doc-view-mode)
-                             doc-view-previous-major-mode
-                           major-mode)))
-    (kill-all-local-variables)
-    (set (make-local-variable 'doc-view-previous-major-mode) prev-major-mode))
-
-  ;; Figure out the document type.
-  (let ((name-types
-         (when buffer-file-name
-           (cdr (assoc (file-name-extension buffer-file-name)
-                       '(("dvi" dvi)
-                         ("pdf" pdf)
-                         ("epdf" pdf)
-                         ("ps" ps)
-                         ("eps" ps))))))
-        (content-types
-         (save-excursion
-           (goto-char (point-min))
-           (cond
-            ((looking-at "%!") '(ps))
-            ((looking-at "%PDF") '(pdf))
-            ((looking-at "\367\002") '(dvi))))))
-    (set (make-local-variable 'doc-view-doc-type)
-         (car (or (doc-view-intersection name-types content-types)
-                  (when (and name-types content-types)
-                    (error "Conflicting types: name says %s but content says %s"
-                           name-types content-types))
-                  name-types content-types
-                  (error "Cannot determine the document type")))))
-
-  (doc-view-make-safe-dir doc-view-cache-directory)
-  ;; Handle compressed files, remote files, files inside archives
-  (set (make-local-variable 'doc-view-buffer-file-name)
-       (cond
-       (jka-compr-really-do-compress
-        (expand-file-name
-         (file-name-nondirectory
-          (file-name-sans-extension buffer-file-name))
-         doc-view-cache-directory))
-        ;; Is the file readable by local processes?
-        ;; We used to use `file-remote-p' but it's unclear what it's
-        ;; supposed to return nil for things like local files accessed via
-        ;; `su' or via file://...
-       ((let ((file-name-handler-alist nil))
-           (not (file-readable-p buffer-file-name)))
-        (expand-file-name
-         (file-name-nondirectory buffer-file-name)
-         doc-view-cache-directory))
-       (t buffer-file-name)))
-  (when (not (string= doc-view-buffer-file-name buffer-file-name))
-    (write-region nil nil doc-view-buffer-file-name))
-
-  (add-hook 'change-major-mode-hook
-           (lambda ()
-              (doc-view-kill-proc)
-              (remove-overlays (point-min) (point-max) 'doc-view t))
-           nil t)
-  (add-hook 'clone-indirect-buffer-hook 'doc-view-clone-buffer-hook nil t)
-  (add-hook 'kill-buffer 'doc-view-kill-proc nil t)
-
-  (remove-overlays (point-min) (point-max) 'doc-view t) ;Just in case.
-  ;; Keep track of display info ([vh]scroll, page number, overlay, ...)
-  ;; for each window in which this document is shown.
-  (add-hook 'image-mode-new-window-functions
-            'doc-view-new-window-function nil t)
-  (image-mode-setup-winprops)
-
-  (set (make-local-variable 'mode-line-position)
-       '(" P" (:eval (number-to-string (doc-view-current-page)))
-        "/" (:eval (number-to-string (length doc-view-current-files)))))
-  ;; Don't scroll unless the user specifically asked for it.
-  (set (make-local-variable 'auto-hscroll-mode) nil)
-  (set (make-local-variable 'cursor-type) nil)
-  (use-local-map doc-view-mode-map)
-  (set (make-local-variable 'after-revert-hook) 'doc-view-reconvert-doc)
-  (set (make-local-variable 'bookmark-make-record-function)
-       'doc-view-bookmark-make-record)
-  (setq mode-name "DocView"
-       buffer-read-only t
-       major-mode 'doc-view-mode)
-  (doc-view-initiate-display)
-  (run-mode-hooks 'doc-view-mode-hook))
+  (if (= (point-min) (point-max))
+      ;; The doc is empty or doesn't exist at all, so fallback to
+      ;; another mode.  We used to also check file-exists-p, but this
+      ;; returns nil for tar members.
+      (let ((auto-mode-alist (remq (rassq 'doc-view-mode auto-mode-alist)
+                                  auto-mode-alist)))
+       (normal-mode))
+
+    (let* ((prev-major-mode (if (eq major-mode 'doc-view-mode)
+                               doc-view-previous-major-mode
+                             major-mode)))
+      (kill-all-local-variables)
+      (set (make-local-variable 'doc-view-previous-major-mode) prev-major-mode))
+
+    ;; Figure out the document type.
+    (let ((name-types
+          (when buffer-file-name
+            (cdr (assoc (file-name-extension buffer-file-name)
+                        '(("dvi" dvi)
+                          ("pdf" pdf)
+                          ("epdf" pdf)
+                          ("ps" ps)
+                          ("eps" ps))))))
+         (content-types
+          (save-excursion
+            (goto-char (point-min))
+            (cond
+             ((looking-at "%!") '(ps))
+             ((looking-at "%PDF") '(pdf))
+             ((looking-at "\367\002") '(dvi))))))
+      (set (make-local-variable 'doc-view-doc-type)
+          (car (or (doc-view-intersection name-types content-types)
+                   (when (and name-types content-types)
+                     (error "Conflicting types: name says %s but content says %s"
+                            name-types content-types))
+                   name-types content-types
+                   (error "Cannot determine the document type")))))
+
+    (doc-view-make-safe-dir doc-view-cache-directory)
+    ;; Handle compressed files, remote files, files inside archives
+    (set (make-local-variable 'doc-view-buffer-file-name)
+        (cond
+         (jka-compr-really-do-compress
+          (expand-file-name
+           (file-name-nondirectory
+            (file-name-sans-extension buffer-file-name))
+           doc-view-cache-directory))
+         ;; Is the file readable by local processes?
+         ;; We used to use `file-remote-p' but it's unclear what it's
+         ;; supposed to return nil for things like local files accessed via
+         ;; `su' or via file://...
+         ((let ((file-name-handler-alist nil))
+            (not (file-readable-p buffer-file-name)))
+          (expand-file-name
+           (file-name-nondirectory buffer-file-name)
+           doc-view-cache-directory))
+         (t buffer-file-name)))
+    (when (not (string= doc-view-buffer-file-name buffer-file-name))
+      (write-region nil nil doc-view-buffer-file-name))
+
+    (add-hook 'change-major-mode-hook
+             (lambda ()
+               (doc-view-kill-proc)
+               (remove-overlays (point-min) (point-max) 'doc-view t))
+             nil t)
+    (add-hook 'clone-indirect-buffer-hook 'doc-view-clone-buffer-hook nil t)
+    (add-hook 'kill-buffer-hook 'doc-view-kill-proc nil t)
+
+    (remove-overlays (point-min) (point-max) 'doc-view t) ;Just in case.
+    ;; Keep track of display info ([vh]scroll, page number, overlay,
+    ;; ...)  for each window in which this document is shown.
+    (add-hook 'image-mode-new-window-functions
+             'doc-view-new-window-function nil t)
+    (image-mode-setup-winprops)
+
+    (set (make-local-variable 'mode-line-position)
+        '(" P" (:eval (number-to-string (doc-view-current-page)))
+          "/" (:eval (number-to-string (length doc-view-current-files)))))
+    ;; Don't scroll unless the user specifically asked for it.
+    (set (make-local-variable 'auto-hscroll-mode) nil)
+    (set (make-local-variable 'cursor-type) nil)
+    (use-local-map doc-view-mode-map)
+    (set (make-local-variable 'after-revert-hook) 'doc-view-reconvert-doc)
+    (set (make-local-variable 'bookmark-make-record-function)
+        'doc-view-bookmark-make-record)
+    (setq mode-name "DocView"
+         buffer-read-only t
+         major-mode 'doc-view-mode)
+    (doc-view-initiate-display)
+    (run-mode-hooks 'doc-view-mode-hook)))
 
 ;;;###autoload
 (define-minor-mode doc-view-minor-mode
@@ -1192,29 +1238,29 @@ See the command `doc-view-mode' for more information on this mode."
 
 ;;;; Bookmark integration
 
-(declare-function bookmark-buffer-file-name "bookmark" ())
+(declare-function bookmark-make-record-default "bookmark"
+                  (&optional point-only))
 (declare-function bookmark-prop-get "bookmark" (bookmark prop))
+(declare-function bookmark-default-handler "bookmark" (bmk))
 
 (defun doc-view-bookmark-make-record ()
-  `((filename . ,(bookmark-buffer-file-name))
-    (page     . ,(doc-view-current-page))
-    (handler  . doc-view-bookmark-jump)))
+  (nconc (bookmark-make-record-default)
+         `((page     . ,(doc-view-current-page))
+           (handler  . doc-view-bookmark-jump))))
 
 
 ;;;###autoload
 (defun doc-view-bookmark-jump (bmk)
   ;; This implements the `handler' function interface for record type
   ;; returned by `doc-view-bookmark-make-record', which see.
-  (let ((filename (bookmark-prop-get bmk 'filename))
-        (page (bookmark-prop-get bmk 'page)))
-    (with-current-buffer (find-file-noselect filename)
+  (prog1 (bookmark-default-handler bmk)
+    (let ((page (bookmark-prop-get bmk 'page)))
       (when (not (eq major-mode 'doc-view-mode))
-       (doc-view-toggle-display))
+        (doc-view-toggle-display))
       (with-selected-window
           (or (get-buffer-window (current-buffer) 0)
               (selected-window))
-        (doc-view-goto-page page))
-      `((buffer ,(current-buffer)) (position ,1)))))
+        (doc-view-goto-page page)))))
 
 
 (provide 'doc-view)