;;; -*- Mode: LISP; Syntax: Ansi-common-lisp; Package: HTML4.0; Base: 10 -*-
;;;
;;; (C) Copyright 1997-1998 by Christopher Vincent (java applet), Rodney S. Daughtrey
;;; (lisp code below), and John C. Mallery (CL-HTTP substrate).
;;; All Rights Reserved.
;;;
;;;---------------------------------------------------------------------------------
;; This file implements a high-level Lisp interface to a java applet (written by
;; Christopher Vincent) for producing interactive twist-down trees. It's also
;; a good example of how to write high-level Lisp interfaces to Java applets (that
;; are driven by applet parameter tags) using CL-HTTP.
(in-package :html4.0)
(export '(*default-twistdown-tree-applet-horizontal-space*
compute-twistdown-tree-applet-vertical-space
generate-twistdown-tree))
;;;-------------------------------------------------------------------
;;;
;;; LISP INTERFACE TO APPLET
;;;
(defun default-node-name-printer (node)
"Default function used by GENERATE-TWISTDOWN-TREE that, given a node,
returns the string to print to represent that node on the client side."
(with-output-to-string (stream)
(print-object node stream)))
(defun default-node-url-producer (node)
"Default function used by GENERATE-TWISTDOWN-TREE that, given a node,
returns the URL to associate with that node."
(declare (ignore node))
http:*cl-http-home-page-url-string*)
(defun default-node-frame-producer (node)
"Default function used by GENERATE-TWISTDOWN-TREE that, given a node,
returns the frame to associate with that node."
(declare (ignore node))
; leave target blank for applet default
"")
(defun default-initial-display-p (node)
"Default function used by GENERATE-TWISTDOWN-TREE that, given a node,
decides whether or not to initially display the node's children."
(declare (ignore node))
nil)
(defun default-node-font-color-producer (node)
"Default function used by GENERATE-TWISTDOWN-TREE that, given a node,
decides what color index to make its label."
(declare (ignore node))
1)
(defun allocate-node-id ()
"Allocates a unique ID for a node."
(declare (special *node-id-counter*))
(let ((id (format nil "n~D" *node-id-counter*)))
(incf *node-id-counter*)
id))
(defun intern-node-id (node node-id children-node-ids)
"Adds a node to a hash table of nodes, keyed on the node's ID (*not* the Lisp object), and
records the Lisp object associated with the ID and the node's childrens' IDs."
(declare (special *node-->node-info-table*))
(setf (gethash node-id *node-->node-info-table*)
(cons node children-node-ids)))
(defun node-id-children-ids (node-id)
"Given a node ID, returns its childrens' IDs."
(declare (special *node-->node-info-table*))
(rest (gethash node-id *node-->node-info-table*)))
(defun node-id-node (node-id)
"Given a node ID, returns its associated Lisp object."
(declare (special *node-->node-info-table*))
(first (gethash node-id *node-->node-info-table*)))
(defgeneric walk-tree-recording-information (node child-producer-function)
(:documentation "Walks the tree (using the root node and child producer
function supplied by the user) and stores the nodes of the tree in a hash table for
later reference."))
(defmethod walk-tree-recording-information (node child-producer-function)
(let* ((children-node-ids
(loop for child-node in (funcall child-producer-function node)
collect (walk-tree-recording-information child-node child-producer-function)))
(node-id
(allocate-node-id)))
(intern-node-id node node-id children-node-ids)
node-id))
(defun note-top-level-java-tree-parameter (stream node-id-list)
(note-java-parameter "top" #'(lambda (stream)
(format stream "~{~A~^,~}" node-id-list))
stream))
(defgeneric note-java-tree-parameter (stream node-id children-node-ids pretty-name-function
url-producer-function frame-producer-function show-children-p-function
font-color-producer-function)
(:documentation "Outputs a java applet parameter given the information passed in. The format for tree parameters for the applet
is as follows:
\" value=\",,,,,,...,\">"))
(defmethod note-java-tree-parameter (stream node-id children-node-ids pretty-name-function url-producer-function
frame-producer-function show-children-p-function font-color-producer-function)
(let ((node (node-id-node node-id)))
(note-java-parameter
node-id
#'(lambda (stream)
(format stream "\"~A,~A,~A,~A,~A,~{~A~^,~}\""
(funcall pretty-name-function node)
(funcall font-color-producer-function node)
(funcall url-producer-function node)
(funcall frame-producer-function node)
(if (funcall show-children-p-function node) "yes" "no")
children-node-ids))
stream)))
(defgeneric generate-java-parameters (stream node-id pretty-name-function url-producer-function
frame-producer-function show-children-p-function
font-color-producer-function)
(:documentation "Function which walks the tree (using the information in the hash table,
not the child producer function) and outputs the tree parameters for the applet."))
(defmethod generate-java-parameters (stream node-id pretty-name-function url-producer-function frame-producer-function
show-children-p-function font-color-producer-function)
(let ((children-node-ids (node-id-children-ids node-id)))
(note-java-tree-parameter stream node-id children-node-ids
pretty-name-function url-producer-function frame-producer-function show-children-p-function
font-color-producer-function)
(loop for child-node-id in children-node-ids
do (generate-java-parameters stream child-node-id pretty-name-function url-producer-function
frame-producer-function show-children-p-function
font-color-producer-function))))
(defparameter *default-twistdown-tree-applet-horizontal-space* 300.
"The default width of the applet. Users can change this if desired.")
(defgeneric compute-twistdown-tree-applet-vertical-space (root-node &key font-size line-spacing)
(:documentation "Function to return the vertical space to be used by the applet. The default
method considers font-size and line-spacing."))
(defmethod compute-twistdown-tree-applet-vertical-space (root-node &key font-size line-spacing)
(declare (ignore root-node font-size) (special *node-->node-info-table*))
(let ((number-of-nodes (hash-table-count *node-->node-info-table*)))
(* (1+ number-of-nodes) line-spacing)))
(declaim (inline intern-color))
(defun intern-color (color)
(etypecase color
((or keyword cons)
(ns1.1:color-mapping color))
(string color)))
(defun note-twistdown-tree-palette-java-parameter (palette stream)
(when palette
(note-java-parameter
"palette"
(loop with palette-string = ""
with length = (length palette)
for counter from 1
for palette-entry in palette
do (setq palette-string (concatenate 'string palette-string (intern-color palette-entry)))
(unless (= counter length)
(setq palette-string (concatenate 'string palette-string ",")))
finally (return palette-string))
stream)))
(defun %generate-twistdown-tree (stream root-object-list child-producer
&optional (object-printer #'default-node-name-printer)
(url-producer #'default-node-url-producer)
(frame-producer #'default-node-frame-producer)
(initial-display-predicate #'default-initial-display-p)
(font-color-producer #'default-node-font-color-producer)
(node-indentation 15)
(line-spacing 15)
(font-size 10)
(palette nil)
(background-color-index 0)
(marker-color-index 1)
(context-url nil)
(debug-p nil)
(applet-alignment :top)
applet-name
applet-alternate-text
applet-horizontal-space
applet-vertical-space)
(let ((*node-id-counter* 0)
(*node-->node-info-table* (make-hash-table :test #'equal))
root-node-id-list)
(declare (special *node-id-counter* *node-->node-info-table*))
;; First, walk the tree(s) and record each java parameter name, node, and its children parameter names
;; in a hash table, and save the node id's of the root nodes
(setq root-node-id-list
(loop for root-object in root-object-list
collect (walk-tree-recording-information root-object child-producer)))
(with-java-applet ("hdir.class"
(or applet-horizontal-space *default-twistdown-tree-applet-horizontal-space*)
(or applet-vertical-space
(compute-twistdown-tree-applet-vertical-space
(first root-object-list) :font-size font-size :line-spacing line-spacing))
applet-alignment
:stream stream
:name applet-name
:alternate-text applet-alternate-text)
;; ...then walk the tree again (using the hash table) and generate the java parameters
(dolist (root-node-id root-node-id-list)
(generate-java-parameters stream root-node-id
object-printer url-producer frame-producer
initial-display-predicate font-color-producer))
;; Finally, generate the special top-level java parameter
(note-top-level-java-tree-parameter stream root-node-id-list)
(note-java-parameter "bg_color" background-color-index stream)
(note-java-parameter "marker_color" marker-color-index stream)
(note-java-parameter "x_space" node-indentation stream)
(note-java-parameter "y_space" line-spacing stream)
(note-java-parameter "font_size" font-size stream)
(note-java-parameter "debug" (if debug-p "yes" "no") stream)
(when palette
(note-twistdown-tree-palette-java-parameter palette stream))
(when context-url
(note-java-parameter "context_url" context-url stream)))))
(defun generate-twistdown-tree (stream root-object-list child-producer
&key (object-printer #'default-node-name-printer)
(url-producer #'default-node-url-producer)
(frame-producer #'default-node-frame-producer)
(initial-display-predicate #'default-initial-display-p)
(font-color-producer #'default-node-font-color-producer)
(node-indentation 15)
(line-spacing 15)
(font-size 10)
(palette nil)
(background-color-index 0)
(marker-color-index 1)
(context-url nil)
(debug-p nil)
(applet-alignment :top)
applet-name
applet-alternate-text
applet-horizontal-space applet-vertical-space)
"The main function to call to produce a twist-down Java UI on the client-side based on
your Lisp object hierarchy on the server side. Some specific notes:
-- The child-producer function is only called once for each node
-- The code is safe across threads
-- The same Lisp object can appear more than once in the hierarchy, i.e. for any hierarchy your
child-producer function can produce, the Lisp code will generate the Java parameters to
display it correctly, even if the hierarchy you want to display isn't strictly a tree.
Arguments to the function are as follows:
Required args:
STREAM:
The stream to which to output the java applet.
ROOT-OBJECT-LIST:
A list of root (Lisp) objects that you want to display.
CHILD-PRODUCER:
A function which returns a list of (Lisp) objects which are the children of the parent object passed in.
Keywords:
OBJECT-PRINTER:
Function to call on the nodes in your Lisp object hierarchy to produce
the string to display for each node.
URL-PRODUCER:
Function to call on the nodes in your Lisp object hierarchy to produce
the URL to associate with that node.
FRAME-PRODUCER:
Function to call on the nodes in your Lisp object hierarchy to produce
the HTML frame to associate with that node.
INITIAL-DISPLAY-PREDICATE:
Function to call on the nodes in your Lisp object hierarchy to determine
whether to initially display a node's children.
NODE-INDENTATION:
Amount of space (in pixels) to indent nodes under their parent nodes.
This is an applet-level setting (not per-node).
LINE-SPACING:
Amount of space (in pixels) between nodes vertically. Applet-level setting.
FONT-SIZE:
Font used for the node names. Applet-level setting.
PALETTE:
Color palette used by the applet. List of colors, where a color can be a
hex string (i.e. \"#FFFF00\"), an RGB triple (i.e. '(255 255 0)), or a color keyword
as defined by NS1.1:*BUILT-IN-CLIENT-COLORS* (i.e. :AQUAMARINE). The list may contain
mixed color representations (i.e. ((255 255 0) :AQUAMARINE)).
BACKGROUND-COLOR-INDEX:
Index (i.e. an integer >= 0) to specify a color in the palette to be used for the background color of the applet.
MARKER-COLOR-INDEX:
Index (i.e. an integer >= 0) to specify a color in the palette to be used for the color of the twistdown triangle
marker.
CONTEXT-URL:
Node URLs are merged against this absolute URL. If unspecified, all node URLs must be absolute.
DEBUG-P:
If non-NIL, debugging information is printed
APPLET-ALIGNMENT:
How the applet is aligned with respect to the client window. See the
ALIGNMENT argument to WITH-JAVA-APPLET.
APPLET-NAME:
See the NAME keyword argument to WITH-JAVA-APPLET.
APPLET-ALTERNATE-TEXT
See the ALTERNATE-TEXT keyword argument to WITH-JAVA-APPLET.
APPLET-HORIZONTAL-SPACE
See the HORIZONTAL-SPACE keyword argument to WITH-JAVA-APPLET.
APPLET-VERTICAL-SPACE
See the VERTICAL-SPACE keyword argument to WITH-JAVA-APPLET."
(%generate-twistdown-tree stream root-object-list child-producer object-printer url-producer frame-producer
initial-display-predicate font-color-producer node-indentation
line-spacing font-size palette background-color-index marker-color-index
context-url debug-p applet-alignment applet-name
applet-alternate-text applet-horizontal-space applet-vertical-space))
;;;-------------------------------------------------------------------
;;;
;;; EXPORTS
;;;
;; Export all the java binary .class files
#|
(http:export-url #u"/cl-http/twistdown-tree/"
:directory
:pathname "http:examples;twistdown-tree;java;"
:immediate-export t)
|#
;;;-------------------------------------------------------------------
;;;
;;; EXAMPLE USAGE
;;;
(defparameter *tree-1* '("Bob"
("Sue" ("Emily") ("Joe") ("Wayne"))
("Carl"
("Ed") ("Jim")
("Jane"
("Brian"
("Tony"
("Chip")))))))
(defparameter *tree-2* '("Greg"
("Peter" ("Bobby"))
("Marcia" ("Jan" ("Cindy")))))
(defun example-font-color-producer (node)
(declare (ignore node))
2)
(defmethod compute-twistdown-tree-example ((url http-url) stream)
(http:with-successful-response (stream :html)
(with-html-document (:stream stream)
(with-document-preamble (:stream stream)
(declare-title "TwistDown Tree Example" :stream stream)
(declare-link :reference "/cl-http/css/base.css"
:relation "stylesheet" :media-type "text/css" :stream stream))
(with-document-body (:background :white :stream stream)
(with-section-heading ("TwistDown Tree Example" :alignment :center :stream stream)
(horizontal-line :stream stream)
(with-paragraph (:stream stream)
(with-emphasis (:quotation :stream stream)
(fast-format stream "This example illustrates how high-level functional interfaces in Common Lisp can
leverage client-side Java to achieve a more inspiring user experience. Here, Java Applets are defined in advance
and exported as Java byte code. As it emits dynamic HTML, Lisp generates parameters to invoke the applets.")
(with-enumeration (stream :itemize)
(enumerating-item (stream)
(fast-format stream "Click on the arrows to expand or collapse nodes."))
(enumerating-item (stream)
(fast-format stream "Click on the text of a node to visit the associated URL.")))))
;; One line of code displays the tree!
(with-paragraph (:alignment :center :stream stream)
(generate-twistdown-tree stream (list *tree-1* *tree-2*) #'rest
:object-printer #'first
:palette '(:turquoise-medium :yellow :black)
:font-color-producer #'example-font-color-producer
:applet-horizontal-space 200))
(with-paragraph (:stream stream)
(with-emphasis (:quotation :stream stream)
(fast-format stream "If you think of any cool enhancements, please let us know on ~I. For applet documentation see ~I."
(note-anchor "WWW-CL@csail.mit.edu" :reference "mailto:WWW-CL@csail.mit.edu" :stream stream)
(note-anchor "/cl-http/twistdown-tree/hdir/index.html" :reference "/cl-http/twistdown-tree/hdir/index.html" :stream stream))))
(horizontal-line :stream stream)
(http:cl-http-signature stream))))))
#|
(http:export-url #u"/cl-http/twistdown-tree/twistdown.html"
:computed
:response-function #'compute-twistdown-tree-example)
(http:export-url #u"/cl-http/twistdown-tree/docs/"
:directory)
|#