|
|
- ;;; pdf-annot.el --- Annotation support for PDF files. -*- lexical-binding: t -*-
-
- ;; Copyright (C) 2013, 2014 Andreas Politz
-
- ;; Author: Andreas Politz <politza@fh-trier.de>
- ;; Keywords:
-
- ;; 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 <http://www.gnu.org/licenses/>.
-
- ;;; Commentary:
- ;;
-
-
- (require 'pdf-view)
- (require 'pdf-info)
- (require 'pdf-cache)
- (require 'pdf-misc)
- (require 'facemenu) ;; list-colors-duplicates
- (require 'faces) ;; color-values
- (require 'org) ;; org-create-formula-image
- (require 'tablist)
- (require 'cl-lib)
-
- ;; * ================================================================== *
- ;; * Customizations
- ;; * ================================================================== *
-
- (defgroup pdf-annot nil
- "Annotation support for PDF documents."
- :group 'pdf-tools)
-
- (defcustom pdf-annot-activate-handler-functions nil
- "A list of functions to activate a annotation.
-
- The functions on this hook will be called when some annotation is
- activated, usually by a mouse-click. Each one is called with the
- annotation as a single argument and it should return a non-nil
- value if it has `handled' it. If no such function exists, the
- default handler `pdf-annot-default-handler' will be
- called.
-
- This hook is meant to allow for custom annotations. FIXME:
- Implement and describe basic org example."
- :group 'pdf-annot
- :type 'hook)
-
- (defcustom pdf-annot-default-text-annotation-properties nil
- "Alist of initial properties for new text annotations."
- :group 'pdf-annot
- :type '(alist :key-type symbol :value-type sexp))
-
- (defcustom pdf-annot-default-markup-annotation-properties nil
- "Alist of initial properties for new markup annotations."
- :group 'pdf-annot
- :type '(alist :key-type symbol :value-type sexp))
-
- (make-obsolete-variable 'pdf-annot-default-text-annotation-properties
- 'pdf-annot-default-annotation-properties
- "0.90")
-
- (make-obsolete-variable 'pdf-annot-default-markup-annotation-properties
- 'pdf-annot-default-annotation-properties
- "0.90")
-
- (defcustom pdf-annot-default-annotation-properties
- `((t (label . ,user-full-name))
- (text (icon . "Note")
- (color . "#ff0000"))
- (highlight (color . "yellow"))
- (squiggly (color . "orange"))
- (strike-out(color . "red"))
- (underline (color . "blue")))
- "An alist of initial properties for new annotations.
-
- The alist contains a sub-alist for each of the currently available
- annotation types, i.e. text, highlight, squiggly, strike-out and
- underline. Additionally a sub-alist with a key of t acts as a default
- entry.
-
- Each of these sub-alists contain default property-values of newly
- added annotations of its respective type.
-
- Some of the most important properties and their types are label
- \(a string\), contents \(a string\), color \(a color\) and, for
- text-annotations only, icon \(one of the standard icon-types, see
- `pdf-annot-standard-text-icons'\).
-
- For example a value of
-
- \(\(t \(color . \"red\"\)
- \(label . \"Joe\"\)
- \(highlight \(color . \"green\"\)\)
-
- would use a green color for highlight and a red one for other
- annotations. Additionally the label for all annotations is set
- to \"Joe\"."
-
- :group 'pdf-annot
- :type (let* ((label '(cons :tag "Label" (const label) string))
- (contents '(cons :tag "Contents" (const contents) string))
- (color '(cons :tag "Color" (const color) color))
- (icon `(cons :tag "Icon"
- (const icon)
- (choice
- ,@(mapcar (lambda (icon)
- `(const ,icon))
- '("Note" "Comment" "Key" "Help" "NewParagraph"
- "Paragraph" "Insert" "Cross" "Circle")))))
- (other '(repeat
- :tag "Other properties"
- (cons :tag "Property"
- (symbol :tag "Key ")
- (sexp :tag "Value"))))
- (text-properties
- `(set ,label ,contents ,color ,icon ,other))
- (markup-properties
- `(set ,label ,contents ,color))
- (all-properties
- `(set ,label ,contents ,color ,icon ,other)))
- `(set
- (cons :tag "All Annotations" (const t) ,all-properties)
- (cons :tag "Text Annotations" (const text) ,text-properties)
- (cons :tag "Highlight Annotations" (const highlight) ,markup-properties)
- (cons :tag "Underline Annotations" (const underline) ,markup-properties)
- (cons :tag "Squiggly Annotations" (const squiggly) ,markup-properties)
- (cons :tag "Strike-out Annotations" (const strike-out) ,markup-properties))))
-
- (defcustom pdf-annot-print-annotation-functions
- '(pdf-annot-print-annotation-latex-maybe)
- "A alist of functions for printing annotations, e.g. for the tooltip.
-
- The functions receive the annotation as single argument and
- should return either a string or nil. The first string returned
- will be used.
-
- If all of them return nil, the default function
- `pdf-annot-print-annotation-default' is used."
- :group 'pdf-annot
- :type 'hook)
-
- (defcustom pdf-annot-latex-string-predicate
- (lambda (str)
- (and str (string-match "\\`[[:space:]\n]*[$\\]" str)))
- "A predicate for recognizing LaTeX fragments.
-
- It receives a string and should return non-nil, if string is a
- LaTeX fragment."
- :group 'pdf-annot
- :type 'function)
-
- (defcustom pdf-annot-latex-header
- (concat org-format-latex-header
- "\n\\setlength{\\textwidth}{12cm}")
- "Header used when latex compiling annotations.
-
- The default value is `org-format-latex-header' + \
- \"\\n\\\\setlength{\\\\textwidth}{12cm}\"."
- :group 'pdf-annot
- :type 'string)
-
- (defcustom pdf-annot-tweak-tooltips t
- "Whether this package should tweak some settings regarding tooltips.
-
- If this variable has a non-nil value,
-
- `x-gtk-use-system-tooltips' is set to nil if appropriate, in
- order to display text properties;
-
- `tooltip-hide-delay' is set to infinity, in order to not being
- annoyed while reading the annotations."
- :group 'pdf-annot
- :type 'boolean)
-
- (defcustom pdf-annot-activate-created-annotations nil
- "Whether to activate (i.e. edit) created annotations."
- :group 'pdf-annot
- :type 'boolean)
-
- (defcustom pdf-annot-attachment-display-buffer-action nil
- "The display action used when displaying attachments."
- :group 'pdf-annot
- :type display-buffer--action-custom-type)
-
- (defconst pdf-annot-annotation-types
- '(3d caret circle file
- free-text highlight ink line link movie poly-line polygon popup
- printer-mark screen sound square squiggly stamp strike-out text
- trap-net underline unknown watermark widget)
- "Complete list of annotation types.")
-
- (defcustom pdf-annot-list-listed-types
- (if (pdf-info-markup-annotations-p)
- (list 'text 'file 'squiggly 'highlight 'underline 'strike-out)
- (list 'text 'file))
- "A list of annotation types displayed in the list buffer."
- :group 'pdf-annot
- :type `(set ,@(mapcar (lambda (type)
- (list 'const type))
- pdf-annot-annotation-types)))
-
- ;; * ================================================================== *
- ;; * Variables and Macros
- ;; * ================================================================== *
-
- (defvar pdf-annot-color-history nil
- "A list of recently used colors for annotations.")
-
- (defvar-local pdf-annot-modified-functions nil
- "Functions to call, when an annotation was modified.
-
- A function on this hook should accept one argument: A CLOSURE
- containing inserted, changed and deleted annotations.
-
- It may access theses annotations by calling CLOSURE with one of
- these arguments:
-
- `:inserted' The list of recently added annotations.
-
- `:deleted' The list of recently deleted annotations.
-
- `:changed' The list of recently changed annotations.
-
- `t' The union of recently added, deleted or changed annotations.
-
- `nil' Just returns nil.
-
- Any other argument signals an error.")
-
- (defconst pdf-annot-text-annotation-size '(24 . 24)
- "The Size of text and file annotations in PDF points.
-
- These values are hard-coded in poppler. And while the size of
- these annotations may be changed, i.e. the edges property, it has
- no effect on the rendering.")
-
- (defconst pdf-annot-markup-annotation-types
- '(text link free-text line square
- circle polygon poly-line highlight underline squiggly
- strike-out stamp caret ink file sound)
- "List of defined markup annotation types.")
-
- (defconst pdf-annot-standard-text-icons
- '("Note" "Comment" "Key" "Help" "NewParagraph"
- "Paragraph" "Insert" "Cross" "Circle")
- "A list of standard icon properties for text annotations.")
-
- (defvar pdf-annot-inhibit-modification-hooks nil
- "Non-nil, if running `pdf-annot-modified-functions' should be
- inhibited after some annotation has changed.")
-
- (defvar-local pdf-annot-delayed-modified-annotations nil
- "A plist of not yet propagated modifications.
-
- It contains three entries :change, :delete and :insert. Each one
- having a list of annotations as value.")
-
- (defvar-local pdf-annot--attachment-file-alist nil
- "Alist mapping attachment ids to unique relative filenames.")
-
- (defmacro pdf-annot-with-atomic-modifications (&rest body)
- "Execute BODY joining multiple modifications.
-
- The effect is, that `pdf-annot-modified-functions' will be called
- only once at the end of BODY.
-
- BODY should not modify annotations in a different then the
- current buffer, because that won't run the hooks properly."
- (declare (indent 0) (debug t))
- `(unwind-protect
- (save-current-buffer
- (let ((pdf-annot-inhibit-modification-hooks t))
- (progn ,@body)))
- (pdf-annot-run-modified-hooks)))
-
- ;; * ================================================================== *
- ;; * Minor mode
- ;; * ================================================================== *
-
- (defcustom pdf-annot-minor-mode-map-prefix (kbd "C-c C-a")
- "The prefix to use for `pdf-annot-minor-mode-map'.
-
- Setting this after the package was loaded has no effect."
- :group 'pdf-annot
- :type 'key-sequence)
-
- (defvar pdf-annot-minor-mode-map
- (let ((kmap (make-sparse-keymap))
- (smap (make-sparse-keymap)))
- (define-key kmap pdf-annot-minor-mode-map-prefix smap)
- (define-key smap "l" 'pdf-annot-list-annotations)
- ;; (define-key smap "d" 'pdf-annot-toggle-display-annotations)
- (define-key smap "a" 'pdf-annot-attachment-dired)
- (when (pdf-info-writable-annotations-p)
- (define-key smap "D" 'pdf-annot-delete)
- (define-key smap "t" 'pdf-annot-add-text-annotation)
- (when (pdf-info-markup-annotations-p)
- (define-key smap "m" 'pdf-annot-add-markup-annotation)
- (define-key smap "s" 'pdf-annot-add-squiggly-markup-annotation)
- (define-key smap "u" 'pdf-annot-add-underline-markup-annotation)
- (define-key smap "o" 'pdf-annot-add-strikeout-markup-annotation)
- (define-key smap "h" 'pdf-annot-add-highlight-markup-annotation)))
- kmap)
- "Keymap used for `pdf-annot-minor-mode'.")
-
- (defvar savehist-minibuffer-history-variables)
-
- ;;;###autoload
- (define-minor-mode pdf-annot-minor-mode
- "Support for PDF Annotations.
-
- \\{pdf-annot-minor-mode-map}"
- nil nil nil
- (cond
- (pdf-annot-minor-mode
- (when pdf-annot-tweak-tooltips
- (when (boundp 'x-gtk-use-system-tooltips)
- (setq x-gtk-use-system-tooltips nil))
- (setq tooltip-hide-delay 3600))
- (pdf-view-add-hotspot-function 'pdf-annot-hotspot-function 9)
- (add-hook 'pdf-info-close-document-hook
- 'pdf-annot-attachment-delete-base-directory nil t)
- (when (featurep 'savehist)
- (add-to-list 'savehist-minibuffer-history-variables
- 'pdf-annot-color-history)))
- (t
- (pdf-view-remove-hotspot-function 'pdf-annot-hotspot-function)
- (remove-hook 'pdf-info-close-document-hook
- 'pdf-annot-attachment-delete-base-directory t)))
- (pdf-view-redisplay t))
-
- (defun pdf-annot-create-context-menu (a)
- "Create a appropriate context menu for annotation A."
- (let ((menu (make-sparse-keymap)))
- ;; (when (and (bound-and-true-p pdf-misc-menu-bar-minor-mode)
- ;; (bound-and-true-p pdf-misc-install-popup-menu))
- ;; (set-keymap-parent menu
- ;; (lookup-key pdf-misc-menu-bar-minor-mode-map
- ;; [menu-bar pdf-tools]))
- ;; (define-key menu [sep-99] menu-bar-separator))
- (when (pdf-info-writable-annotations-p)
- (define-key menu [delete-annotation]
- `(menu-item "Delete annotation"
- ,(lambda ()
- (interactive)
- (pdf-annot-delete a)
- (message "Annotation deleted"))
- :help
- "Delete this annotation.")))
- (define-key menu [goto-annotation]
- `(menu-item "List annotation"
- ,(lambda ()
- (interactive)
- (pdf-annot-show-annotation a t)
- (pdf-annot-list-annotations)
- (pdf-annot-list-goto-annotation a))
- :help "Find this annotation in the list buffer."))
- (when (pdf-annot-text-annotation-p a)
- (define-key menu [change-text-icon]
- `(menu-item "Change icon"
- ,(pdf-annot-create-icon-submenu a)
- :help "Change the appearance of this annotation.")))
- (define-key menu [change-color]
- `(menu-item "Change color"
- ,(pdf-annot-create-color-submenu a)
- :help "Change the appearance of this annotation."))
- (define-key menu [activate-annotation]
- `(menu-item "Activate"
- ,(lambda ()
- (interactive)
- (pdf-annot-activate-annotation a))
- :help "Activate this annotation."))
- menu))
-
- (defun pdf-annot-create-color-submenu (a)
- (let ((menu (make-sparse-keymap)))
- (define-key menu [color-chooser]
- `(menu-item "Choose ..."
- ,(lambda ()
- (interactive)
- (list-colors-display
- nil "*Choose annotation color*"
- ;; list-colors-print does not like closures.
- (let ((callback (make-symbol "xcallback")))
- (fset callback
- (lambda (color)
- (pdf-annot-put a 'color color)
- (setq pdf-annot-color-history
- (cons color
- (remove color pdf-annot-color-history)))
- (quit-window t)))
- (list 'function callback))))))
- (dolist (color (butlast (reverse pdf-annot-color-history)
- (max 0 (- (length pdf-annot-color-history)
- 12))))
- (define-key menu (vector (intern (format "color-%s" color)))
- `(menu-item ,color
- ,(lambda nil
- (interactive)
- (pdf-annot-put a 'color color)))))
- menu))
-
- (defun pdf-annot-create-icon-submenu (a)
- (let ((menu (make-sparse-keymap)))
- (dolist (icon (reverse pdf-annot-standard-text-icons))
- (define-key menu (vector (intern (format "icon-%s" icon)))
- `(menu-item ,icon
- ,(lambda nil
- (interactive)
- (pdf-annot-put a 'icon icon)))))
- menu))
- ;; * ================================================================== *
- ;; * Annotation Basics
- ;; * ================================================================== *
-
- (defun pdf-annot-create (alist &optional buffer)
- "Create a annotation from ALIST in BUFFER.
-
- ALIST should be a property list as returned by
- `pdf-cache-getannots'. BUFFER should be the buffer of the
- corresponding PDF document. It defaults to the current buffer."
-
- (cons `(buffer . ,(or buffer (current-buffer)))
- alist))
-
- (defun pdf-annot-getannots (&optional pages types buffer)
- "Return a list of annotations on PAGES of TYPES in BUFFER.
-
- See `pdf-info-normalize-pages' for valid values of PAGES. TYPES
- may be a symbol or list of symbols denoting annotation types.
-
- PAGES defaults to all pages, TYPES to all types and BUFFER to the
- current buffer."
-
- (pdf-util-assert-pdf-buffer buffer)
- (unless buffer
- (setq buffer (current-buffer)))
- (unless (listp types)
- (setq types (list types)))
- (with-current-buffer buffer
- (let (result)
- (dolist (a (pdf-info-getannots pages))
- (when (or (null types)
- (memq (pdf-annot-get a 'type) types))
- (push (pdf-annot-create a) result)))
- result)))
-
- (defun pdf-annot-getannot (id &optional buffer)
- (pdf-annot-create
- (pdf-info-getannot id buffer)
- buffer))
-
- (defun pdf-annot-get (a property &optional default)
- "Get annotation A's value of PROPERTY.
-
- Return DEFAULT, if value is nil."
- (or (cdr (assq property a)) default))
-
- (defun pdf-annot-put (a property value)
- "Set annotation A's PROPERTY to VALUE.
-
- Unless VALUE is `equal' to the current value, sets A's buffer's
- modified flag and runs the hook `pdf-annot-modified-functions'.
-
- Signals an error, if PROPERTY is not modifiable.
-
- Returns the modified annotation."
-
- (declare (indent 2))
- (unless (equal value (pdf-annot-get a property))
- (unless (pdf-annot-property-modifiable-p a property)
- (error "Property `%s' is read-only for this annotation"
- property))
- (with-current-buffer (pdf-annot-get-buffer a)
- (setq a (pdf-annot-create
- (pdf-info-editannot
- (pdf-annot-get-id a)
- `((,property . ,value)))))
- (set-buffer-modified-p t)
- (pdf-annot-run-modified-hooks :change a)))
- a)
-
- (defun pdf-annot-run-modified-hooks (&optional operation &rest annotations)
- "Run `pdf-annot-modified-functions' using OPERATION on ANNOTATIONS.
-
- OPERATION should be one of nil, :change, :insert or :delete. If
- nil, annotations should be empty.
-
- Redisplay modified pages.
-
- If `pdf-annot-inhibit-modification-hooks' in non-nil, this just
- saves ANNOTATIONS and does not call the hooks until later, when
- the variable is nil and this function is called again."
-
- (unless (memq operation '(nil :insert :change :delete))
- (error "Invalid operation: %s" operation))
- (when (and (null operation) annotations)
- (error "Missing operation argument"))
-
- (when operation
- (let ((list (plist-get pdf-annot-delayed-modified-annotations operation)))
- (dolist (a annotations)
- (cl-pushnew a list :test 'pdf-annot-equal))
- (setq pdf-annot-delayed-modified-annotations
- (plist-put pdf-annot-delayed-modified-annotations
- operation list))))
- (unless pdf-annot-inhibit-modification-hooks
- (let* ((changed (plist-get pdf-annot-delayed-modified-annotations :change))
- (inserted (mapcar (lambda (a)
- (or (car (cl-member a changed :test 'pdf-annot-equal))
- a))
- (plist-get pdf-annot-delayed-modified-annotations :insert)))
- (deleted (plist-get pdf-annot-delayed-modified-annotations :delete))
- (union (cl-union (cl-union changed inserted :test 'pdf-annot-equal)
- deleted :test 'pdf-annot-equal))
- (closure (lambda (arg)
- (cl-ecase arg
- (:inserted (copy-sequence inserted))
- (:changed (copy-sequence changed))
- (:deleted (copy-sequence deleted))
- (t (copy-sequence union))
- (nil nil))))
- (pages (mapcar (lambda (a) (pdf-annot-get a 'page)) union)))
- (when union
- (unwind-protect
- (run-hook-with-args
- 'pdf-annot-modified-functions closure)
- (setq pdf-annot-delayed-modified-annotations nil)
- (apply 'pdf-view-redisplay-pages pages))))))
-
- (defun pdf-annot-equal (a1 a2)
- "Return non-nil, if annotations A1 and A2 are equal.
-
- Two annotations are equal, if they belong to the same buffer and
- have identical id properties."
- (and (eq (pdf-annot-get-buffer a1)
- (pdf-annot-get-buffer a2))
- (eq (pdf-annot-get-id a1)
- (pdf-annot-get-id a2))))
-
- (defun pdf-annot-get-buffer (a)
- "Return annotation A's buffer."
- (pdf-annot-get a 'buffer))
-
- (defun pdf-annot-get-id (a)
- "Return id property of annotation A."
- (pdf-annot-get a 'id))
-
- (defun pdf-annot-get-type (a)
- "Return type property of annotation A."
- (pdf-annot-get a 'type))
-
- (defun pdf-annot-get-display-edges (a)
- "Return a list of EDGES used for display for annotation A.
-
- This returns a list of \(LEFT TOP RIGHT BOT\) demarking the
- rectangles of the page where A is rendered."
-
- (or (pdf-annot-get a 'markup-edges)
- (list (pdf-annot-get a 'edges))))
-
- (defun pdf-annot-delete (a)
- "Delete annotation A.
-
- Sets A's buffer's modified flag and runs the hook
- `pdf-annot-modified-functions'.
-
- This function always returns nil."
- (interactive
- (list (pdf-annot-read-annotation
- "Click on the annotation you wish to delete")))
- (with-current-buffer (pdf-annot-get-buffer a)
- (pdf-info-delannot
- (pdf-annot-get-id a))
- (set-buffer-modified-p t)
- (pdf-annot-run-modified-hooks :delete a))
- (when (called-interactively-p 'any)
- (message "Annotation deleted"))
- nil)
-
- (defun pdf-annot-text-annotation-p (a)
- (eq 'text (pdf-annot-get a 'type)))
-
- (defun pdf-annot-markup-annotation-p (a)
- (not (null
- (memq (pdf-annot-get a 'type)
- pdf-annot-markup-annotation-types))))
-
- (defun pdf-annot-property-modifiable-p (a property)
- (or (memq property '(edges color flags contents))
- (and (pdf-annot-markup-annotation-p a)
- (memq property '(label opacity popup popup-is-open)))
- (and (pdf-annot-text-annotation-p a)
- (memq property '(icon is-open)))))
-
- (defun pdf-annot-activate-annotation (a)
- (or (run-hook-with-args-until-success
- 'pdf-annot-activate-handler-functions
- a)
- (pdf-annot-default-activate-handler a)))
-
- (defun pdf-annot-default-activate-handler (a)
- (cond
- ((pdf-annot-has-attachment-p a)
- (pdf-annot-pop-to-attachment a))
- (t (pdf-annot-edit-contents a))))
-
- ;; * ================================================================== *
- ;; * Handling attachments
- ;; * ================================================================== *
-
- (defun pdf-annot-has-attachment-p (a)
- "Return non-nil if annotation A's has data attached."
- (eq 'file (pdf-annot-get a 'type)))
-
- (defun pdf-annot-get-attachment (a &optional do-save)
- "Retrieve annotation A's attachment.
-
- The DO-SAVE argument is given to
- `pdf-info-getattachment-from-annot', which see."
- (unless (pdf-annot-has-attachment-p a)
- (error "Annotation has no data attached: %s" a))
- (pdf-info-getattachment-from-annot
- (pdf-annot-get-id a)
- do-save
- (pdf-annot-get-buffer a)))
-
- (defun pdf-annot-attachment-base-directory ()
- "Return the base directory for saving attachments."
- (let ((dir (pdf-util-expand-file-name "attachments")))
- (unless (file-exists-p dir)
- (make-directory dir))
- dir))
-
- (defun pdf-annot-attachment-delete-base-directory ()
- "Delete all saved attachment files of the current buffer."
- (setq pdf-annot--attachment-file-alist nil)
- (delete-directory (pdf-annot-attachment-base-directory) t))
-
- (defun pdf-annot-attachment-unique-filename (attachment)
- "Return a unique absolute filename for ATTACHMENT."
- (let* ((filename (or (cdr (assq 'filename attachment))
- "attachment"))
- (id (cdr (assq 'id attachment)))
- (unique
- (or (cdr (assoc id pdf-annot--attachment-file-alist))
- (let* ((sans-ext
- (expand-file-name
- (concat (file-name-as-directory ".")
- (file-name-sans-extension filename))
- (pdf-annot-attachment-base-directory)))
- (ext (file-name-extension filename))
- (newname (concat sans-ext "." ext))
- (i 0))
- (while (rassoc newname pdf-annot--attachment-file-alist)
- (setq newname (format "%s-%d.%s" sans-ext (cl-incf i) ext)))
- (push (cons id newname) pdf-annot--attachment-file-alist)
- newname)))
- (directory (file-name-directory unique)))
- (unless (file-exists-p directory)
- (make-directory directory t))
- unique))
-
-
- (defun pdf-annot-attachment-save (attachment &optional regenerate-p)
- "Save ATTACHMENT's data to a unique filename and return it's name.
-
- If REGENERATE-P is non-nil, copy attachment's file even if the
- copy already exists.
-
- Signal an error, if ATTACHMENT has no, or a non-existing, `file'
- property, i.e. it was retrieved with an unset do-save argument.
- See `pdf-info-getattachments'"
-
- (let ((datafile (cdr (assq 'file attachment))))
- (unless (and datafile
- (file-exists-p datafile))
- (error "Attachment's file property is invalid"))
- (let* ((filename
- (pdf-annot-attachment-unique-filename attachment)))
- (when (or regenerate-p
- (not (file-exists-p filename)))
- (copy-file datafile filename nil nil t t))
- filename)))
-
- (defun pdf-annot-find-attachment-noselect (a)
- "Find annotation A's attachment in a buffer, without selecting it.
-
- Signals an error, if A has no data attached."
- (let ((attachment (pdf-annot-get-attachment a t)))
- (unwind-protect
- (find-file-noselect
- (pdf-annot-attachment-save attachment))
- (let ((tmpfile (cdr (assq 'file attachment))))
- (when (and tmpfile
- (file-exists-p tmpfile))
- (delete-file tmpfile))))))
-
- (defun pdf-annot-attachment-dired (&optional regenerate-p)
- "List all attachments in a dired buffer.
-
- If REGENERATE-P is non-nil, create attachment's files even if
- they already exist. Interactively REGENERATE-P is non-nil if a
- prefix argument was given.
-
- Return the dired buffer."
- (interactive (list current-prefix-arg))
- (let ((attachments (pdf-info-getattachments t)))
- (unwind-protect
- (progn
- (dolist (a (pdf-annot-getannots nil 'file))
- (push (pdf-annot-get-attachment a t)
- attachments ))
- (dolist (att attachments)
- (pdf-annot-attachment-save att regenerate-p))
- (unless attachments
- (error "Document has no data attached"))
- (dired (pdf-annot-attachment-base-directory)))
- (dolist (att attachments)
- (let ((tmpfile (cdr (assq 'file att))))
- (when (and tmpfile (file-exists-p tmpfile))
- (delete-file tmpfile)))))))
-
- (defun pdf-annot-display-attachment (a &optional display-action select-window-p)
- "Display file annotation A's data in a buffer.
-
- DISPLAY-ACTION should be a valid `display-buffer' action. If
- nil, `pdf-annot-attachment-display-buffer-action' is used.
-
- Select the window, if SELECT-WINDOW-P is non-nil.
-
- Return the window attachment is displayed in."
-
- (interactive
- (list (pdf-annot-read-annotation
- "Select a file annotation by clicking on it")))
- (let* ((buffer (pdf-annot-find-attachment-noselect a))
- (window (display-buffer
- buffer (or display-action
- pdf-annot-attachment-display-buffer-action))))
- (when select-window-p
- (select-window window))
- window))
-
- (defun pdf-annot-pop-to-attachment (a)
- "Display annotation A's attachment in a window and select it."
- (interactive
- (list (pdf-annot-read-annotation
- "Select a file annotation by clicking on it")))
- (pdf-annot-display-attachment a nil t))
-
- ;; * ================================================================== *
- ;; * Interfacing with the display
- ;; * ================================================================== *
-
- (defun pdf-annot-image-position (a &optional image-size)
- "Return the position of annotation A in image coordinates.
-
- IMAGE-SIZE should be a cons \(WIDTH . HEIGHT\) and defaults to
- the page-image of the selected window."
-
- (unless image-size
- (pdf-util-assert-pdf-window)
- (setq image-size (pdf-view-image-size)))
- (let ((e (pdf-util-scale
- (pdf-annot-get a 'edges)
- image-size)))
- (pdf-util-with-edges (e)
- `(,e-left . ,e-top))))
-
- (defun pdf-annot-image-set-position (a x y &optional image-size)
- "Set annotation A's position to X,Y in image coordinates.
-
- See `pdf-annot-image-position' for IMAGE-SIZE."
-
- (unless image-size
- (pdf-util-assert-pdf-window)
- (setq image-size (pdf-view-image-size)))
- (let* ((edges (pdf-annot-get a 'edges))
- (x (/ x (float (car image-size))))
- (y (/ y (float (cdr image-size)))))
- (pdf-util-with-edges (edges)
- (let* ((w edges-width)
- (h edges-height)
- (x (max 0 (min x (- 1 w))))
- (y (max 0 (min y (- 1 h)))))
- (pdf-annot-put a 'edges
- (list x y -1 -1))))))
-
- (defun pdf-annot-image-size (a &optional image-size)
- "Return the size of annotation A in image coordinates.
-
- Returns \(WIDTH . HEIGHT\).
-
- See `pdf-annot-image-position' for IMAGE-SIZE."
- (unless image-size
- (pdf-util-assert-pdf-window)
- (setq image-size (pdf-view-image-size)))
- (let ((edges (pdf-util-scale
- (pdf-annot-get a 'edges) image-size)))
- (pdf-util-with-edges (edges)
- (cons edges-width edges-height))))
-
- (defun pdf-annot-image-set-size (a &optional width height image-size)
- "Set annotation A's size in image to WIDTH and/or HEIGHT.
-
- See `pdf-annot-image-position' for IMAGE-SIZE."
- (unless image-size
- (pdf-util-assert-pdf-window)
- (setq image-size (pdf-view-image-size)))
- (let* ((edges (pdf-annot-get a 'edges))
- (w (and width
- (/ width (float (car image-size)))))
- (h (and height
- (/ height (float (cdr image-size))))))
- (pdf-util-with-edges (edges)
- (pdf-annot-put a 'edges
- (list edges-left
- edges-top
- (if w (+ edges-left w) edges-right)
- (if h (+ edges-top h) edges-bot))))))
-
- (defun pdf-annot-at-position (pos)
- "Return annotation at POS in the selected window.
-
- POS should be an absolute image position as a cons \(X . Y\).
- Alternatively POS may also be an event position, in which case
- `posn-window' and `posn-object-x-y' is used to find the image
- position.
-
- Return nil, if no annotation was found."
- (let (window)
- (when (posnp pos)
- (setq window (posn-window pos)
- pos (posn-object-x-y pos)))
- (save-selected-window
- (when window (select-window window))
- (let* ((annots (pdf-annot-getannots (pdf-view-current-page)))
- (size (pdf-view-image-size))
- (rx (/ (car pos) (float (car size))))
- (ry (/ (cdr pos) (float (cdr size))))
- (rpos (cons rx ry)))
- (or (cl-some (lambda (a)
- (and (cl-some
- (lambda (e)
- (pdf-util-edges-inside-p e rpos))
- (pdf-annot-get-display-edges a))
- a))
- annots)
- (error "No annotation at this position"))))))
-
- (defun pdf-annot-mouse-move (event &optional annot)
- "Start moving an annotation at EVENT's position.
-
- EVENT should be a mouse event originating the request and is used
- as a reference point.
-
- ANNOT is the annotation to operate on and defaults to the
- annotation at EVENT's start position.
-
- This function does not return until the operation is completed,
- i.e. a non mouse-movement event is read."
-
- (interactive "@e")
- (pdf-util-assert-pdf-window (posn-window (event-start event)))
- (select-window (posn-window (event-start event)))
- (let* ((mpos (posn-object-x-y (event-start event)))
- (a (or annot
- (pdf-annot-at-position mpos))))
- (unless a
- (error "No annotation at this position: %s" mpos))
- (let* ((apos (pdf-annot-image-position a))
- (offset (cons (- (car mpos) (car apos))
- (- (cdr mpos) (cdr apos))))
- (window (selected-window))
- make-pointer-invisible)
- (when (pdf-util-track-mouse-dragging (ev 0.1)
- (when (and (eq window (posn-window (event-start ev)))
- (eq 'image (car-safe (posn-object (event-start ev)))))
- (let ((pdf-view-inhibit-hotspots t)
- (pdf-annot-inhibit-modification-hooks t)
- (pdf-cache-image-inihibit t)
- (xy (posn-object-x-y (event-start ev))))
- (pdf-annot-image-set-position
- a (- (car xy) (car offset))
- (- (cdr xy) (cdr offset)))
- (pdf-view-redisplay))))
- (pdf-annot-run-modified-hooks)))
- nil))
-
- (defun pdf-annot-hotspot-function (page size)
- "Create image hotspots for page PAGE of size SIZE."
- (apply 'nconc (mapcar (lambda (a)
- (unless (eq (pdf-annot-get a 'type)
- 'link)
- (pdf-annot-create-hotspots a size)))
- (pdf-annot-getannots page))))
-
- (defun pdf-annot-create-hotspots (a size)
- "Return a list of image hotspots for annotation A."
- (let ((id (pdf-annot-get-id a))
- (edges (pdf-util-scale
- (pdf-annot-get-display-edges a)
- size 'round))
- (moveable-p (memq (pdf-annot-get a 'type)
- '(file text)))
- hotspots)
- (dolist (e edges)
- (pdf-util-with-edges (e)
- (push `((rect . ((,e-left . ,e-top) . (,e-right . ,e-bot)))
- ,id
- (pointer
- hand
- help-echo
- ,(pdf-annot-print-annotation a)))
- hotspots)))
- (pdf-annot-create-hotspot-binding id moveable-p a)
- hotspots))
-
- ;; FIXME: Define a keymap as a template for this. Much cleaner.
- (defun pdf-annot-create-hotspot-binding (id moveable-p annotation)
- ;; Activating
- (local-set-key
- (vector id 'mouse-1)
- (lambda ()
- (interactive)
- (pdf-annot-activate-annotation annotation)))
- ;; Move
- (when moveable-p
- (local-set-key
- (vector id 'down-mouse-1)
- (lambda (ev)
- (interactive "@e")
- (pdf-annot-mouse-move ev annotation))))
- ;; Context Menu
- (local-set-key
- (vector id 'down-mouse-3)
- (lambda ()
- (interactive "@")
- (popup-menu (pdf-annot-create-context-menu annotation))))
- ;; Everything else
- (local-set-key
- (vector id t)
- 'pdf-util-image-map-mouse-event-proxy))
-
- (defun pdf-annot-show-annotation (a &optional highlight-p window)
- "Make annotation A visible.
-
- Turn to A's page in WINDOW, and scroll it if necessary.
-
- If HIGHLIGHT-P is non-nil, visually distinguish annotation A from
- other annotations."
-
- (save-selected-window
- (when window (select-window window))
- (pdf-util-assert-pdf-window)
- (let ((page (pdf-annot-get a 'page))
- (size (pdf-view-image-size)))
- (unless (= page (pdf-view-current-page))
- (pdf-view-goto-page page))
- (let ((edges (pdf-annot-get-display-edges a)))
- (when highlight-p
- (pdf-view-display-image
- (pdf-view-create-image
- (pdf-cache-renderpage-highlight
- page (car size)
- `("white" "steel blue" 0.35 ,@edges))
- :map (pdf-view-apply-hotspot-functions
- window page size))))
- (pdf-util-scroll-to-edges
- (pdf-util-scale-relative-to-pixel (car edges)))))))
-
- (defun pdf-annot-read-annotation (&optional prompt)
- "Let the user choose a annotation a mouse click using PROMPT."
- (pdf-annot-at-position
- (pdf-util-read-image-position
- (or prompt "Choose a annotation by clicking on it"))))
-
- ;; * ================================================================== *
- ;; * Creating annotations
- ;; * ================================================================== *
-
- (defun pdf-annot-add-annotation (type edges &optional property-alist page)
- "Creates and adds a new annotation of type TYPE to the document.
-
- TYPE determines the kind of annotation to add and maybe one of
- `text', `squiggly', `underline', `strike-out' or `highlight'.
-
- EDGES determines where the annotation will appear on the page.
- If type is `text', this should be a single list of \(LEFT TOP
- RIGHT BOT\). Though, in this case only LEFT and TOP are used,
- since the size of text annotations is fixed. Otherwise EDGES may
- be a list of such elements. All values should be image relative
- coordinates, i.e. in the range \[0;1\].
-
- PROPERTY-ALIST is a list of annotation properties, which will be
- put on the created annotation.
-
- PAGE determines the page of the annotation. It defaults to the
- page currently displayed in the selected window.
-
- Signal an error, if PROPERTY-ALIST contains non-modifiable
- properties or PAGE is nil and the selected window does not
- display a PDF document or creating annotations of type TYPE is
- not supported.
-
- Set buffers modified flag and calls
- `pdf-annot-activate-annotation' if
- `pdf-annot-activate-created-annotations' is non-nil.
-
- Return the new annotation."
-
- (unless (memq type (pdf-info-creatable-annotation-types))
- (error "Unsupported annotation type: %s" type))
- (unless page
- (pdf-util-assert-pdf-window)
- (setq page (pdf-view-current-page)))
- (unless (consp (car-safe edges))
- (setq edges (list edges)))
- (when (and (eq type 'text)
- (> (length edges) 1))
- (error "Edges argument should be a single edge-list for text annotations"))
- (let* ((a (apply 'pdf-info-addannot
- page
- (if (eq type 'text)
- (car edges)
- (apply #'pdf-util-edges-union
- (apply #'append
- (mapcar
- (lambda (e)
- (pdf-info-getselection page e))
- edges))))
- type
- nil
- (if (not (eq type 'text)) edges)))
- (id (pdf-annot-get-id a)))
- (when property-alist
- (condition-case err
- (setq a (pdf-info-editannot id property-alist))
- (error
- (pdf-info-delannot id)
- (signal (car err) (cdr err)))))
- (setq a (pdf-annot-create a))
- (set-buffer-modified-p t)
- (pdf-annot-run-modified-hooks :insert a)
- (when pdf-annot-activate-created-annotations
- (pdf-annot-activate-annotation a))
- a))
-
- (defun pdf-annot-add-text-annotation (pos &optional icon property-alist)
- "Add a new text annotation at POS in the selected window.
-
- POS should be a image position object or a cons \(X . Y\), both
- being image coordinates.
-
- ICON determines how the annotation is displayed and should be
- listed in `pdf-annot-standard-text-icons'. Any other value is ok
- as well, but will render the annotation invisible.
-
- Adjust X and Y accordingly, if the position would render the
- annotation off-page.
-
- Merge ICON as a icon property with PROPERTY-ALIST and
- `pdf-annot-default-text-annotation-properties' and apply the
- result to the created annotation.
-
- See also `pdf-annot-add-annotation'.
-
- Return the new annotation."
-
- (interactive
- (let* ((posn (pdf-util-read-image-position
- "Click where a new text annotation should be added ..."))
- (window (posn-window posn)))
- (select-window window)
- (list posn)))
- (pdf-util-assert-pdf-window)
- (when (posnp pos)
- (setq pos (posn-object-x-y pos)))
- (let ((isize (pdf-view-image-size))
- (x (car pos))
- (y (cdr pos)))
- (unless (and (>= x 0)
- (< x (car isize)))
- (signal 'args-out-of-range (list pos)))
- (unless (and (>= y 0)
- (< y (cdr isize)))
- (signal 'args-out-of-range (list pos)))
- (let ((size (pdf-util-scale-points-to-pixel
- pdf-annot-text-annotation-size 'round)))
- (setcar size (min (car size) (car isize)))
- (setcdr size (min (cdr size) (cdr isize)))
- (cl-decf x (max 0 (- (+ x (car size)) (car isize))))
- (cl-decf y (max 0 (- (+ y (cdr size)) (cdr isize))))
- (pdf-annot-add-annotation
- 'text (pdf-util-scale-pixel-to-relative
- (list x y -1 -1))
- (pdf-annot-merge-alists
- (and icon `((icon . ,icon)))
- property-alist
- pdf-annot-default-text-annotation-properties
- (cdr (assq 'text pdf-annot-default-annotation-properties))
- (cdr (assq t pdf-annot-default-annotation-properties))
- `((color . ,(car pdf-annot-color-history))))))))
-
- (defun pdf-annot-mouse-add-text-annotation (ev)
- (interactive "@e")
- (pdf-annot-add-text-annotation
- (if (eq (car-safe ev)
- 'menu-bar)
- (let (echo-keystrokes)
- (message nil)
- (pdf-util-read-image-position
- "Click where a new text annotation should be added ..."))
- (event-start ev))))
-
- (defun pdf-annot-add-markup-annotation (list-of-edges type &optional color
- property-alist)
- "Add a new markup annotation in the selected window.
-
- LIST-OF-EDGES determines the marked up area and should be a list
- of \(LEFT TOP RIGHT BOT\), each value a relative coordinate.
-
- TYPE should be one of `squiggly', `underline', `strike-out' or
- `highlight'.
-
- Merge COLOR as a color property with PROPERTY-ALIST and
- `pdf-annot-default-markup-annotation-properties' and apply the
- result to the created annotation.
-
- See also `pdf-annot-add-annotation'.
-
- Return the new annotation."
- (interactive
- (list (pdf-view-active-region t)
- (let ((type (completing-read "Markup type (default highlight): "
- '("squiggly" "highlight" "underline" "strike-out")
- nil t)))
- (if (equal type "") 'highlight (intern type)))
- (pdf-annot-read-color)))
- (pdf-util-assert-pdf-window)
- (pdf-annot-add-annotation
- type
- list-of-edges
- (pdf-annot-merge-alists
- (and color `((color . ,color)))
- property-alist
- pdf-annot-default-markup-annotation-properties
- (cdr (assq type pdf-annot-default-annotation-properties))
- (cdr (assq t pdf-annot-default-annotation-properties))
- (when pdf-annot-color-history
- `((color . ,(car pdf-annot-color-history))))
- '((color . "#ffff00")))
- (pdf-view-current-page)))
-
- (defun pdf-annot-add-squiggly-markup-annotation (list-of-edges
- &optional color property-alist)
- "Add a new squiggly annotation in the selected window.
-
- See also `pdf-annot-add-markup-annotation'."
- (interactive (list (pdf-view-active-region t)))
- (pdf-annot-add-markup-annotation list-of-edges 'squiggly color property-alist))
-
- (defun pdf-annot-add-underline-markup-annotation (list-of-edges
- &optional color property-alist)
- "Add a new underline annotation in the selected window.
-
- See also `pdf-annot-add-markup-annotation'."
- (interactive (list (pdf-view-active-region t)))
- (pdf-annot-add-markup-annotation list-of-edges 'underline color property-alist))
-
- (defun pdf-annot-add-strikeout-markup-annotation (list-of-edges
- &optional color property-alist)
- "Add a new strike-out annotation in the selected window.
-
- See also `pdf-annot-add-markup-annotation'."
- (interactive (list (pdf-view-active-region t)))
- (pdf-annot-add-markup-annotation list-of-edges 'strike-out color property-alist))
-
- (defun pdf-annot-add-highlight-markup-annotation (list-of-edges
- &optional color property-alist)
- "Add a new highlight annotation in the selected window.
-
- See also `pdf-annot-add-markup-annotation'."
- (interactive (list (pdf-view-active-region t)))
- (pdf-annot-add-markup-annotation list-of-edges 'highlight color property-alist))
-
- (defun pdf-annot-read-color (&optional prompt)
- "Read and return a color using PROMPT.
-
- Offer `pdf-annot-color-history' as default values."
- (let* ((defaults (append
- (delq nil
- (list
- (cdr (assq 'color
- pdf-annot-default-markup-annotation-properties))
- (cdr (assq 'color
- pdf-annot-default-text-annotation-properties))))
- pdf-annot-color-history))
- (prompt
- (format "%s%s: "
- (or prompt "Color")
- (if defaults (format " (default %s)" (car defaults)) "")))
- (current-completing-read-function completing-read-function)
- (completing-read-function
- (lambda (prompt collection &optional predicate require-match
- initial-input _hist _def inherit-input-method)
- (funcall current-completing-read-function
- prompt collection predicate require-match
- initial-input 'pdf-annot-color-history
- defaults
- inherit-input-method))))
- (read-color prompt)))
-
- (defun pdf-annot-merge-alists (&rest alists)
- "Merge ALISTS into a single one.
-
- Suppresses successive duplicate entries of keys after the first
- occurrence in ALISTS."
-
- (let (merged)
- (dolist (elt (apply 'append alists))
- (unless (assq (car elt) merged)
- (push elt merged)))
- (nreverse merged)))
-
-
- ;; * ================================================================== *
- ;; * Displaying annotation contents
- ;; * ================================================================== *
-
- (defun pdf-annot-print-property (a property)
- "Pretty print annotation A's property PROPERTY."
- (let ((value (pdf-annot-get a property)))
- (cl-case property
- (color
- (propertize (or value "")
- 'face (and value
- `(:background ,value))))
- ((created modified)
- (let ((date value))
- (if (null date)
- "No date"
- (current-time-string date))))
- ;; print verbatim
- (subject
- (or value "No subject"))
- (opacity
- (let ((opacity (or value 1.0)))
- (format "%d%%" (round (* 100 opacity)))))
- (t (format "%s" (or value ""))))))
-
- (defun pdf-annot-print-annotation (a)
- "Pretty print annotation A."
- (or (run-hook-with-args-until-success
- 'pdf-annot-print-annotation-functions a)
- (pdf-annot-print-annotation-default a)))
-
- (defun pdf-annot-print-annotation-default (a)
- "Default pretty printer for annotation A.
-
- The result consists of a header (as printed with
- `pdf-annot-print-annotation-header') a newline and A's contents
- property."
- (concat
- (pdf-annot-print-annotation-header a)
- "\n"
- (pdf-annot-get a 'contents)))
-
- (defun pdf-annot-print-annotation-header (a)
- "Emit a suitable header string for annotation A."
- (let ((header
- (cond
- ((eq 'file (pdf-annot-get a 'type))
- (let ((att (pdf-annot-get-attachment a)))
- (format "File attachment `%s' of %s"
- (or (cdr (assq 'filename att)) "unnamed")
- (if (cdr (assq 'size att))
- (format "size %s" (file-size-human-readable
- (cdr (assq 'size att))))
- "unknown size"))))
- (t
- (format "%s"
- (mapconcat
- 'identity
- (mapcar
- (lambda (property)
- (pdf-annot-print-property
- a property))
- `(subject
- label
- modified))
- ";"))))))
- (setq header (propertize header 'face 'header-line
- 'intangible t 'read-only t))
- ;; This `trick' makes the face apply in a tooltip.
- (propertize header 'display header)))
-
- (defun pdf-annot-print-annotation-latex-maybe (a)
- "Maybe print annotation A's content as a LaTeX fragment.
-
- See `pdf-annot-latex-string-predicate'."
- (when (and (functionp pdf-annot-latex-string-predicate)
- (funcall pdf-annot-latex-string-predicate
- (pdf-annot-get a 'contents)))
- (pdf-annot-print-annotation-latex a)))
-
- (defun pdf-annot-print-annotation-latex (a)
- "Print annotation A's content as a LaTeX fragment.
-
- This compiles A's contents as a LaTeX fragment and puts the
- resulting image as a display property on the contents, prefixed
- by a header."
-
- (let (tempfile)
- (unwind-protect
- (with-current-buffer (pdf-annot-get-buffer a)
- (let* ((page (pdf-annot-get a 'page))
- (header (pdf-annot-print-annotation-header a))
- (contents (pdf-annot-get a 'contents))
- (hash (sxhash (format
- "pdf-annot-print-annotation-latex%s%s%s"
- page header contents)))
- (data (pdf-cache-lookup-image page 0 nil hash))
- (org-format-latex-header
- pdf-annot-latex-header)
- (temporary-file-directory
- (pdf-util-expand-file-name "pdf-annot-print-annotation-latex")))
- (unless (file-directory-p temporary-file-directory)
- (make-directory temporary-file-directory))
- (unless data
- (setq tempfile (make-temp-file "pdf-annot" nil ".png"))
- ;; FIXME: Why is this with-temp-buffer needed (which it is) ?
- (with-temp-buffer
- (org-create-formula-image
- contents tempfile org-format-latex-options t))
- (setq data (pdf-util-munch-file tempfile))
- (if (and (> (length data) 3)
- (equal (substring data 1 4)
- "PNG"))
- (pdf-cache-put-image page 0 data hash)
- (setq data nil)))
- (concat
- header
- "\n"
- (if data
- (propertize
- contents 'display (pdf-view-create-image data))
- (propertize
- contents
- 'display
- (concat
- (propertize "Failed to compile latex fragment\n"
- 'face 'error)
- contents))))))
- (when (and tempfile
- (file-exists-p tempfile))
- (delete-file tempfile)))))
-
- ;; * ================================================================== *
- ;; * Editing annotation contents
- ;; * ================================================================== *
-
- (defvar-local pdf-annot-edit-contents--annotation nil)
- (put 'pdf-annot-edit-contents--annotation 'permanent-local t)
- (defvar-local pdf-annot-edit-contents--buffer nil)
-
- (defcustom pdf-annot-edit-contents-setup-function
- (lambda (a)
- (let ((mode (if (funcall pdf-annot-latex-string-predicate
- (pdf-annot-get a 'contents))
- 'latex-mode
- 'text-mode)))
- (unless (derived-mode-p mode)
- (funcall mode))))
- "A function for setting up, e.g. the major-mode, of the edit buffer.
-
- The function receives one argument, the annotation whose contents
- is about to be edited in this buffer.
-
- The default value turns on `latex-mode' if
- `pdf-annot-latex-string-predicate' returns non-nil on the
- annotation's contents and otherwise `text-mode'. "
- :group 'pdf-annot
- :type 'function)
-
- (defcustom pdf-annot-edit-contents-display-buffer-action
- '((display-buffer-reuse-window
- display-buffer-split-below-and-attach)
- (inhibit-same-window . t)
- (window-height . 0.25))
- "Display action when showing the edit buffer."
- :group 'pdf-annot
- :type display-buffer--action-custom-type)
-
- (defvar pdf-annot-edit-contents-minor-mode-map
- (let ((kmap (make-sparse-keymap)))
- (set-keymap-parent kmap text-mode-map)
- (define-key kmap (kbd "C-c C-c") 'pdf-annot-edit-contents-commit)
- (define-key kmap (kbd "C-c C-q") 'pdf-annot-edit-contents-abort)
- kmap))
-
- (define-minor-mode pdf-annot-edit-contents-minor-mode
- "Active when editing the contents of annotations."
- nil nil nil
- (when pdf-annot-edit-contents-minor-mode
- (message "%s"
- (substitute-command-keys
- "Press \\[pdf-annot-edit-contents-commit] to commit your changes, \\[pdf-annot-edit-contents-abort] to abandon them."))))
-
- (put 'pdf-annot-edit-contents-minor-mode 'permanent-local t)
-
- ;; FIXME: Document pdf-annot-edit-* functions below.
- (defun pdf-annot-edit-contents-finalize (do-save &optional do-kill)
- (when (buffer-modified-p)
- (cond
- ((eq do-save 'ask)
- (save-window-excursion
- (display-buffer (current-buffer) nil (selected-frame))
- (when (y-or-n-p "Save changes to this annotation ?")
- (pdf-annot-edit-contents-save-annotation))))
- (do-save
- (pdf-annot-edit-contents-save-annotation)))
- (set-buffer-modified-p nil))
- (dolist (win (get-buffer-window-list))
- (quit-window do-kill win)))
-
- (defun pdf-annot-edit-contents-save-annotation ()
- (when pdf-annot-edit-contents--annotation
- (pdf-annot-put pdf-annot-edit-contents--annotation
- 'contents
- (buffer-substring-no-properties (point-min) (point-max)))
- (set-buffer-modified-p nil)))
-
- (defun pdf-annot-edit-contents-commit ()
- (interactive)
- (pdf-annot-edit-contents-finalize t))
-
- (defun pdf-annot-edit-contents-abort ()
- (interactive)
- (pdf-annot-edit-contents-finalize nil t))
-
- (defun pdf-annot-edit-contents-noselect (a)
- (with-current-buffer (pdf-annot-get-buffer a)
- (when (and (buffer-live-p pdf-annot-edit-contents--buffer)
- (not (eq a pdf-annot-edit-contents--annotation)))
- (with-current-buffer pdf-annot-edit-contents--buffer
- (pdf-annot-edit-contents-finalize 'ask)))
- (unless (buffer-live-p pdf-annot-edit-contents--buffer)
- (setq pdf-annot-edit-contents--buffer
- (with-current-buffer (get-buffer-create
- (format "*Edit Annotation %s*"
- (buffer-name)))
- (pdf-annot-edit-contents-minor-mode 1)
- (current-buffer))))
- (with-current-buffer pdf-annot-edit-contents--buffer
- (let ((inhibit-read-only t))
- (erase-buffer)
- (save-excursion (insert (pdf-annot-get a 'contents)))
- (set-buffer-modified-p nil))
- (setq pdf-annot-edit-contents--annotation a)
- (funcall pdf-annot-edit-contents-setup-function a)
- (current-buffer))))
-
- (defun pdf-annot-edit-contents (a)
- (select-window
- (display-buffer
- (pdf-annot-edit-contents-noselect a)
- pdf-annot-edit-contents-display-buffer-action)))
-
- (defun pdf-annot-edit-contents-mouse (ev)
- (interactive "@e")
- (let* ((pos (posn-object-x-y (event-start ev)))
- (a (and pos (pdf-annot-at-position pos))))
- (unless a
- (error "No annotation at this position"))
- (pdf-annot-edit-contents a)))
-
-
- ;; * ================================================================== *
- ;; * Listing annotations
- ;; * ================================================================== *
-
- (defcustom pdf-annot-list-display-buffer-action
- '((display-buffer-reuse-window
- display-buffer-pop-up-window)
- (inhibit-same-window . t))
- "Display action used when displaying the list buffer."
- :group 'pdf-annot
- :type display-buffer--action-custom-type)
-
- (defcustom pdf-annot-list-format
- '((page . 3)
- (type . 10)
- (label . 24)
- (date . 24))
- "Annotation properties visible in the annotation list.
-
- It should be a list of \(PROPERTIZE. WIDTH\), where PROPERTY is a
- symbol naming one of supported properties to list and WIDTH its
- desired column-width.
-
- Currently supported properties are page, type, label, date and contents."
- :type '(alist :key-type (symbol))
- :options '((page (integer :value 3 :tag "Column Width"))
- (type (integer :value 10 :tag "Column Width" ))
- (label (integer :value 24 :tag "Column Width"))
- (date (integer :value 24 :tag "Column Width"))
- (contents (integer :value 56 :tag "Column Width")))
- :group 'pdf-annot)
-
- (defcustom pdf-annot-list-highlight-type nil
- "Whether to highlight \"Type\" column annotation list with annotation color."
- :group 'pdf-annot
- :type 'boolean)
-
- (defvar-local pdf-annot-list-buffer nil)
-
- (defvar-local pdf-annot-list-document-buffer nil)
-
- (defvar pdf-annot-list-mode-map
- (let ((km (make-sparse-keymap)))
- (define-key km (kbd "C-c C-f") 'pdf-annot-list-follow-minor-mode)
- (define-key km (kbd "SPC") 'pdf-annot-list-display-annotation-from-id)
- km))
-
- (defun pdf-annot-property-completions (property)
- "Return a list of completion candidates for annotation property PROPERTY.
-
- Return nil, if not available."
- (cl-case property
- (color (pdf-util-color-completions))
- (icon (copy-sequence pdf-annot-standard-text-icons))))
-
- (defun pdf-annot-compare-annotations (a1 a2)
- "Compare annotations A1 and A2.
-
- Return non-nil if A1's page is less than A2's one or if they
- belong to the same page and A1 is displayed above/left of A2."
- (let ((p1 (pdf-annot-get a1 'page))
- (p2 (pdf-annot-get a2 'page)))
- (or (< p1 p2)
- (and (= p1 p2)
- (let ((e1 (pdf-util-scale
- (car (pdf-annot-get-display-edges a1))
- '(1000 . 1000)))
- (e2 (pdf-util-scale
- (car (pdf-annot-get-display-edges a2))
- '(1000 . 1000))))
- (pdf-util-with-edges (e1 e2)
- (or (< e1-top e2-top)
- (and (= e1-top e2-top)
- (<= e1-left e2-left)))))))))
-
- (defun pdf-annot-list-entries ()
- (unless (buffer-live-p pdf-annot-list-document-buffer)
- (error "No PDF document associated with this buffer"))
- (mapcar 'pdf-annot-list-create-entry
- (sort (pdf-annot-getannots nil pdf-annot-list-listed-types
- pdf-annot-list-document-buffer)
- 'pdf-annot-compare-annotations)))
-
- (defun pdf-annot--make-entry-formatter (a)
- (lambda (fmt)
- (let ((entry-type (car fmt))
- (entry-width (cdr fmt))
- ;; Taken from css-mode.el
- (contrasty-color
- (lambda (name)
- (if (> (color-distance name "black") 292485)
- "black" "white")))
- (prune-newlines
- (lambda (str)
- (replace-regexp-in-string "\n" " " str t t))))
- (cl-ecase entry-type
- (date (pdf-annot-print-property a 'modified))
- (page (pdf-annot-print-property a 'page))
- (label (funcall prune-newlines
- (pdf-annot-print-property a 'label)))
- (contents
- (truncate-string-to-width
- (funcall prune-newlines
- (pdf-annot-print-property a 'contents))
- entry-width))
- (type
- (let ((color (pdf-annot-get a 'color))
- (type (pdf-annot-print-property a 'type)))
- (if pdf-annot-list-highlight-type
- (propertize
- type 'face
- `(:background ,color
- :foreground ,(funcall contrasty-color color)))
- type)))))))
-
- (defun pdf-annot-list-create-entry (a)
- "Create a `tabulated-list-entries' entry for annotation A."
- (list (pdf-annot-get-id a)
- (vconcat
- (mapcar (pdf-annot--make-entry-formatter a)
- pdf-annot-list-format))))
-
- (define-derived-mode pdf-annot-list-mode tablist-mode "Annots"
- (let* ((page-sorter
- (lambda (a b)
- (< (string-to-number (aref (cadr a) 0))
- (string-to-number (aref (cadr b) 0)))))
- (format-generator
- (lambda (format)
- (let ((field (car format))
- (width (cdr format)))
- (cl-case field
- (page `("Pg." 3 ,page-sorter :read-only t :right-alight t))
- (t (list
- (capitalize (symbol-name field))
- width t :read-only t)))))))
- (setq tabulated-list-entries 'pdf-annot-list-entries
- tabulated-list-format (vconcat
- (mapcar
- format-generator
- pdf-annot-list-format))
- tabulated-list-padding 2))
- (set-keymap-parent pdf-annot-list-mode-map tablist-mode-map)
- (use-local-map pdf-annot-list-mode-map)
- (when (assq 'type pdf-annot-list-format)
- (setq tablist-current-filter
- `(not (== "Type" "link"))))
- (tabulated-list-init-header))
-
- (defun pdf-annot-list-annotations ()
- "List annotations in a dired like buffer.
-
- \\{pdf-annot-list-mode-map}"
- (interactive)
- (pdf-util-assert-pdf-buffer)
- (let ((buffer (current-buffer)))
- (with-current-buffer (get-buffer-create
- (format "*%s's annots*"
- (file-name-sans-extension
- (buffer-name))))
- (delay-mode-hooks
- (unless (derived-mode-p 'pdf-annot-list-mode)
- (pdf-annot-list-mode))
- (setq pdf-annot-list-document-buffer buffer)
- (tabulated-list-print)
- (setq tablist-context-window-function
- (lambda (id) (pdf-annot-list-context-function id buffer))
- tablist-operations-function 'pdf-annot-list-operation-function)
- (let ((list-buffer (current-buffer)))
- (with-current-buffer buffer
- (setq pdf-annot-list-buffer list-buffer))))
- (run-mode-hooks)
- (pop-to-buffer
- (current-buffer)
- pdf-annot-list-display-buffer-action)
- (tablist-move-to-major-column)
- (tablist-display-context-window))
- (add-hook 'pdf-info-close-document-hook
- 'pdf-annot-list-update nil t)
- (add-hook 'pdf-annot-modified-functions
- 'pdf-annot-list-update nil t)))
-
- (defun pdf-annot-list-goto-annotation (a)
- (with-current-buffer (pdf-annot-get-buffer a)
- (unless (and (buffer-live-p pdf-annot-list-buffer)
- (get-buffer-window pdf-annot-list-buffer))
- (pdf-annot-list-annotations))
- (with-selected-window (get-buffer-window pdf-annot-list-buffer)
- (goto-char (point-min))
- (let ((id (pdf-annot-get-id a)))
- (while (and (not (eobp))
- (not (eq id (tabulated-list-get-id))))
- (forward-line))
- (unless (eq id (tabulated-list-get-id))
- (error "Unable to find annotation"))
- (when (invisible-p (point))
- (tablist-suspend-filter t))
- (tablist-move-to-major-column)))))
-
-
- (defun pdf-annot-list-update (&optional _fn)
- (when (buffer-live-p pdf-annot-list-buffer)
- (with-current-buffer pdf-annot-list-buffer
- (unless tablist-edit-column-minor-mode
- (tablist-revert))
- (tablist-context-window-update))))
-
- (defun pdf-annot-list-context-function (id buffer)
- (with-current-buffer (get-buffer-create "*Contents*")
- (set-window-buffer nil (current-buffer))
- (let ((inhibit-read-only t))
- (erase-buffer)
- (when id
- (save-excursion
- (insert
- (pdf-annot-print-annotation
- (pdf-annot-getannot id buffer)))))
- (read-only-mode 1))))
-
- (defun pdf-annot-list-operation-function (op &rest args)
- (cl-ecase op
- (supported-operations '(delete find-entry))
- (delete
- (cl-destructuring-bind (ids)
- args
- (when (buffer-live-p pdf-annot-list-document-buffer)
- (with-current-buffer pdf-annot-list-document-buffer
- (pdf-annot-with-atomic-modifications
- (dolist (a (mapcar 'pdf-annot-getannot ids))
- (pdf-annot-delete a)))))))
- (find-entry
- (cl-destructuring-bind (id)
- args
- (unless (buffer-live-p pdf-annot-list-document-buffer)
- (error "No PDF document associated with this buffer"))
- (let* ((buffer pdf-annot-list-document-buffer)
- (a (pdf-annot-getannot id buffer))
- (pdf-window (save-selected-window
- (or (get-buffer-window buffer)
- (display-buffer buffer))))
- window)
- (with-current-buffer buffer
- (pdf-annot-activate-annotation a)
- (setq window (selected-window)))
- ;; Make it so that quitting the edit window returns to the
- ;; list window.
- (unless (memq window (list (selected-window) pdf-window))
- (let* ((quit-restore
- (window-parameter window 'quit-restore)))
- (when quit-restore
- (setcar (nthcdr 2 quit-restore) (selected-window))))))))))
-
- (defvar pdf-annot-list-display-annotation--timer nil)
-
- (defun pdf-annot-list-display-annotation-from-id (id)
- (interactive (list (tabulated-list-get-id)))
- (when id
- (unless (buffer-live-p pdf-annot-list-document-buffer)
- (error "PDF buffer was killed"))
- (when (timerp pdf-annot-list-display-annotation--timer)
- (cancel-timer pdf-annot-list-display-annotation--timer))
- (setq pdf-annot-list-display-annotation--timer
- (run-with-idle-timer 0.1 nil
- (lambda (buffer a)
- (when (buffer-live-p buffer)
- (with-selected-window
- (or (get-buffer-window buffer)
- (display-buffer
- buffer
- '(nil (inhibit-same-window . t))))
- (pdf-annot-show-annotation a t))))
- pdf-annot-list-document-buffer
- (pdf-annot-getannot id pdf-annot-list-document-buffer)))))
-
- (define-minor-mode pdf-annot-list-follow-minor-mode
- "" nil nil nil
- (unless (derived-mode-p 'pdf-annot-list-mode)
- (error "No in pdf-annot-list-mode."))
- (cond
- (pdf-annot-list-follow-minor-mode
- (add-hook 'tablist-selection-changed-functions
- 'pdf-annot-list-display-annotation-from-id nil t)
- (let ((id (tabulated-list-get-id)))
- (when id
- (pdf-annot-list-display-annotation-from-id id))))
- (t
- (remove-hook 'tablist-selection-changed-functions
- 'pdf-annot-list-display-annotation-from-id t))))
-
- (provide 'pdf-annot)
- ;;; pdf-annot.el ends here
|