;;; pdf-outline.el --- Outline for PDF buffer -*- lexical-binding: t -*- ;; Copyright (C) 2013, 2014 Andreas Politz ;; Author: Andreas Politz ;; Keywords: files, multimedia ;; This program 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 of the License, or ;; (at your option) any later version. ;; This program 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 this program. If not, see . ;;; Commentary: ;; (require 'outline) (require 'pdf-links) (require 'pdf-view) (require 'pdf-util) (require 'cl-lib) (require 'imenu) (require 'let-alist) ;;; Code: ;; ;; User options ;; (defgroup pdf-outline nil "Display a navigatable outline of a PDF document." :group 'pdf-tools) (defcustom pdf-outline-buffer-indent 2 "The level of indent in the Outline buffer." :type 'integer) (defcustom pdf-outline-enable-imenu t "Whether `imenu' should be enabled in PDF documents." :type '(choice (const :tag "Yes" t) (const :tag "No" nil))) (defcustom pdf-outline-imenu-keep-order t "Whether `imenu' should be advised not to reorder the outline." :type '(choice (const :tag "Yes" t) (const :tag "No" nil))) (defcustom pdf-outline-imenu-use-flat-menus nil "Whether the constructed Imenu should be a list, rather than a tree." :type '(choice (const :tag "Yes" t) (const :tag "No" nil))) (defcustom pdf-outline-display-buffer-action '(nil . nil) "The display action used, when displaying the outline buffer." :type display-buffer--action-custom-type) (defcustom pdf-outline-display-labels nil "Whether the outline should display labels instead of page numbers. Usually a page's label is its displayed page number." :type 'boolean) (defcustom pdf-outline-fill-column fill-column "The value of `fill-column' in pdf outline buffers. Set to nil to disable line wrapping." :type 'integer) (defvar pdf-outline-minor-mode-map (let ((km (make-sparse-keymap))) (define-key km (kbd "o") #'pdf-outline) km) "Keymap used for `pdf-outline-minor-mode'.") (defvar pdf-outline-buffer-mode-map (let ((kmap (make-sparse-keymap))) (dotimes (i 10) (define-key kmap (vector (+ i ?0)) #'digit-argument)) (define-key kmap "-" #'negative-argument) (define-key kmap (kbd "p") #'previous-line) (define-key kmap (kbd "n") #'next-line) (define-key kmap (kbd "b") #'outline-backward-same-level) (define-key kmap (kbd "d") #'hide-subtree) (define-key kmap (kbd "a") #'show-all) (define-key kmap (kbd "s") #'show-subtree) (define-key kmap (kbd "f") #'outline-forward-same-level) (define-key kmap (kbd "u") #'pdf-outline-up-heading) (define-key kmap (kbd "Q") #'hide-sublevels) (define-key kmap (kbd "<") #'beginning-of-buffer) (define-key kmap (kbd ">") #'pdf-outline-end-of-buffer) (define-key kmap (kbd "TAB") #'outline-toggle-children) (define-key kmap (kbd "RET") #'pdf-outline-follow-link) (define-key kmap (kbd "C-o") #'pdf-outline-display-link) (define-key kmap (kbd "SPC") #'pdf-outline-display-link) (define-key kmap [mouse-1] #'pdf-outline-mouse-display-link) (define-key kmap (kbd "o") #'pdf-outline-select-pdf-window) (define-key kmap (kbd ".") #'pdf-outline-move-to-current-page) ;; (define-key kmap (kbd "Q") #'pdf-outline-quit) (define-key kmap (kbd "C-c C-q") #'pdf-outline-quit-and-kill) (define-key kmap (kbd "q") #'quit-window) (define-key kmap (kbd "M-RET") #'pdf-outline-follow-link-and-quit) (define-key kmap (kbd "C-c C-f") #'pdf-outline-follow-mode) kmap) "Keymap used in `pdf-outline-buffer-mode'.") ;; ;; Internal Variables ;; (define-button-type 'pdf-outline 'face nil 'keymap nil) (defvar-local pdf-outline-pdf-window nil "The PDF window corresponding to this outline buffer.") (defvar-local pdf-outline-pdf-document nil "The PDF filename or buffer corresponding to this outline buffer.") (defvar-local pdf-outline-follow-mode-last-link nil) ;; ;; Functions ;; ;;;###autoload (define-minor-mode pdf-outline-minor-mode "Display an outline of a PDF document. This provides a PDF's outline on the menu bar via imenu. Additionally the same outline may be viewed in a designated buffer. \\{pdf-outline-minor-mode-map}" :group 'pdf-outline (pdf-util-assert-pdf-buffer) (cond (pdf-outline-minor-mode (when pdf-outline-enable-imenu (pdf-outline-imenu-enable))) (t (when pdf-outline-enable-imenu (pdf-outline-imenu-disable))))) (define-derived-mode pdf-outline-buffer-mode outline-mode "PDF Outline" "View and traverse the outline of a PDF file. Press \\[pdf-outline-display-link] to display the PDF document, \\[pdf-outline-select-pdf-window] to select its window, \\[pdf-outline-move-to-current-page] to move to the outline item of the current page, \\[pdf-outline-follow-link] to goto the corresponding page or \\[pdf-outline-follow-link-and-quit] to additionally quit the Outline. \\[pdf-outline-follow-mode] enters a variant of `next-error-follow-mode'. Most `outline-mode' commands are rebound to their respective last character. \\{pdf-outline-buffer-mode-map}" (setq-local outline-regexp "\\( *\\).") (setq-local outline-level (lambda nil (1+ (/ (length (match-string 1)) pdf-outline-buffer-indent)))) (toggle-truncate-lines 1) (setq buffer-read-only t) (when (> (count-lines 1 (point-max)) (* 1.5 (frame-height))) (hide-sublevels 1)) (message "%s" (substitute-command-keys (concat "Try \\[pdf-outline-display-link], " "\\[pdf-outline-select-pdf-window], " "\\[pdf-outline-move-to-current-page] or " "\\[pdf-outline-follow-link-and-quit]")))) (define-minor-mode pdf-outline-follow-mode "Display links as point moves." :group 'pdf-outline (setq pdf-outline-follow-mode-last-link nil) (cond (pdf-outline-follow-mode (add-hook 'post-command-hook #'pdf-outline-follow-mode-pch nil t)) (t (remove-hook 'post-command-hook #'pdf-outline-follow-mode-pch t)))) (defun pdf-outline-follow-mode-pch () (let ((link (pdf-outline-link-at-pos (point)))) (when (and link (not (eq link pdf-outline-follow-mode-last-link))) (setq pdf-outline-follow-mode-last-link link) (pdf-outline-display-link (point))))) ;;;###autoload (defun pdf-outline (&optional buffer no-select-window-p) "Display an PDF outline of BUFFER. BUFFER defaults to the current buffer. Select the outline buffer, unless NO-SELECT-WINDOW-P is non-nil." (interactive (list nil (or current-prefix-arg (consp last-nonmenu-event)))) (let ((win (display-buffer (pdf-outline-noselect buffer) pdf-outline-display-buffer-action))) (unless no-select-window-p (select-window win)))) (defun pdf-outline-noselect (&optional buffer) "Create an PDF outline of BUFFER, but don't display it." (save-current-buffer (and buffer (set-buffer buffer)) (pdf-util-assert-pdf-buffer) (let* ((pdf-buffer (current-buffer)) (pdf-file (pdf-view-buffer-file-name)) (pdf-window (and (eq pdf-buffer (window-buffer)) (selected-window))) (bname (pdf-outline-buffer-name)) (buffer-exists-p (get-buffer bname)) (buffer (get-buffer-create bname))) (with-current-buffer buffer (setq-local fill-column pdf-outline-fill-column) (unless buffer-exists-p (when (= 0 (save-excursion (pdf-outline-insert-outline pdf-buffer))) (kill-buffer buffer) (error "PDF has no outline")) (pdf-outline-buffer-mode)) (set (make-local-variable 'other-window-scroll-buffer) pdf-buffer) (setq pdf-outline-pdf-window pdf-window pdf-outline-pdf-document (or pdf-file pdf-buffer)) (current-buffer))))) (defun pdf-outline-buffer-name (&optional pdf-buffer) (unless pdf-buffer (setq pdf-buffer (current-buffer))) (let ((buf (format "*Outline %s*" (buffer-name pdf-buffer)))) ;; (when (buffer-live-p (get-buffer buf)) ;; (kill-buffer buf)) buf)) (defun pdf-outline-insert-outline (pdf-buffer) (let ((labels (and pdf-outline-display-labels (pdf-info-pagelabels pdf-buffer))) (nitems 0)) (dolist (item (pdf-info-outline pdf-buffer)) (let-alist item (when (eq .type 'goto-dest) (insert-text-button (concat (make-string (* (1- .depth) pdf-outline-buffer-indent) ?\s) .title (if (> .page 0) (format " (%s)" (if labels (nth (1- .page) labels) .page)) "(invalid)")) 'type 'pdf-outline 'help-echo (pdf-links-action-to-string item) 'pdf-outline-link item) (newline) (cl-incf nitems)))) nitems)) (defun pdf-outline-get-pdf-window (&optional if-visible-p) (save-selected-window (let* ((buffer (cond ((buffer-live-p pdf-outline-pdf-document) pdf-outline-pdf-document) ((bufferp pdf-outline-pdf-document) (error "PDF buffer was killed")) (t (or (find-buffer-visiting pdf-outline-pdf-document) (find-file-noselect pdf-outline-pdf-document))))) (pdf-window (if (and (window-live-p pdf-outline-pdf-window) (eq buffer (window-buffer pdf-outline-pdf-window))) pdf-outline-pdf-window (or (get-buffer-window buffer) (and (null if-visible-p) (display-buffer buffer '(nil (inhibit-same-window . t)))))))) (setq pdf-outline-pdf-window pdf-window)))) ;; ;; Commands ;; (defun pdf-outline-move-to-current-page () "Move to the item corresponding to the current page. Open nodes as necessary." (interactive) (let (page) (with-selected-window (pdf-outline-get-pdf-window) (setq page (pdf-view-current-page))) (pdf-outline-move-to-page page))) (defun pdf-outline-quit-and-kill () "Quit browsing the outline and kill its buffer." (interactive) (pdf-outline-quit t)) (defun pdf-outline-quit (&optional kill) "Quit browsing the outline buffer." (interactive "P") (let ((win (selected-window))) (pdf-outline-select-pdf-window t) (quit-window kill win))) (defun pdf-outline-up-heading (arg &optional invisible-ok) "Like `outline-up-heading', but `push-mark' first." (interactive "p") (let ((pos (point))) (outline-up-heading arg invisible-ok) (unless (= pos (point)) (push-mark pos)))) (defun pdf-outline-end-of-buffer () "Move to the end of the outline buffer." (interactive) (let ((pos (point))) (goto-char (point-max)) (when (and (eobp) (not (bobp)) (null (button-at (point)))) (forward-line -1)) (unless (= pos (point)) (push-mark pos)))) (defun pdf-outline-link-at-pos (&optional pos) (unless pos (setq pos (point))) (let ((button (or (button-at pos) (button-at (1- pos))))) (and button (button-get button 'pdf-outline-link)))) (defun pdf-outline-follow-link (&optional pos) "Select PDF window and move to the page corresponding to POS." (interactive) (unless pos (setq pos (point))) (let ((link (pdf-outline-link-at-pos pos))) (unless link (error "Nothing to follow here")) (select-window (pdf-outline-get-pdf-window)) (pdf-links-action-perform link))) (defun pdf-outline-follow-link-and-quit (&optional pos) "Select PDF window and move to the page corresponding to POS. Then quit the outline window." (interactive) (let ((link (pdf-outline-link-at-pos (or pos (point))))) (pdf-outline-quit) (unless link (error "Nothing to follow here")) (pdf-links-action-perform link))) (defun pdf-outline-display-link (&optional pos) "Display the page corresponding to the link at POS." (interactive) (unless pos (setq pos (point))) (let ((inhibit-redisplay t) (link (pdf-outline-link-at-pos pos))) (unless link (error "Nothing to follow here")) (with-selected-window (pdf-outline-get-pdf-window) (pdf-links-action-perform link)) (force-mode-line-update t))) (defun pdf-outline-mouse-display-link (event) "Display the page corresponding to the position of EVENT." (interactive "@e") (pdf-outline-display-link (posn-point (event-start event)))) (defun pdf-outline-select-pdf-window (&optional no-create-p) "Display and select the PDF document window." (interactive) (let ((win (pdf-outline-get-pdf-window no-create-p))) (and (window-live-p win) (select-window win)))) (defun pdf-outline-toggle-subtree () "Toggle hidden state of the current complete subtree." (interactive) (save-excursion (outline-back-to-heading) (if (not (outline-invisible-p (line-end-position))) (hide-subtree) (show-subtree)))) (defun pdf-outline-move-to-page (page) "Move to an outline item corresponding to PAGE." (interactive (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg)) (read-number "Page: ")))) (goto-char (pdf-outline-position-of-page page)) (save-excursion (while (outline-invisible-p) (outline-up-heading 1 t) (show-children))) (save-excursion (when (outline-invisible-p) (outline-up-heading 1 t) (show-children))) (back-to-indentation)) (defun pdf-outline-position-of-page (page) (let (curpage) (save-excursion (goto-char (point-min)) (while (and (setq curpage (alist-get 'page (pdf-outline-link-at-pos))) (< curpage page)) (forward-line)) (point)))) ;; ;; Imenu Support ;; ;;;###autoload (defun pdf-outline-imenu-enable () "Enable imenu in the current PDF buffer." (interactive) (pdf-util-assert-pdf-buffer) (setq-local imenu-create-index-function (if pdf-outline-imenu-use-flat-menus 'pdf-outline-imenu-create-index-flat 'pdf-outline-imenu-create-index-tree)) (imenu-add-to-menubar "PDF Outline")) (defun pdf-outline-imenu-disable () "Disable imenu in the current PDF buffer." (interactive) (pdf-util-assert-pdf-buffer) (setq-local imenu-create-index-function nil) (local-set-key [menu-bar index] nil) (when (eq pdf-view-mode-map (keymap-parent (current-local-map))) (use-local-map (keymap-parent (current-local-map))))) (defun pdf-outline-imenu-create-item (link &optional labels) (let-alist link (list (format "%s (%s)" .title (if labels (nth (1- .page) labels) .page)) 0 'pdf-outline-imenu-activate-link link))) (defun pdf-outline-imenu-create-index-flat () (let ((labels (and pdf-outline-display-labels (pdf-info-pagelabels))) index) (dolist (item (pdf-info-outline)) (let-alist item (when (eq .type 'goto-dest) (push (pdf-outline-imenu-create-item item labels) index)))) (nreverse index))) (defun pdf-outline-imenu-create-index-tree () (pdf-outline-imenu-create-index-tree-1 (pdf-outline-treeify-outline-list (cl-remove-if-not (lambda (type) (eq type 'goto-dest)) (pdf-info-outline) :key (apply-partially 'alist-get 'type))) (and pdf-outline-display-labels (pdf-info-pagelabels)))) (defun pdf-outline-imenu-create-index-tree-1 (nodes &optional labels) (mapcar (lambda (node) (let (children) (when (consp (caar node)) (setq children (cdr node) node (car node))) (let ((item (pdf-outline-imenu-create-item node labels))) (if children (cons (alist-get 'title node) (cons item (pdf-outline-imenu-create-index-tree-1 children labels))) item)))) nodes)) (defun pdf-outline-treeify-outline-list (list) (when list (let ((depth (alist-get 'depth (car list))) result) (while (and list (>= (alist-get 'depth (car list)) depth)) (when (= (alist-get 'depth (car list)) depth) (let ((item (car list))) (when (and (cdr list) (> (alist-get 'depth (cadr list)) depth)) (setq item (cons item (pdf-outline-treeify-outline-list (cdr list))))) (push item result))) (setq list (cdr list))) (reverse result)))) (defun pdf-outline-imenu-activate-link (&rest args) ;; bug #14029 (when (eq (nth 2 args) 'pdf-outline-imenu-activate-link) (setq args (cdr args))) (pdf-links-action-perform (nth 2 args))) (defadvice imenu--split-menu (around pdf-outline activate) "Advice to keep the original outline order. Calls `pdf-outline-imenu--split-menu' instead, if in a PDF buffer and `pdf-outline-imenu-keep-order' is non-nil." (if (not (and (pdf-util-pdf-buffer-p) pdf-outline-imenu-keep-order)) ad-do-it (setq ad-return-value (pdf-outline-imenu--split-menu menulist title)))) (defvar imenu--rescan-item) (defvar imenu-sort-function) (defvar imenu-create-index-function) (defvar imenu-max-items) (defun pdf-outline-imenu--split-menu (menulist title) "Replacement function for `imenu--split-menu'. This function does not move sub-menus to the top, therefore keeping the original outline order of the document. Also it does not call `imenu-sort-function'." (let ((menulist (copy-sequence menulist)) keep-at-top) (if (memq imenu--rescan-item menulist) (setq keep-at-top (list imenu--rescan-item) menulist (delq imenu--rescan-item menulist))) (if (> (length menulist) imenu-max-items) (setq menulist (mapcar (lambda (menu) (cons (format "From: %s" (caar menu)) menu)) (imenu--split menulist imenu-max-items)))) (cons title (nconc (nreverse keep-at-top) menulist)))) ;; bugfix for imenu in Emacs 24.3 and below. (when (condition-case nil (progn (imenu--truncate-items '(("" 0))) nil) (error t)) (eval-after-load "imenu" '(defun imenu--truncate-items (menulist) "Truncate all strings in MENULIST to `imenu-max-item-length'." (mapc (lambda (item) ;; Truncate if necessary. (when (and (numberp imenu-max-item-length) (> (length (car item)) imenu-max-item-length)) (setcar item (substring (car item) 0 imenu-max-item-length))) (when (imenu--subalist-p item) (imenu--truncate-items (cdr item)))) menulist)))) (provide 'pdf-outline) ;;; pdf-outline.el ends here ;; Local Variables: ;; byte-compile-warnings: (not obsolete) ;; End: