1 ;;; xwidget.el --- api functions for xwidgets -*- lexical-binding: t -*-
3 ;; Copyright (C) 2011-2016 Free Software Foundation, Inc.
5 ;; Author: Joakim Verona (joakim@verona.se)
7 ;; This file is part of GNU Emacs.
9 ;; GNU Emacs is free software: you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22 ;; --------------------------------------------------------------------
26 ;; See xwidget.c for more api functions.
28 ;; This breaks compilation when we don't have xwidgets.
29 ;; And is pointless when we do, since it's in C and so preloaded.
30 ;;(require 'xwidget-internal)
37 (defcustom xwidget-webkit-scroll-behavior
'native
38 "Scrolling behavior of the webkit instance.
39 The possible values are: `native' or `image'."
41 :group
'frames
; TODO add xwidgets group if more options are added
42 :type
'(choice (const native
) (const image
)))
44 (declare-function make-xwidget
"xwidget.c"
45 (beg end type title width height arguments
&optional buffer
))
46 (declare-function xwidget-set-adjustment
"xwidget.c"
47 (xwidget axis relative value
))
48 (declare-function xwidget-buffer
"xwidget.c" (xwidget))
49 (declare-function xwidget-webkit-get-title
"xwidget.c" (xwidget))
50 (declare-function xwidget-size-request
"xwidget.c" (xwidget))
51 (declare-function xwidget-resize
"xwidget.c" (xwidget new-width new-height
))
52 (declare-function xwidget-webkit-execute-script
"xwidget.c" (xwidget script
))
53 (declare-function xwidget-webkit-goto-uri
"xwidget.c" (xwidget uri
))
54 (declare-function xwidget-plist
"xwidget.c" (xwidget))
55 (declare-function set-xwidget-plist
"xwidget.c" (xwidget plist
))
56 (declare-function xwidget-view-window
"xwidget.c" (xwidget-view))
57 (declare-function xwidget-view-model
"xwidget.c" (xwidget-view))
58 (declare-function delete-xwidget-view
"xwidget.c" (xwidget-view))
59 (declare-function get-buffer-xwidgets
"xwidget.c" (buffer))
61 (defun xwidget-insert (pos type title width height
&optional args
)
62 "Insert an xwidget at position POS.
63 Supply the xwidget's TYPE, TITLE, WIDTH, and HEIGHT.
64 See `make-xwidget' for the possible TYPE values.
65 The usage of optional argument ARGS depends on the xwidget.
66 This returns the result of `make-xwidget'."
68 (let ((id (make-xwidget (point) (point)
69 type title width height args
)))
70 (put-text-property (point) (+ 1 (point))
71 'display
(list 'xwidget
':xwidget id
))
74 (defun xwidget-at (pos)
75 "Return xwidget at POS."
76 ;; TODO this function is a bit tedious because the C layer isn't well
77 ;; protected yet and xwidgetp apparently doesn't work yet.
78 (let* ((disp (get-text-property pos
'display
))
79 (xw (car (cdr (cdr disp
)))))
80 ;;(if (xwidgetp xw) xw nil)
81 (if (equal 'xwidget
(car disp
)) xw
)))
85 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
88 (require 'image-mode
);;for some image-mode alike functionality
91 (defun xwidget-webkit-browse-url (url &optional new-session
)
92 "Ask xwidget-webkit to browse URL.
93 NEW-SESSION specifies whether to create a new xwidget-webkit session.
94 Interactively, URL defaults to the string looking like a url around point."
97 (browse-url-interactive-arg "xwidget-webkit URL: "
98 ;;(xwidget-webkit-current-url)
102 (xwidget-webkit-new-session url
)
103 (xwidget-webkit-goto-url url
))))
106 ;; - check that the webkit support is compiled in
107 (defvar xwidget-webkit-mode-map
108 (let ((map (make-sparse-keymap)))
109 (define-key map
"g" 'xwidget-webkit-browse-url
)
110 (define-key map
"a" 'xwidget-webkit-adjust-size-dispatch
)
111 (define-key map
"b" 'xwidget-webkit-back
)
112 (define-key map
"r" 'xwidget-webkit-reload
)
113 (define-key map
"t" (lambda () (interactive) (message "o"))) ;FIXME: ?!?
114 (define-key map
"\C-m" 'xwidget-webkit-insert-string
)
115 (define-key map
"w" 'xwidget-webkit-current-url
)
117 ;;similar to image mode bindings
118 (define-key map
(kbd "SPC") 'xwidget-webkit-scroll-up
)
119 (define-key map
(kbd "DEL") 'xwidget-webkit-scroll-down
)
121 (define-key map
[remap scroll-up
] 'xwidget-webkit-scroll-up
)
122 (define-key map
[remap scroll-up-command
] 'xwidget-webkit-scroll-up
)
124 (define-key map
[remap scroll-down
] 'xwidget-webkit-scroll-down
)
125 (define-key map
[remap scroll-down-command
] 'xwidget-webkit-scroll-down
)
127 (define-key map
[remap forward-char
] 'xwidget-webkit-scroll-forward
)
128 (define-key map
[remap backward-char
] 'xwidget-webkit-scroll-backward
)
129 (define-key map
[remap right-char
] 'xwidget-webkit-scroll-forward
)
130 (define-key map
[remap left-char
] 'xwidget-webkit-scroll-backward
)
131 ;; (define-key map [remap previous-line] 'image-previous-line)
132 ;; (define-key map [remap next-line] 'image-next-line)
134 ;; (define-key map [remap move-beginning-of-line] 'image-bol)
135 ;; (define-key map [remap move-end-of-line] 'image-eol)
136 ;; (define-key map [remap beginning-of-buffer] 'image-bob)
137 ;; (define-key map [remap end-of-buffer] 'image-eob)
139 "Keymap for `xwidget-webkit-mode'.")
141 (defun xwidget-webkit-scroll-up ()
143 Depending on the value of `xwidget-webkit-scroll-behavior',
144 this scrolls in 'native' fashion, or like `image-mode' would."
146 (if (eq xwidget-webkit-scroll-behavior
'native
)
147 (xwidget-set-adjustment (xwidget-webkit-last-session) 'vertical t
50)
150 (defun xwidget-webkit-scroll-down ()
152 Depending on the value of `xwidget-webkit-scroll-behavior',
153 this scrolls in 'native' fashion, or like `image-mode' would."
155 (if (eq xwidget-webkit-scroll-behavior
'native
)
156 (xwidget-set-adjustment (xwidget-webkit-last-session) 'vertical t -
50)
157 (image-scroll-down)))
159 (defun xwidget-webkit-scroll-forward ()
160 "Scroll webkit forwards.
161 Depending on the value of `xwidget-webkit-scroll-behavior',
162 this scrolls in 'native' fashion, or like `image-mode' would."
164 (if (eq xwidget-webkit-scroll-behavior
'native
)
165 (xwidget-set-adjustment (xwidget-webkit-last-session) 'horizontal t
50)
166 (xwidget-webkit-scroll-forward))) ; FIXME infloop!
168 (defun xwidget-webkit-scroll-backward ()
169 "Scroll webkit backwards.
170 Depending on the value of `xwidget-webkit-scroll-behavior',
171 this scrolls in 'native' fashion, or like `image-mode' would."
173 (if (eq xwidget-webkit-scroll-behavior
'native
)
174 (xwidget-set-adjustment (xwidget-webkit-last-session) 'horizontal t -
50)
175 (xwidget-webkit-scroll-backward))) ; FIXME infloop!
178 ;; The xwidget event needs to go into a higher level handler
179 ;; since the xwidget can generate an event even if it's offscreen.
180 ;; TODO this needs to use callbacks and consider different xwidget event types.
181 (define-key (current-global-map) [xwidget-event
] #'xwidget-event-handler
)
182 (defun xwidget-log (&rest msg
)
183 "Log MSG to a buffer."
184 (let ((buf (get-buffer-create " *xwidget-log*")))
185 (with-current-buffer buf
186 (insert (apply #'format msg
))
189 (defun xwidget-event-handler ()
190 "Receive xwidget event."
192 (xwidget-log "stuff happened to xwidget %S" last-input-event
)
194 ((xwidget-event-type (nth 1 last-input-event
))
195 (xwidget (nth 2 last-input-event
))
196 ;;(xwidget-callback (xwidget-get xwidget 'callback))
197 ;;TODO stopped working for some reason
199 ;;(funcall xwidget-callback xwidget xwidget-event-type)
200 (message "xw callback %s" xwidget
)
201 (funcall 'xwidget-webkit-callback xwidget xwidget-event-type
)))
203 (defun xwidget-webkit-callback (xwidget xwidget-event-type
)
204 "Callback for xwidgets.
205 XWIDGET instance, XWIDGET-EVENT-TYPE depends on the originating xwidget."
206 (if (not (buffer-live-p (xwidget-buffer xwidget
)))
208 "error: callback called for xwidget with dead buffer")
209 (with-current-buffer (xwidget-buffer xwidget
)
210 (let* ((strarg (nth 3 last-input-event
)))
211 (cond ((eq xwidget-event-type
'document-load-finished
)
212 (xwidget-log "webkit finished loading: '%s'"
213 (xwidget-webkit-get-title xwidget
))
214 ;;TODO - check the native/internal scroll
215 ;;(xwidget-adjust-size-to-content xwidget)
216 (xwidget-webkit-adjust-size-dispatch) ;;TODO xwidget arg
217 (rename-buffer (format "*xwidget webkit: %s *"
218 (xwidget-webkit-get-title xwidget
)))
219 (pop-to-buffer (current-buffer)))
220 ((eq xwidget-event-type
221 'navigation-policy-decision-requested
)
222 (if (string-match ".*#\\(.*\\)" strarg
)
223 (xwidget-webkit-show-id-or-named-element
225 (match-string 1 strarg
))))
226 (t (xwidget-log "unhandled event:%s" xwidget-event-type
)))))))
228 (defvar bookmark-make-record-function
)
229 (define-derived-mode xwidget-webkit-mode
230 special-mode
"xwidget-webkit" "Xwidget webkit view mode."
231 (setq buffer-read-only t
)
232 (setq-local bookmark-make-record-function
233 #'xwidget-webkit-bookmark-make-record
)
234 ;; Keep track of [vh]scroll when switching buffers
235 (image-mode-setup-winprops))
237 (defun xwidget-webkit-bookmark-make-record ()
238 "Integrate Emacs bookmarks with the webkit xwidget."
239 (nconc (bookmark-make-record-default t t
)
240 `((page .
,(xwidget-webkit-current-url))
241 (handler .
(lambda (bmk) (browse-url
242 (bookmark-prop-get bmk
'page
)))))))
245 (defvar xwidget-webkit-last-session-buffer nil
)
247 (defun xwidget-webkit-last-session ()
248 "Last active webkit, or nil."
249 (if (buffer-live-p xwidget-webkit-last-session-buffer
)
250 (with-current-buffer xwidget-webkit-last-session-buffer
251 (xwidget-at (point-min)))
254 (defun xwidget-webkit-current-session ()
255 "Either the webkit in the current buffer, or the last one used.
256 The latter might be nil."
257 (or (xwidget-at (point-min)) (xwidget-webkit-last-session)))
259 (defun xwidget-adjust-size-to-content (xw)
260 "Resize XW to content."
261 ;; xwidgets doesn't support widgets that have their own opinions about
262 ;; size well, yet this reads the desired size and resizes the Emacs
263 ;; allocated area accordingly.
264 (let ((size (xwidget-size-request xw
)))
265 (xwidget-resize xw
(car size
) (cadr size
))))
268 (defvar xwidget-webkit-activeelement-js
"
269 function findactiveelement(doc){
270 //alert(doc.activeElement.value);
271 if(doc.activeElement.value != undefined){
272 return doc.activeElement;
274 // recurse over the child documents:
275 var frames = doc.getElementsByTagName('frame');
276 for (var i = 0; i < frames.length; i++)
278 var d = frames[i].contentDocument;
279 var rv = findactiveelement(d);
291 "javascript that finds the active element."
292 ;; Yes it's ugly, because:
293 ;; - there is apparently no way to find the active frame other than recursion
294 ;; - the js "for each" construct misbehaved on the "frames" collection
295 ;; - a window with no frameset still has frames.length == 1, but
296 ;; frames[0].document.activeElement != document.activeElement
297 ;;TODO the activeelement type needs to be examined, for iframe, etc.
300 (defun xwidget-webkit-insert-string (xw str
)
301 "Insert string STR in the active field in the webkit XW."
302 ;; Read out the string in the field first and provide for edit.
304 (let* ((xww (xwidget-webkit-current-session))
308 (xwidget-webkit-execute-script xww xwidget-webkit-activeelement-js
)
309 (xwidget-webkit-execute-script-rv
311 "findactiveelement(document).value;")))
312 (field-type (xwidget-webkit-execute-script-rv
314 "findactiveelement(document).type;")))
316 (cond ((equal "text" field-type
)
317 (read-string "Text: " field-value
))
318 ((equal "password" field-type
)
319 (read-passwd "Password: " nil field-value
))
320 ((equal "textarea" field-type
)
321 (xwidget-webkit-begin-edit-textarea xww field-value
))))))
322 (xwidget-webkit-execute-script
324 (format "findactiveelement(document).value='%s'" str
)))
326 (defvar xwidget-xwbl
)
327 (defun xwidget-webkit-begin-edit-textarea (xw text
)
328 "Start editing of a webkit text area.
329 XW is the xwidget identifier, TEXT is retrieved from the webkit."
331 (generate-new-buffer "textarea"))
332 (set (make-local-variable 'xwidget-xwbl
) xw
)
335 (defun xwidget-webkit-end-edit-textarea ()
336 "End editing of a webkit text area."
338 (goto-char (point-min))
339 (while (search-forward "\n" nil t
)
340 (replace-match "\\n" nil t
))
341 (xwidget-webkit-execute-script
343 (format "findactiveelement(document).value='%s'"
344 (buffer-substring (point-min) (point-max))))
345 ;;TODO convert linefeed to \n
348 (defun xwidget-webkit-show-named-element (xw element-name
)
349 "Make webkit xwidget XW show a named element ELEMENT-NAME.
350 For example, use this to display an anchor."
351 (interactive (list (xwidget-webkit-current-session)
352 (read-string "Element name: ")))
353 ;;TODO since an xwidget is an Emacs object, it is not trivial to do
354 ;; some things that are taken for granted in a normal browser.
355 ;; scrolling an anchor/named-element into view is one such thing.
356 ;; This function implements a proof-of-concept for this. Problems
357 ;; remaining: - The selected window is scrolled but this is not
358 ;; always correct - This needs to be interfaced into browse-url
359 ;; somehow. The tricky part is that we need to do this in two steps:
360 ;; A: load the base url, wait for load signal to arrive B: navigate
361 ;; to the anchor when the base url is finished rendering
363 ;; This part figures out the Y coordinate of the element
364 (let ((y (string-to-number
365 (xwidget-webkit-execute-script-rv
368 "document.getElementsByName('%s')[0].getBoundingClientRect().top"
371 ;; Now we need to tell Emacs to scroll the element into view.
372 (xwidget-log "scroll: %d" y
)
373 (set-window-vscroll (selected-window) y t
)))
375 (defun xwidget-webkit-show-id-element (xw element-id
)
376 "Make webkit xwidget XW show an id-element ELEMENT-ID.
377 For example, use this to display an anchor."
378 (interactive (list (xwidget-webkit-current-session)
379 (read-string "Element id: ")))
380 (let ((y (string-to-number
381 (xwidget-webkit-execute-script-rv
383 (format "document.getElementById('%s').getBoundingClientRect().top"
386 ;; Now we need to tell Emacs to scroll the element into view.
387 (xwidget-log "scroll: %d" y
)
388 (set-window-vscroll (selected-window) y t
)))
390 (defun xwidget-webkit-show-id-or-named-element (xw element-id
)
391 "Make webkit xwidget XW show a name or element id ELEMENT-ID.
392 For example, use this to display an anchor."
393 (interactive (list (xwidget-webkit-current-session)
394 (read-string "Name or element id: ")))
395 (let* ((y1 (string-to-number
396 (xwidget-webkit-execute-script-rv
398 (format "document.getElementsByName('%s')[0].getBoundingClientRect().top" element-id
)
400 (y2 (string-to-number
401 (xwidget-webkit-execute-script-rv
403 (format "document.getElementById('%s').getBoundingClientRect().top" element-id
)
406 ;; Now we need to tell Emacs to scroll the element into view.
407 (xwidget-log "scroll: %d" y3
)
408 (set-window-vscroll (selected-window) y3 t
)))
410 (defun xwidget-webkit-adjust-size-to-content ()
411 "Adjust webkit to content size."
413 (xwidget-adjust-size-to-content (xwidget-webkit-current-session)))
415 (defun xwidget-webkit-adjust-size-dispatch ()
416 "Adjust size according to mode."
418 (if (eq xwidget-webkit-scroll-behavior
'native
)
419 (xwidget-webkit-adjust-size-to-window)
420 (xwidget-webkit-adjust-size-to-content))
421 ;; The recenter is intended to correct a visual glitch.
422 ;; It errors out if the buffer isn't visible, but then we don't get
423 ;; the glitch, so silence errors.
425 (recenter-top-bottom)))
427 (defun xwidget-webkit-adjust-size-to-window ()
428 "Adjust webkit to window."
430 (xwidget-resize (xwidget-webkit-current-session) (window-pixel-width)
431 (window-pixel-height)))
433 (defun xwidget-webkit-adjust-size (w h
)
434 "Manually set webkit size to width W, height H."
435 ;; TODO shouldn't be tied to the webkit xwidget
436 (interactive "nWidth:\nnHeight:\n")
437 (xwidget-resize (xwidget-webkit-current-session) w h
))
439 (defun xwidget-webkit-fit-width ()
440 "Adjust width of webkit to window width."
442 (xwidget-webkit-adjust-size (- (nth 2 (window-inside-pixel-edges))
443 (car (window-inside-pixel-edges)))
446 (defun xwidget-webkit-new-session (url)
447 "Create a new webkit session buffer with URL."
449 ((bufname (generate-new-buffer-name "*xwidget-webkit*"))
451 (setq xwidget-webkit-last-session-buffer
(switch-to-buffer
452 (get-buffer-create bufname
)))
453 (insert " 'a' adjusts the xwidget size.")
454 (setq xw
(xwidget-insert 1 'webkit-osr bufname
1000 1000))
455 (xwidget-put xw
'callback
'xwidget-webkit-callback
)
456 (xwidget-webkit-mode)
457 (xwidget-webkit-goto-uri (xwidget-webkit-last-session) url
)))
460 (defun xwidget-webkit-goto-url (url)
462 (if (xwidget-webkit-current-session)
464 (xwidget-webkit-goto-uri (xwidget-webkit-current-session) url
))
465 (xwidget-webkit-new-session url
)))
467 (defun xwidget-webkit-back ()
468 "Go back in history."
470 (xwidget-webkit-execute-script (xwidget-webkit-current-session)
473 (defun xwidget-webkit-reload ()
474 "Reload current url."
476 (xwidget-webkit-execute-script (xwidget-webkit-current-session)
479 (defun xwidget-webkit-current-url ()
480 "Get the webkit url and place it on the kill-ring."
482 (let* ((rv (xwidget-webkit-execute-script-rv (xwidget-webkit-current-session)
484 (url (kill-new (or rv
""))))
485 (message "url: %s" url
)
488 (defun xwidget-webkit-execute-script-rv (xw script
&optional default
)
489 "Same as `xwidget-webkit-execute-script' but with return value.
490 XW is the webkit instance. SCRIPT is the script to execute.
491 DEFAULT is the default return value."
492 ;; Notice the ugly "title" hack. It is needed because the Webkit
493 ;; API at the time of writing didn't support returning values. This
494 ;; is a wrapper for the title hack so it's easy to remove should
495 ;; Webkit someday support JS return values or we find some other way
496 ;; to access the DOM.
498 ;; Reset webkit title. Not very nice.
499 (let* ((emptytag "titlecantbewhitespaceohthehorror")
501 (xwidget-webkit-execute-script xw
(format "document.title=\"%s\";"
502 (or default emptytag
)))
503 (xwidget-webkit-execute-script xw
(format "document.title=%s;" script
))
504 (setq title
(xwidget-webkit-get-title xw
))
505 (if (equal emptytag title
)
508 (setq title default
))
511 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
512 (defun xwidget-webkit-get-selection ()
513 "Get the webkit selection."
514 (xwidget-webkit-execute-script-rv (xwidget-webkit-current-session)
515 "window.getSelection().toString();"))
517 (defun xwidget-webkit-copy-selection-as-kill ()
518 "Get the webkit selection and put it on the kill-ring."
520 (kill-new (xwidget-webkit-get-selection)))
523 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
524 ;; Xwidget plist management (similar to the process plist functions)
526 (defun xwidget-get (xwidget propname
)
527 "Get an xwidget's property value.
528 XWIDGET is an xwidget, PROPNAME a property.
529 Returns the last value stored with `xwidget-put'."
530 (plist-get (xwidget-plist xwidget
) propname
))
532 (defun xwidget-put (xwidget propname value
)
533 "Set an xwidget's property value.
534 XWIDGET is an xwidget, PROPNAME a property to be set to specified VALUE.
535 You can retrieve the value with `xwidget-get'."
536 (set-xwidget-plist xwidget
537 (plist-put (xwidget-plist xwidget
) propname value
)))
540 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
542 (defvar xwidget-view-list
) ; xwidget.c
543 (defvar xwidget-list
) ; xwidget.c
545 (defun xwidget-delete-zombies ()
546 "Helper for `xwidget-cleanup'."
547 (dolist (xwidget-view xwidget-view-list
)
548 (when (or (not (window-live-p (xwidget-view-window xwidget-view
)))
549 (not (memq (xwidget-view-model xwidget-view
)
551 (delete-xwidget-view xwidget-view
))))
553 (defun xwidget-cleanup ()
554 "Delete zombie xwidgets."
555 ;; During development it was sometimes easy to wind up with zombie
556 ;; xwidget instances.
557 ;; This function tries to implement a workaround should it occur again.
559 ;; Kill xviews that should have been deleted but still linger.
560 (xwidget-delete-zombies)
561 ;; Redraw display otherwise ghost of zombies will remain to haunt the screen
564 (defun xwidget-kill-buffer-query-function ()
565 "Ask before killing a buffer that has xwidgets."
566 (let ((xwidgets (get-buffer-xwidgets (current-buffer))))
568 (not (memq t
(mapcar #'xwidget-query-on-exit-flag xwidgets
)))
570 (format "Buffer %S has xwidgets; kill it? " (buffer-name))))))
572 (when (featurep 'xwidget-internal
)
573 (add-hook 'kill-buffer-query-functions
#'xwidget-kill-buffer-query-function
)
574 ;; This would have felt better in C, but this seems to work well in
576 (add-hook 'window-configuration-change-hook
#'xwidget-delete-zombies
))
579 ;;; xwidget.el ends here