summaryrefslogtreecommitdiff
path: root/lisp/pdf-roll.el
blob: 2cc5d161a3a39ae728c1d428970448e6c421a7fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
;;; pdf-roll.el --- Add continuous scroll. -*- lexical-binding: t -*-

;; Copyright (C) 2013, 2014  Andreas Politz

;; Author: Daniel Nicolai <dalanicolai@gmail.com>
;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:
;;

;;; Code:
(require 'pdf-view)

(put 'pdf-roll 'display '(space :width 25 :height 1000))
(put 'pdf-roll 'evaporate t)
(put 'pdf-roll-margin 'evaporate t)

;;; Custom Variables
(defgroup pdf-roll nil
  "Image roll configurations."
  :group 'pdf-view)

(defface pdf-roll-default `((t :font ,(font-spec :family "monospace" :size 1)))
  "Default face for image roll documents.")

(defcustom pdf-roll-vertical-margin 2
  "Vertical margin between images in pixels, i.e. page separation height."
  :type 'integer)

(defcustom pdf-roll-margin-color "gray"
  "Background color of overlay, i.e. page separation color."
  :type 'color
  :set (lambda (sym color)
         (set-default sym color)
         (put 'pdf-roll-margin 'face `(:background ,color))))

;;; Variables
(defvar pdf-roll--state nil
  "Local variable that tracks window, point and vscroll to handle changes.")

;;; Utility Macros and functions
(defsubst pdf-roll-page-to-pos (page)
  "Get the buffer position displaing PAGE."
  (- (* 4 page) 3))

(defun pdf-roll--pos-overlay (pos window)
  "Return an overlay for WINDOW at POS."
  (cl-find window (overlays-at pos) :key (lambda (ov) (overlay-get ov 'window))))

(defun pdf-roll-page-overlay (&optional page window)
  "Return overlay displaying PAGE in WINDOW."
  (pdf-roll--pos-overlay
   (pdf-roll-page-to-pos (or page (pdf-roll-page-at-current-pos)))
   (or window (selected-window))))

(defun pdf-roll-page-at-current-pos ()
  "Page at point."
  (if (cl-oddp (point))
      (/ (+ (point) 3) 4)
    (error "No page is displayed at current position (%s)" (point))))

(defun pdf-roll-set-vscroll (vscroll win)
  "Set vscroll to VSCROLL in window WIN."
  (image-mode-winprops win t)
  (image-mode-window-put 'vscroll vscroll win)
  (set-window-vscroll win vscroll t))

;;; Displaying/Undisplaying pages
(defun pdf-roll-maybe-slice-image (image &optional window inhibit-slice-p)
  "Return a sliced IMAGE if `pdf-view-current-slice' in WINDOW is non-nil.
If INHIBIT-SLICE-P is non-nil, disregard `pdf-view-current-slice'."
  (if-let ((slice (pdf-view-current-slice window))
           ((not inhibit-slice-p)))
      (list (cons 'slice
                  (pdf-util-scale slice (image-size image t) 'round))
            image)
    image))

(defun pdf-roll-display-image (image page &optional window inhibit-slice-p)
  "Display IMAGE for PAGE in WINDOW.
If INHIBIT-SLICE-P is non-nil, disregard `pdf-view-current-slice'."
  (let* ((image (pdf-roll-maybe-slice-image image window inhibit-slice-p))
         (size (image-display-size image t))
         (overlay (pdf-roll-page-overlay page window))
         (margin-pos (+ (pdf-roll-page-to-pos page) 2))
         (margin-overlay (pdf-roll--pos-overlay margin-pos window))
         (offset (when (> (window-width window t) (car size))
                   `(space :width (,(/ (- (window-width window t) (car size)) 2))))))
    (overlay-put overlay 'display image)
    (overlay-put overlay 'line-prefix offset)
    (overlay-put margin-overlay 'display `(space :width (,(car size)) :height (,pdf-roll-vertical-margin)))
    (overlay-put margin-overlay 'line-prefix offset)
    (cdr size)))

(defun pdf-roll-display-page (page window &optional force)
  "Display PAGE in WINDOW.
With FORCE non-nil display fetch page again even if it is already displayed."
  (let ((display (overlay-get (pdf-roll-page-overlay page window) 'display)))
    (if (or force (not display) (eq (car display) 'space))
        (pdf-roll-display-image (pdf-view-create-page page window) page window)
      (cdr (image-display-size display t)))))

(defun pdf-roll-display-pages (page &optional window force pscrolling)
  "Display pages to fill the WINDOW starting from PAGE.
If FORCE is non-nill redisplay a page even if it is already displayed."
  (let (displayed
        (available-height (window-pixel-height window)))
    (when (and pscrolling (> page 1))
      (pdf-roll-display-page (1- page) window force)
      (push (1- page) displayed))
    (let ((vscroll (image-mode-window-get 'vscroll window))
          (im-height (pdf-roll-display-page page window force)))
      (pdf-roll-set-vscroll (min vscroll (1- im-height)) window)
      (cl-callf - available-height (- im-height (window-vscroll window t))))
    (push page displayed)
    (while (and (> available-height 0) (< page (pdf-cache-number-of-pages)))
      (cl-callf - available-height (pdf-roll-display-page (cl-incf page) window force))
      (push page displayed))
    (when (and pscrolling (< page (pdf-cache-number-of-pages)))
      (pdf-roll-display-page (cl-incf page) window force)
      (push page displayed))
    ;; store displayed images for determining which images to update when update
    ;; is triggered
    (cl-callf cl-union (image-mode-window-get 'displayed-pages window) displayed)
    displayed))

(defun pdf-roll-undisplay-pages (pages &optional window)
  "Undisplay PAGES from WINDOW.
Replaces the display property of the overlay holding a page with a space."
  (dolist (page pages)
    (overlay-put (pdf-roll-page-overlay page window)
                 'display (get 'pdf-roll 'display))))

;;; State Management
(defun pdf-roll-new-window-function (&optional win)
  "Setup image roll in a new window WIN.
If the buffer is newly created, then it does not contain any
overlay and this function erases the buffer contents, after which
it inserts empty spaces that each hold a overlay. If the buffer
already has overlays (i.e. a second or subsequent window is
created), the function simply copies the overlays and adds the
new window as window overlay-property to each overlay.

This function should be added to pdf-roll (continuous scroll)
minor mode commands, after erasing the buffer to create the
overlays."
  (setq win (or (and (windowp win) win) (selected-window)))
  (if (not (overlays-at 1))
      (let ((pages (pdf-cache-number-of-pages))
            (inhibit-read-only t))
        (erase-buffer)
        (setq pdf-roll--state (list t))
        (dotimes (i (* 2 pages))
          (insert " ")
          (let ((o (make-overlay (1- (point)) (point))))
            (overlay-put o 'category (if (eq 0 (mod i 2)) 'pdf-roll 'pdf-roll-margin))
            (overlay-put o 'window win))
          (insert "\n"))
        (delete-char -1)
        (set-buffer-modified-p nil))
    (unless (pdf-roll-page-overlay 1 win)
      (dotimes (i (/ (point-max) 2))
        (overlay-put (copy-overlay (car (overlays-at (1+ (* 2 i)))))
                     'window win))
      (dolist (win-st pdf-roll--state)
        (when-let ((win-old (car-safe win-st))
                   ((not (window-live-p win-old))))
          (remove-overlays (point-min) (point-max) 'window win-old)))
      (cl-callf2 cl-delete-if-not #'window-live-p pdf-roll--state :key #'car-safe)))
  ;; initial `pdf-roll-redisplay' needs to know which page(s) to display
  (cl-callf or (pdf-view-current-page win) 1)
  (cl-callf or (image-mode-window-get 'vscroll win) 0))

(defun pdf-roll-redisplay (&optional window)
  "Analogue of `pdf-view-redisplay' for WINDOW."
  (setq window (if (windowp window) window (selected-window)))
  (when (pdf-roll-page-overlay 1 window)
    (setf (alist-get window pdf-roll--state) nil)
    (force-window-update window)))

(defun pdf-roll-pre-redisplay (win)
  "Handle modifications to the state in window WIN.
It should be added to `pre-redisplay-functions' buffer locally."
  (with-demoted-errors "Error in image roll pre-redisplay: %S"
    (unless (pdf-roll-page-overlay 1 win)
      (pdf-roll-new-window-function win))
    (let* ((state (alist-get win pdf-roll--state))
           (pscrolling (memq last-command
                             '(pixel-scroll-precision pixel-scroll-start-momentum
                               pixel-scroll-interpolate-up pixel-scroll-interpolate-down)))
           (page (progn (when pscrolling
                          (setf (pdf-view-current-page win)
                                (/ (min (+ (window-start win) 5) (point-max)) 4)))
                        (pdf-view-current-page win)))
           (height (window-pixel-height win))
           (vscroll (image-mode-window-get 'vscroll win))
           (size-changed (not (and (eq height (nth 1 state))
                                   (eq (window-pixel-width win) (nth 2 state)))))
           (page-changed (not (eq page (nth 0 state))))
           (vscroll-changed (not (eq vscroll (nth 3 state))))
           (start (pdf-roll-page-to-pos page)))
      (if (and pscrolling
               (or (not (eq start (- (point-max) 3)))
                   (let ((visible-pixels (nth 4 (pos-visible-in-window-p start win t))))
                     (and visible-pixels (> visible-pixels (/ (window-text-height win t) 2))))
                   (prog1 nil (message "End of buffer"))))
          (progn (image-mode-window-put 'vscroll (window-vscroll win t) win)
                 (image-mode-window-put 'hscroll (window-hscroll win)) win)
        (set-window-vscroll win vscroll t)
        (set-window-hscroll win (or (image-mode-window-get 'hscroll win) 0))
        (set-window-start win start t))
      (setq disable-point-adjustment t)
      (when (or size-changed page-changed vscroll-changed)
        (let ((old (image-mode-window-get 'displayed-pages win))
              (new (pdf-roll-display-pages page win size-changed pscrolling)))
          ;; If images/pages are small enough (or after jumps), there
          ;; might be multiple image that need to get updated
          (pdf-roll-undisplay-pages (cl-set-difference old new) win)
          (image-mode-window-put 'displayed-pages new win)
          (set-window-point win (+ start
                                   (if (pos-visible-in-window-p (+ 2 start) win) 2 0))))
        (setf (alist-get win pdf-roll--state)
              `(,page ,height ,(window-pixel-width win) ,vscroll nil))
        (when page-changed (run-hooks 'pdf-view-after-change-page-hook))))))

;;; Page navigation commands
(defun pdf-roll-goto-page-start ()
  "Go to the start of the first displayed page."
  (interactive)
  (pdf-roll-set-vscroll 0 nil))

(defun pdf-roll-goto-page (page &optional window)
  "Go to PAGE in WINDOW."
  (interactive
   (list (if current-prefix-arg
             (prefix-numeric-value current-prefix-arg)
           (read-number "Page: "))))
  (unless (and (>= page 1)
               (<= page (pdf-cache-number-of-pages)))
    (error "No such page: %d" page))
  (setf (pdf-view-current-page window) page)
  (pdf-roll-set-vscroll 0 window))

(defun pdf-roll-next-page (&optional n)
  "Go to next page or next Nth page."
  (interactive "p")
  (pdf-roll-goto-page (+ (pdf-roll-page-at-current-pos) n)))

(defun pdf-roll-previous-page (&optional n)
  "Go to previous page or previous Nth page."
  (interactive "p")
  (pdf-roll-next-page (- n)))

;;; Scrolling Commands
(defun pdf-roll-scroll-forward (&optional n window pixels)
  "Scroll image N lines forward in WINDOW.
Line height is determined by `frame-char-height'. When N is negative
scroll backward instead. With a prefix arg N is its numeric value.

If PIXELS is non-nil N is number of pixels instead of lines."
  (interactive (list (prefix-numeric-value current-prefix-arg)))
  (setq n (* (or n 1) (if pixels 1 (frame-char-height))))
  (setq window (or window (selected-window)))
  (when (> 0 n) (pdf-roll-scroll-backward (- n) window))
  (let ((pos (goto-char (window-start window))))
    (while (let* ((data (pos-visible-in-window-p (point) window t))
                  (occupied-pixels (cond ((nth 2 data) (nth 4 data))
                                         (data (line-pixel-height))
                                         (t (pdf-roll-display-page
                                             (pdf-roll-page-at-current-pos) window)))))
             (if (eq (point) (- (point-max) 3))
                 (prog1 nil
                   (setq n (min n (max 0 (- occupied-pixels (/ (window-text-height window t) 2)))))
                   (message "End of buffer"))
               (when (>= n occupied-pixels)
                 (cl-decf n occupied-pixels))))
      (forward-char 4))
    (setf (pdf-view-current-page window) (pdf-roll-page-at-current-pos))
    (pdf-roll-set-vscroll (+ (if (eq pos (point)) (window-vscroll window t) 0) n)
                          window)))

(defun pdf-roll-scroll-backward (&optional n window pixels)
  "Scroll image N lines backwards in WINDOW.
Line height is determined by `frame-char-height'. When N is negative
scroll forward instead. With a prefix arg N is its numeric value.

If PIXELS is non-nil N is number of pixels instead of lines."
  (interactive (list (prefix-numeric-value current-prefix-arg)))
  (setq n (* (or n 1) (if pixels 1 (frame-char-height))))
  (setq window (or window (selected-window)))
  (when (> 0 n) (pdf-roll-scroll-backward (- n) window))
  (goto-char (window-start window))
  (let* ((data (pos-visible-in-window-p (point) window t))
         (pixels-top (if (nth 2 data) (nth 2 data) 0)))
    (if (< n pixels-top)
        (pdf-roll-set-vscroll (- (window-vscroll window t) n)
                                window)
      (cl-decf n pixels-top)
      (while (and (if (bobp)
                      (prog1 nil (message "Beginning of buffer."))
                    t)
                  (progn (forward-char -4)
                         (pdf-roll-display-page
                          (pdf-roll-page-at-current-pos) window)
                         (cl-decf n (line-pixel-height)))
                  (> n 0)))
      (pdf-roll-set-vscroll (- n) window)))
  (setf (pdf-view-current-page window) (pdf-roll-page-at-current-pos)))

(defun pdf-roll-scroll-screen-forward (&optional arg)
  "Scroll forward by (almost) ARG many full screens."
  (interactive "p")
  (pdf-roll-scroll-forward
   (- (* (window-text-height nil t) arg) (* next-screen-context-lines (frame-char-height)))
   nil t))

(defun pdf-roll-scroll-screen-backward (&optional arg)
  "Scroll backward by (almost) ARG many full screens."
  (interactive "p")
  (pdf-roll-scroll-backward
   (- (* (window-text-height nil t) arg) (* next-screen-context-lines (frame-char-height)))
   nil t))

;;; Minor mode
(defun pdf-roll-initialize (&rest _args)
  "Fun to initialize `pdf-view-roll-minor-mode'.
It is also added to `revert-buffer-function'."
  (let ((inhibit-read-only t))
    (erase-buffer)
    (remove-overlays))
  (image-mode-window-put 'displayed-pages nil)
  (pdf-roll-new-window-function))

;;;###autoload
(define-minor-mode pdf-view-roll-minor-mode
  "If enabled display document on a virtual scroll providing continuous scrolling."
  :lighter " Continuous"
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map [remap pdf-view-previous-line-or-previous-page] 'pdf-roll-scroll-backward)
            (define-key map [remap pdf-view-next-line-or-next-page] 'pdf-roll-scroll-forward)
            (define-key map [remap pdf-view-scroll-down-or-previous-page] 'pdf-roll-scroll-backward)
            (define-key map [remap pdf-view-scroll-up-or-next-page] 'pdf-roll-scroll-forward)
            (define-key map [remap mouse-set-point] 'ignore)
            (define-key map (kbd "S-<next>") 'pdf-roll-scroll-screen-forward)
            (define-key map (kbd "S-<prior>") 'pdf-roll-scroll-screen-backward)
            map)
  :version 28.1

  (cond (pdf-view-roll-minor-mode
         (setq-local face-remapping-alist '((default . pdf-roll-default))
                     mwheel-scroll-up-function #'pdf-roll-scroll-forward
                     mwheel-scroll-down-function #'pdf-roll-scroll-backward)

         (remove-hook 'window-configuration-change-hook 'image-mode-reapply-winprops t)
         (remove-hook 'window-configuration-change-hook 'pdf-view-redisplay-some-windows t)
         (remove-hook 'image-mode-new-window-functions#'pdf-view-new-window-function t)

         (add-hook 'pre-redisplay-functions 'pdf-roll-pre-redisplay nil t)
         (add-hook 'pdf-roll-after-change-page-hook 'pdf-history-before-change-page-hook nil t)

         (add-function :after (local 'revert-buffer-function) #'pdf-roll-initialize)

         (make-local-variable 'pdf-roll--state)

         (when (local-variable-p 'pixel-scroll-precision-mode)
           (kill-local-variable 'pixel-scroll-precision-mode)
           (kill-local-variable 'mwheel-coalesce-scroll-events))

         (pdf-roll-initialize))
        (t
         (setq-local mwheel-scroll-up-function #'pdf-view-scroll-up-or-next-page
                     mwheel-scroll-down-function #'pdf-view-scroll-down-or-previous-page)

         (add-hook 'window-configuration-change-hook 'image-mode-reapply-winprops nil t)
         (add-hook 'window-configuration-change-hook 'pdf-view-redisplay-some-windows nil t)
         (add-hook 'image-mode-new-window-functions #'pdf-view-new-window-function nil t)

         (remove-function (local 'revert-buffer-function) #'pdf-roll-initialize)

         (remove-hook 'pre-redisplay-functions 'pdf-roll-pre-redisplay t)
         (remove-hook 'pdf-roll-after-change-page-hook 'pdf-history-before-change-page-hook t)

         (kill-local-variable 'pdf-roll--state)

         (when (bound-and-true-p pixel-scroll-precision-mode)
             (setq-local pixel-scroll-precision-mode nil)
             (setq-local mwheel-coalesce-scroll-events t))

         (let ((inhibit-read-only t))
           (remove-overlays)
           (image-mode-window-put 'displayed-pages nil)
           (pdf-view-new-window-function (list (selected-window)))
           (set-buffer-modified-p nil)))))

(defun pdf-roll--get-display-property ()
  "`:before-until' advice for `image-get-display-property'.
`image-get-display-property' looks at the `point-min'. This function instead
returns the display property for the current page if `pdf-view-roll-minor-mode'
is non-nil."
  (when pdf-view-roll-minor-mode
    (get-char-property (pdf-roll-page-to-pos (pdf-view-current-page))
                       'display
                       (if (eq (window-buffer) (current-buffer))
                           (selected-window)))))

(advice-add 'image-get-display-property :before-until #'pdf-roll--get-display-property)

(provide 'pdf-roll)

;;; pdf-roll.el ends here