;-*- Mode:LISP; Package:(ORG :USE (USER GLOBAL SYSTEM)); Fonts:CPTFONT; Base:8 -*- ;; Copyright LISP Machine, Inc. 1984 ;; See filename "Copyright.text" for ;; licensing and release information. ;;; ORG: Organization Chart Program ;;; REVISION HISTORY ;;; ;;; E. Smith and L. Wilde 2/85 Revise to work with Release 2.0 ;;; S. Strassman 7/84 Added additional features ;;; J. Hendler 6/84 Modify nodes to use "FLAVORS" object class system ;;; L. Hawkinson Unknown Original Version ;------------------------------------------------------------------------------ ; To use this code: ; ; Just load this file up and type SYSTEM-O. ; ;------------------------------------------------------------------------------ ; Set up our global variables. ; ; Note the convention of beginning each one with an asterisk. ; Also, every one has a documentation string. ; (defvar *chart-frame :unbound "frame for org process") (defvar *chart-pane :unbound "org chart pane in org chart frame") (defvar *command-pane :unbound "command pane in org chart frame") (defvar *prompt-pane :unbound "prompt pane in org chart frame") (defvar *default-attributes '(("Name" "anonymous") ("Title" *needs-filling-in) ("Location" *needs-filling-in)) "The default list of attributes a node could have.") (defconst *needs-filling-in "*****" "value for descriptor not (yet) filled in") (defvar *closed-width 15. "The width of a closed node") (defvar *closed-height 8. "The height of a closed node") (defconst *alu tv:alu-ior "TV algorithm for drawing characters") (defconst *scroll-amount 100. "Number of pixels to scroll on the scroll command") (defvar *file (fs:merge-pathname-defaults "foo.chart") "The filename to store the current chart under.") (defvar *default-h-separation 10) (defvar *default-v-separation 10) ;------------------------------------------------------------------------------ ; Nodes ; A node has the following instance variables, which keep track of the state ; of that node: ; ; opened?: A closed node exists, but is invisible. An opened node is visible. ; attributes: A list of sublists (i.e. an "alist"). Each sublist is a list of ; two strings: a slot (eg: "Name"), and its value (eg: "Melvin"). ; A special value is the variable *NEEDS-FILLING-IN. ; justification One of three keywords: :CENTER, :LEFT, or :RIGHT. ; extra-width: How much whitespace to put inside the box around the text ; (horizontally) ; vertical-separation: How much whitespace to put beneath this node and its inferiors. ; horizontal-separation: How much whitespace to put between each inferior. ; above: The node above this one ; below: A list of the nodes below this one ; pane: The window this node draws itself on ; height: The height of this node's box in pixels ; width: The width of this node's box in pixels ; sub-tree-height: The height of the sub-tree including this node in pixels ; sub-tree-width: The width of the sub-tree including this node in pixels ; left: The left edge of this node in screen coordinates ; top: The top edge of this node in screen coordinates (defflavor node ((opened? t) ; are you visible? (attributes *default-attributes) ; an alist of print values (justification ':center) ; how to print the values (extra-width 10.) ; extra margin in pixels (vertical-separation *default-v-separation) (horizontal-separation *default-v-separation) (above nil) ; the node above (below nil) ; the nodes below pane ; the window to draw-self on height ; in pixels width ; in pixels sub-tree-width ; visible sub-tree sub-tree-height ; visible sub-tree left ; x coordinate of left edge top) ; y coordinate of top edge () :gettable-instance-variables :settable-instance-variables :initable-instance-variables) (defmethod (node :after :init) (&rest ignore) (send self ':compute-dimensions)) ;------------------------------------------------------------------------------ ; ; Node methods I: ; How to draw nodes; including geometry computations ; ; This computes the height and width of a node, based on what strings go ; inside it. Note that this is independent of where on the screen the node is. ; (defmethod (node :compute-dimensions) (&rest ignore) (cond ((not opened?) (setq width *closed-width) (setq height *closed-height) (setq sub-tree-width *closed-width) (setq sub-tree-height *closed-height)) (t (setq width (+ extra-width (loop for attribute in attributes if (send self ':ought-to-print-attribute? attribute) maximize (send pane ':string-length (eval (second attribute)))))) (setq height (+ (send pane ':vsp) (loop for attribute in attributes if (send self ':ought-to-print-attribute? attribute) sum (+ (send pane ':vsp) (tv:font-char-height (send pane ':current-font))))))))) ; This is a recursive procedure. You call it on the topmost node ; which you're interested in, and all of its underlings will be ; called with the same :COMPUTE-TOTAL-DIMENSIONS message. Thus, this ; returns the total width and total height (in pixels) of the sub-tree ; beginning with any node. Unopened cells, of course, take up no room. ; ; First, the node's own height and width is computed. ; Then, all the space occupied below is computed (depth-first). ; The value returned is a list of the entire width and the entire height. ; Note that the width is the maximum of this node's width and ; the width of its underlings (max width total-width) ; The height is the sum of three things: this node's height, the vertical ; separation between layers, and the max of the underlings' heights. ; This method makes sure everyone's HEIGHT and WIDTH are up to date. ; The next method, :reposition-underlings, makes sure everyone's ; TOP and LEFT are up to date. ; (defmethod (node :compute-total-dimensions) (&rest ignore) (send self ':compute-dimensions) (cond ((not opened?) (list width height)) ((null below) (setq sub-tree-width width) (setq sub-tree-height height) (list width height)) (t (loop for underling in below for under-dim = (send underling ':compute-total-dimensions) sum (+ horizontal-separation (first under-dim)) into under-width maximize (second under-dim) into under-height finally (return (progn (setq sub-tree-width (max width (- under-width horizontal-separation))) (setq sub-tree-height (+ height ; my height vertical-separation ; height directly under me under-height)) ; my underlings' height (list sub-tree-width sub-tree-height))))))) ; This is also a recursive procedure which makes sure everyone's ; TOP and LEFT are ok. It does this by finding out where its underlings ; are, and then positioning itself on top of them, in the center. ; Note that this really has to call :COMPUTE-TOTAL-DIMENSIONS first, ; but :COMPUTE-TOTAL-DIMENSIONS doesn't really need to call ; :REPOSITION-UNDERLINGS in order to work. For extra credit: Why is this so? ; (defmethod (node :reposition-underlings) () (if (not opened?) nil (let* ((under-width ; the width of all my underlings (- (loop for underling in below summing (+ (send underling ':sub-tree-width) horizontal-separation)) horizontal-separation)) (under-left (- (+ left (* .5 width)) ; middle of this node (* .5 under-width)))) ; left edge of sub-tree (loop for underling in below for dx first under-left then (+ dx under-sub-tree-width horizontal-separation) for under-sub-tree-width = (send underling ':sub-tree-width) do (send underling ':set-top (+ top height vertical-separation)) (send underling ':set-left (- (+ dx (* .5 under-sub-tree-width)) ; center of sub-tree (* .5 (send underling ':width)))) ; offset of underling's width (send underling ':reposition-underlings))))) ; Drawing stuff. ; ; This is the procedure you call on the top node to get the whole tree ; drawn. It draws itself, and then each of its underlings (which, of course ; head subtrees themselves) get drawn the same way. If you have the special ; case that a node is "closed", you give it to the special :DRAW-CLOSED method. ; (defmethod (node :draw-sub-tree) () (if (not opened?) (send self ':draw-closed) (send self ':draw) (cond ((> (length below) 1) (send self ':draw-under-line) (loop for underling in below do (send self ':draw-line-to underling) do (send underling ':draw-sub-tree))) ((= (length below) 1) (send pane ':draw-line (fix (+ left (* .5 width))) (fix (+ top height)) (fix (+ left (* .5 width))) (fix (+ top height vertical-separation -1))) (send (car below) ':draw-sub-tree))))) (defmethod (node :draw-under-line) () (let* ((left-node (first below)) (right-node (car (last below))) (top-x (fix (+ left (* .5 width)))) (top-y (fix (+ top height))) (left-x (fix (+ (send left-node ':left) (* .5 (send left-node ':width))))) (right-x (fix (+ (send right-node ':left) (* .5 (send right-node ':width))))) (bottom-y (fix (+ top-y (* .5 vertical-separation))))) (send pane ':draw-line top-x top-y top-x (1- bottom-y) *alu) (send pane ':draw-line left-x bottom-y right-x bottom-y *alu))) ; This draws the line from superior to underling. ; The FIXR operations convert any kind of number into an integer. ; FIX does this by truncation, FIXR rounds off the argument. ; This is because graphics commands can't handle floating point ; for screen coordinates. ; (defmethod (node :draw-line-to) (underling) (let* ((top-x (fix (+ (send underling ':left) (* .5 (send underling ':width))))) (top-y (fix (+ top height (* .5 vertical-separation)))) (bottom-y (fix (- (send underling ':top) 1)))) (send pane ':draw-line top-x top-y top-x bottom-y *alu))) ; This is called by :DRAW-SUB-TREE. It draws just the box and contents for ; itself, but it doesn't draw connecting lines or any of its underlings. ; If an attribute shouldn't be printed, it doesn't get printed. ; Notice the abstractions here. Such decisions as "How should the edges ; be drawn?" "Should this attribute be printed?" "How should it look?" ; are not decided here. Why did I write these as separate functions? ; (defmethod (node :draw) (&aux (char-height (tv:font-char-height (send pane ':current-font)))) (send self ':draw-edges) (loop for attribute in attributes for print? = (send self ':ought-to-print-attribute? attribute) for y first (+ top (send pane ':vsp)) then (if print? (+ y char-height (send pane ':vsp)) y) do (if print? (send self ':print-attribute attribute y)))) ; The box around the node ; (defmethod (node :draw-edges) () (send pane ':draw-lines *alu (fix left)(fix top) (fix (+ left width)) (fix top) (fix (+ left width)) (fix (+ top height)) (fix left)(fix (+ top height)) (fix left)(fix top))) ; This method defines how closed boxes draw themselves. You draw ; only its edges (which were set to be really small in :COMPUTE-DIMENSIONS ; because this node is closed, and then color it gray. (defmethod (node :draw-closed) () (send self ':draw-edges) (send pane ':bitblt *alu (fix (1- width)) (fix (1- height)) tv:33%-gray 0 0 (fix (1+ left)) (fix (1+ top)))) ; Notice the way this implementation allows you to use "*****" ; to represent the notion of needing filling-in, yet you are not ; prohibited from including your Martian employees ("*****" is ; a common Martian family name, you know) on this chart. ; A professional programmer never has "magic" values that might ; cause an innnocent user some grief some day. ; (defmethod (node :ought-to-print-attribute?) (attribute) (and (cdr attribute) (not (eq (second attribute) '*needs-filling-in)))) ; Notice the idiom (* .5 foo). This is used to figure out half ; of a region, so things get centered nicely. ; (defmethod (node :print-attribute) (attribute y) (let* ((string (eval (second attribute))) (free-space (- width (send pane ':string-length string)))) (selectq justification (:center (label pane (+ left (* .5 free-space)) y string)) (:left (label pane (+ (* .5 extra-width) left) y string)) (:right (label pane (+ left free-space (* -.5 extra-width)) y string))))) ; The following is a short-hand notation for the corrected :STRING-OUT message. ; (defun label (window left top string) (if (or (< top 0) (< left 0)) nil (send window ':set-cursorpos (fix left) (correct window top)) (send window ':string-out string))) (defun correct (window top) (fix (+ top (tv:font-baseline (send window ':current-font)) (- (tv:font-baseline (aref (send window ':font-map) (send window ':largest-font))))))) ; The string-out message is very useful, since it's a very explicit ; way of getting typout on a screen. Some other favorite ways of printing ; include (this is by no means an exhaustive list): ; ; (send ':draw-char ) ; (format . ) ; (print ) ; (princ ) ; (prin1 ) ; (send ':tyo ) ;------------------------------------------------------------------------------ ; ; Node methods II: ; Maintaining the contents ; ; There's several ways you may want to edit the attributes, and ; rather than bother the user with one big menu, this is an exercise ; in menu hacking. ; ; In creating a menu, you must specify everything the computer cannot ; figure out for itself. For example, things you must specify include: ; ; What the items to choose from are; what to do when you select one of them, ; what to do when you DON'T select one of them, what the mouse documentation ; who-line should say when you're pointing at the item, etc. ; ; Every menu has an which, for each item, specifies ; just what kind of actions and items you have. For more information ; on the different kinds of menus, consult the Window System Manual. ; (defmethod (node :edit) () (send pane ':set-current-node self) (tv:menu-choose '(("Modify Contents" :eval (send (send *chart-pane ':current-node) ':fill-in-self) :documentation "Modify the contents of this node") ("Select Attributes" :eval (send (send *chart-pane ':current-node) ':select-attributes) :documentation "Remove one or more attributes of this node") ("Change Geometry" :eval (send (send *chart-pane ':current-node) ':change-geometry) :documentation "Change the display features of this node"))) (send pane ':refresh)) ; Choose-variable-values only modifies the values of variables. ; I wanted to modify the elements of a list, so I needed to GENSYM some new ; variables. GENSYM creates a symbol that is guaranteed brand-new, ; and I generate as many as I need to hold all the attributes. ; Then I call up a CHOOSE-VARIABLE-VALUES on these variables, and ; put everything back when I'm finished. ; (defmethod (node :fill-in-self) () (let ((vars (loop for attribute in attributes for var = (gensym 'attribute) do (setf (plist var) (append (plist var) '(si:documentation-property (("variable")) special t))) (set var (second attribute)) collect var)) (base 10.) (ibase 10.)) (tv:choose-variable-values (loop for var in vars for attribute in attributes collecting (list var (first attribute) ':string)) ':label "Fill in as appropriate:" ':near-mode '(:mouse) ':margin-choices '("Mouse here when done")) (setq attributes (loop for var in vars for attribute in attributes collecting (list (first attribute) (eval var)))))) ; Multiple-choose menus provide columns and rows of boxes. ; Each box can be on or off, and turning it on or off may cause other boxes ; to turn on or off. Consult the Window System Manual for more info. ; ; The items are labeled with strings like "Name: Mary" if a value is found, ; and a string like "Name" if not found. Notice the use of back-quoted lists ; and commas in order to control creation of the item list. ; (defmethod (node :select-attributes) () (let ((selections (tv:multiple-choose "This node's attributes" (loop for default-attribute in *default-attributes for found = (assoc (first default-attribute) attributes) collecting (if found (copytree (list found (string-append (first found) ": " (second found)) '((:keep t t nil nil) (:remove nil t nil nil)))) ; if not found (copytree (list default-attribute (first default-attribute) '((:keep nil t nil nil) (:remove t t nil nil)))))) (copytree '((:keep " Keep " nil t t nil) (:remove " Remove " nil t t nil)))))) (if selections (setq attributes (loop for item in selections if (eq (second item) ':keep) collect (first item)))))) ; This allow a user to modify the appearance of a box, or the separation ; of a box's underlings. This is in a different menu from the other two, ; because the nature of the information is rather different. ; (defmethod (node :change-geometry) () (let* ((base 10.) (ibase 10.) (properties '(justification extra-width vertical-separation horizontal-separation)) (vars (loop for property in properties for var = (gensym 'prop) do (setf (plist property) (append (plist property) '(si:documentation-property (("variable")) special t))) (setf (plist var) (append (plist var) '(si:documentation-property (("variable")) special t))) (set var (send self ':eval-inside-yourself property)) collect var))) (tv:choose-variable-values (loop for var in vars for doc in '(("Justification" :choose (:center :left :right)) ("Extra box width (10 is good)" :number) ("Vertical distance to underlings" :number) ("Separation between underlings" :number)) collecting (cons var doc)) ':label "All numbers are in pixels:" ':near-mode '(:mouse) ':margin-choices '("Mouse here when done")) (loop for property in properties for var in vars do (send self ':eval-inside-yourself `(setq ,property ,var))))) ;------------------------------------------------------------------------------ ; ; Node methods III: ; Adding and deleting nodes ; Yes Virginia, FIND-POSITION-IN-LIST, FIRSTN, and NTHCDR are lisp primitives! ; Aren't you glad some nut wrote them for us? ; (defmethod (node :left-siblings-of) (node) (if (not (memq node below)) nil (firstn (find-position-in-list node below) below))) (defmethod (node :right-siblings-of) (node) (if (not (memq node below)) nil (cdr (nthcdr (find-position-in-list node below) below)))) ; Create a new underling ; (defmethod (node :add-below) () (let ((new (make-instance 'node ':pane pane ':horizontal-separation *default-h-separation ':vertical-separation *default-v-separation))) (setq below (append below (list new))) (send new ':set-above self) (send new ':fill-in-self) (send pane ':refresh) (send pane ':set-current-node new))) ; Create a new superior. Don't forget to inform the superior's superior ; about this, and if you've made a brand-new top-node for the chart, ; inform the chart-pane too, while you're at it. ; (defmethod (node :add-above) () (let ((new (make-instance 'node ':pane pane ':horizontal-separation *default-h-separation ':vertical-separation *default-v-separation))) (send new ':set-above above) (send new ':set-below (list self)) (setq above new) (cond ((eq self (send pane ':top-node)) ; If a new top is created (send pane ':set-top-node new) (send new ':fill-in-self) (send pane ':go-to-top)) ((send new ':above) ; Else we must inform it of a new inferior (send (send new ':above) ':set-below (append (send (send new ':above) ':left-siblings-of self) (list new) (send (send new ':above) ':right-siblings-of self))) (send new ':fill-in-self) (send pane ':refresh) (send pane ':set-current-node new))))) ; A new sibling ; (defmethod (node :add-right) () (if (null above) (beep-on-terminal) (let ((new (make-instance 'node ':pane pane ':horizontal-separation *default-h-separation ':vertical-separation *default-v-separation))) (send new ':set-above above) (send above ':set-below (append (send above ':left-siblings-of self) (list self new) (send above ':right-siblings-of self))) (send new ':fill-in-self) (send pane ':refresh) (send pane ':set-current-node new)))) ; A new sibling ; (defmethod (node :add-left) () (if (null above) (beep-on-terminal) (let ((new (make-instance 'node ':pane pane ':horizontal-separation *default-h-separation ':vertical-separation *default-v-separation))) (send new ':set-above above) (send above ':set-below (append (send above ':left-siblings-of self) (list new self) (send above ':right-siblings-of self))) (send new ':fill-in-self) (send pane ':refresh) (send pane ':set-current-node new)))) (defmethod (node :remove-self) () (cond ((null below) (if (null above) (beep-on-terminal) (send above ':set-below (remq self (send above ':below))) (send pane ':set-current-node above) (send pane ':refresh))) ((null above) (if (< 1 (length below)) (beep-on-terminal) (send (car below) ':set-above nil) (send pane ':set-top-node (car below)) (send pane ':go-to-top))) (t (send above ':set-below (append (send above ':left-siblings-of self) below (send above ':right-siblings-of self))) (loop for orphan in below do (send orphan ':set-above above)) (send pane ':set-current-node above) (send pane ':refresh)))) ; This keeps a node, and eliminates all subtrees ; (defmethod (node :remove-below) () (if (null below) (beep-on-terminal) (setq below nil) (send pane ':refresh))) ;------------------------------------------------------------------------------ ; ; User queries ; This prompts the user on the prompt pane for a yes or no answer. ; It cleans up the prompt pane when finished by sending a :REFRESH message. ; (defun let-user-confirm (query-string) (let ((answer (fquery () query-string))) (send *prompt-pane ':refresh) answer)) ; This beeps on (and maybe flashes) the user's terminal. ; (defun beep-on-terminal () (send terminal-io ':beep)) ;------------------------------------------------------------------------------ ; The pane with the organization chart ; (defflavor chart-pane ((top-node nil) (current-node) (font 3) (largest-font 4) (smallest-font 0)) (tv:list-mouse-buttons-mixin tv:truncating-window) (:documentation "ORG Chart pane") (:default-init-plist :font-map '(fonts:5x5 fonts:tr8 fonts:tr10 fonts:tr12 fonts:mets) :save-bits t :blinker-flavor 'tv:box-blinker :label nil) ; no window label :gettable-instance-variables :settable-instance-variables :initable-instance-variables) (defmethod (chart-pane :after :init) (&rest ignore) (setq top-node (make-instance 'node ':pane self ':horizontal-separation *default-h-separation ':vertical-separation *default-v-separation)) (setq current-node top-node) (send current-node ':set-top (* .1 (send self ':height))) (send current-node ':set-left (- (* .5 (send self ':width)) (* .5 (send current-node ':width))))) (defmethod (chart-pane :after :refresh) (&rest ignore) (send self ':set-current-font font) (send top-node ':compute-total-dimensions) (send top-node ':reposition-underlings) (send top-node ':draw-sub-tree) ; This actually draws the whole graph (send self ':update-blinker)) (defmethod (chart-pane :after :set-current-node) (ignore) (send self ':scroll-if-necessary) (send self ':update-blinker)) (defmethod (chart-pane :update-blinker) () (let ((blinker (car (send self ':blinker-list)))) (send blinker ':set-size (+ 5 (send current-node ':width)) (+ 5 (send current-node ':height))) (send blinker ':set-cursorpos (- (send current-node ':left) 2) (- (send current-node ':top) 2)) (send blinker ':set-visibility t))) ;------------------------------------------------------------------------------ ; ; The command pane's functions ; ; File storage stuff ; (defmethod (chart-pane :save-as-file) () (with-open-file (file (send self ':get-file-name-from-user "Save this graph into which file?") ':write) (format file ";-*- Mode:LISP; Package:(ORG :USE (USER GLOBAL SYSTEM)); Base:10. -*- ~3%") (format file "; This file was generated by the ORG program. It contains the ~%") (format file "; information necessary to reconstruct a chart from scratch. ~%") (format file "; ~%") (format file "; Modify this file only if you really know what you're doing.~%") (format file "; It's much better to run ORG, then load this file, modify it,~%") (format file "; and save out the preferred version.~2%") (format file "; This is the top node:") (dump-world t file (send top-node ':dump-subtree)) (format file "~3%; That's all, folks!")) (format t "Done.~%")) (defun dump-world (top-node? stream string-tree) (if (null string-tree) nil (let ((node-string (first string-tree)) (symbol (second string-tree)) (kids (third string-tree))) (format stream "~2%(defconst ~A ~A)" symbol node-string) (if (not top-node?) nil (format stream "~2%(send *chart-pane ':set-top-node ~A)" symbol) (format stream "~%(send ~A ':set-pane *chart-pane)" symbol)) (loop for kid in kids do (dump-world nil stream kid) do (format stream "~%(send ~A ':greet-new-inferior ~A)" symbol (second kid)))))) (defmethod (node :greet-new-inferior) (node) (setq below (append below (list node))) (send node ':set-above self)) (defmethod (node :dump-subtree) () (let ((nodename (gensym 'node))) (list (send self ':dump-string) nodename (loop for kid in below collect (send kid ':dump-subtree))))) (defmethod (node :dump-string) () (let ((base 10.)) (string-append (format nil "~% (make-instance 'node ~%") (format nil " ':pane *chart-pane ~%") (format nil " ':opened? ~S ~%" opened?) (format nil " ':attributes~%") (format nil " '~S ~%" attributes) (format nil " ':justification ':~S ~%" justification) (format nil " ':extra-width ~S ~%" extra-width) (format nil " ':vertical-separation ~S ~%" vertical-separation) (format nil " ':horizontal-separation ~S)" horizontal-separation)))) (defmethod (chart-pane :get-file-name-from-user) (prompt-string) (let ((fn (prompt-and-read `(:pathname :defaults ,*file) "~%~A (default: ~A) " prompt-string *file))) (setq *file (fs:merge-pathname-defaults fn)))) (defmethod (chart-pane :restore-from-file) () (cond ((fquery () "Are you sure you want to wipe out everything and load a brand-new chart? ") (load (send self ':get-file-name-from-user "File to load")) (send self ':go-to-top) (format t "Done.~%")))) ;------------------------------------------------------------------------------ ; ; Font nonsense ; (defmethod (chart-pane :make-bigger) () (if (equal font largest-font) (beep-on-terminal) (setq font (1+ font)) (send self ':set-current-font font)) (send self ':refresh)) (defmethod (chart-pane :make-smaller) () (if (equal font smallest-font) (beep-on-terminal) (setq font (1- font)) (send self ':set-current-font font)) (send self ':refresh)) (defmethod (chart-pane :change-parameters) () (let ((base 10.) (ibase 10.)) (tv:choose-variable-values `((*closed-width "Width of a closed box (15 is good)" :number) (*closed-height "Height of a closed box (8 is good)" :number) (*default-v-separation "Default space under a node (10 is good)" :number) (*default-h-separation "Default space between nodes (10 is good)" :number) (*scroll-amount "How big a jump to make when scrolling (100 is good)" :number) (*file "The pathname of the current file" :pathname))))) (defmethod (chart-pane :go-to-top) () (setq current-node top-node) (send current-node ':set-top (* .1 (send self ':height))) (send current-node ':set-left (- (* .5 (send self ':width)) (* .5 (send current-node ':width)))) (send self ':refresh)) (defmethod (chart-pane :go-up) () (let ((boss (send current-node ':above))) (if (null boss) (beep-on-terminal) (send self ':set-current-node boss)))) (defmethod (chart-pane :go-down) () (let ((underlings (send current-node ':below))) (if (or (not (send current-node ':opened?)) (null underlings)) (beep-on-terminal) (send self ':set-current-node (car underlings))))) (defmethod (chart-pane :go-left) () (let ((boss (send current-node ':above))) (if (null boss) (beep-on-terminal) (let ((left-kid (car (last (send boss ':left-siblings-of current-node))))) (cond ((null left-kid) (send self ':go-up)) (t (send self ':set-current-node left-kid))))))) (defmethod (chart-pane :go-right) () (let ((boss (send current-node ':above))) (if (null boss) (send self ':go-down) (let ((right-kid (car (send boss ':right-siblings-of current-node)))) (cond ((and (null right-kid) (null (send current-node ':below))) (beep-on-terminal)) ((null right-kid) (send self ':go-down)) (t (send self ':set-current-node right-kid))))))) (defmethod (chart-pane :far-right) () (let ((boss (send current-node ':above))) (if (null boss) (beep-on-terminal) (let ((right-kid (car (last (send boss ':right-siblings-of current-node))))) (cond ((null right-kid) (beep-on-terminal)) (t (send self ':set-current-node right-kid))))))) (defmethod (chart-pane :far-left) () (let ((boss (send current-node ':above))) (if (null boss) (beep-on-terminal) (let ((left-kid (car (send boss ':left-siblings-of current-node)))) (cond ((null left-kid) (beep-on-terminal)) (t (send self ':set-current-node left-kid))))))) ; The following three methods are needed in case you :GO-DOWN or :GO-RIGHT, ; etc. to a node which lies off the screen. ; (defmethod (chart-pane :scroll-if-necessary) () (if (or (send self ':fix-horizontal?) (send self ':fix-vertical?)) (send self ':refresh) (send self ':update-blinker))) ; If the node is on screen, this returns nil and nothing happens. ; If the node lies off-screen, the top-node is moved to compensate, ; and T is returned. Thus, :SCROLL-IF-NECESSARY will only refresh once, ; even if we need to scroll both horizontally and vertically. ; ; For a discussion on the existance of the 2's and the 7's in the code, see the ; documentation for the scrolling methods (e.g. :SCROLL-UP, :SCROLL-LEFT, etc.) ; (defmethod (chart-pane :fix-horizontal?) () (let ((new-left (send current-node ':left)) (origin-left (send top-node ':left)) (right-boundary (- (send self ':width) 7 (send current-node ':width)))) (cond ((and (>= new-left 2) (<= new-left right-boundary)) nil) ; OK, return nil ((< new-left 2) (send top-node ':set-left (+ origin-left (- 2 new-left))) t) ; Was bad, return t (t (send top-node ':set-left (- origin-left (- new-left right-boundary))) t)))) ; Was bad, return t (defmethod (chart-pane :fix-vertical?) () (let ((new-top (send current-node ':top)) (origin-top (send top-node ':top)) (bottom-boundary (- (send self ':height) 7 (send current-node ':height)))) (cond ((and (>= new-top 2) (<= new-top bottom-boundary)) nil) ; OK, return nil ((< new-top 2) (send top-node ':set-top (+ origin-top (- 2 new-top))) t) ; Was bad, return t (t (send top-node ':set-top (- origin-top (- new-top bottom-boundary))) t)))) ; Was bad, return t (defmethod (chart-pane :add-below) () (send current-node ':add-below)) (defmethod (chart-pane :add-right) () (send current-node ':add-right)) (defmethod (chart-pane :add-left) () (send current-node ':add-left)) (defmethod (chart-pane :add-above) () (send current-node ':add-above)) (defmethod (chart-pane :open) () (send current-node ':set-opened? t) (send self ':refresh)) (defmethod (chart-pane :open-below) () (loop for kid in (send current-node ':below) do (send kid ':set-opened? t)) (send self ':refresh)) (defmethod (chart-pane :close) () (send current-node ':set-opened? nil) (send self ':refresh)) (defmethod (chart-pane :close-below) () (loop for kid in (send current-node ':below) do (send kid ':set-opened? nil)) (send self ':refresh)) (defmethod (chart-pane :edit) () (send current-node ':edit)) (defmethod (chart-pane :make-this-top) () (cond ((let-user-confirm "Are you sure you want to wipe out everything above this node? ") (setq top-node current-node) (send self ':go-to-top)) (t nil))) (defmethod (chart-pane :remove-current) () (cond ((eq current-node top-node) (beep-on-terminal)) ((let-user-confirm "Are you sure you want to wipe out this node? ") (send current-node ':remove-self)) (t nil))) (defmethod (chart-pane :remove-below) () (cond ((let-user-confirm "Are you sure you want to wipe out everybody under this node? ") (send current-node ':remove-below)) (t nil))) ; First, compute DY (the amount to move up), and HERE (where you are now). ; By taking the MIN of the default *SCROLL-AMOUNT and our current-node's ; position, you ensure you never move in a step large enough to take ; your current node off the screen. ; ; "Why do you subtract 2?" you may ask... This ensures that you never ; bring the current-node to a y-coordinate of less than +2, not zero. ; Leaving it at +2 gives it a few aesthetic pixels of upper border. ; Remember, your craftsmanship and attention to aesthetic detail ; really shows in applications programs like this. ; (defmethod (chart-pane :scroll-up) () (let ((dy (min *scroll-amount (- (send current-node ':top) 2))) (here (send top-node ':top))) (send top-node ':set-top (- here dy)) (if (not (zerop dy)) (send self ':refresh)))) ; The 7 here is like the 2 above. It includes compensation for the border drawn ; around the current-node, plus the screen's blinker, which is a blinker of ; TV:BOX-BLINKER flavor and thickness 2. ; (defmethod (chart-pane :scroll-down) () (let ((dy (min *scroll-amount (- (send self ':height) 7 (send current-node ':top) (send current-node ':height)))) (here (send top-node ':top))) (send top-node ':set-top (+ here dy)) (if (not (zerop dy)) (send self ':refresh)))) (defmethod (chart-pane :scroll-left) () (let ((dx (min *scroll-amount (- (send current-node ':left) 2))) (here (send top-node ':left))) (send top-node ':set-left (- here dx)) (if (not (zerop dx)) (send self ':refresh)))) (defmethod (chart-pane :scroll-right) () (let ((dx (min *scroll-amount (- (send self ':width) 7 (send current-node ':left) (send current-node ':width)))) (here (send top-node ':left))) (send top-node ':set-left (+ here dx)) (if (not (zerop dx)) (send self ':refresh)))) ;------------------------------------------------------------------------------ ; The command pane ; (defflavor org-command-pane () (tv:command-menu) :gettable-instance-variables :settable-instance-variables :initable-instance-variables (:documentation "ORG Command Pane") (:default-init-plist :font-map '(fonts:hl12bi) :columns 4 :item-list *command-list)) (defvar *column-1-commands '(("Save as File (c-S)" :save-as-file "Save current organization chart in a file") ("Read from File (c-R)" :restore-from-file "Restore organization chart previously saved in a file") () () ("Far Left (c-A)" :far-left "Go to the furthest left on this level") ("Far Right (c-E)" :far-right "Go to the furthest right on this level") ("Parameters (m-X)" :change-parameters "Change the parameters for the whole chart") ("Edit This Node (c-X)" :edit "Edit this individual"))) (defvar *column-2-commands '(("Add Below (h-N)" :add-below "Add a new inferior") ("Add Left (h-B)" :add-left "Add a new sibling to the left") ("Add Right (h-F)" :add-right "Add a new sibling to the right") ("Add Above (h-P)" :add-above "Insert a new node above this one") ("Go Up (c-P)" :go-up "Move the current node upwards") ("Go Down (c-N)" :go-down "Move the current node downwards") ("Go Left (c-B)" :go-left "Move the current node to the left") ("Go Right (c-F)" :go-right "Move the current node upwards"))) (defvar *column-3-commands '(("Delete (c-D)" :remove-current "Delete exactly this node. (Keep its underlings, if any)") ("Delete Below (m-D)" :remove-below "Delete everything under this node") ("Go to the Top (m-<)" :go-to-top "Go to the top of the chart") () ("Scroll Up (m-P)" :scroll-up "Move the display upwards") ("Scroll Down (m-N)" :scroll-down "Move the display downwards") ("Scroll Left (m-B)" :scroll-left "Move the display to the left") ("Scroll Right (m-F)" :scroll-right "Move the display to the right"))) (defvar *column-4-commands '(("Open (c-O)" :open "Open up the current node") ("Open Below (m-O)" :open-below "Open up the underlings") ("Close (c-C)" :close "Close the current node") ("Close Below (m-C)" :close-below "Close the underlings") () () ("Make Bigger (c->)" :make-bigger "Expand this chart") ("Make Smaller (c-<)" :make-smaller "Shrink this chart"))) (defun select-command-items (command-column) (if (null command-column) '("" :no-select nil) `(,(first command-column) :eval (send *chart-pane ',(second command-column)) :documentation ,(third command-column)))) (defvar *command-list (loop for command-column-1 in *column-1-commands for command-column-2 in *column-2-commands for command-column-3 in *column-3-commands for command-column-4 in *column-4-commands collecting (select-command-items command-column-1) collecting (select-command-items command-column-2) collecting (select-command-items command-column-3) collecting (select-command-items command-column-4))) ;;; Note that, in item-list values, ("" :no-select nil) can be used for dummy ;;; (blank) menu items. ;------------------------------------------------------------------------------ ; ; The pane at the bottom of the screen ; (defflavor prompt-pane () (tv:pane-mixin tv:window)) (defmethod (prompt-pane :after :refresh)(&rest ignore) (send self ':home-cursor)) ;------------------------------------------------------------------------------ ; Org Chart Frame: The master window with many panes ; (defflavor chart-frame () (tv:process-mixin tv:bordered-constraint-frame-with-shared-io-buffer) :gettable-instance-variables :settable-instance-variables :initable-instance-variables (:documentation "Organization Chart Program Frame") (:default-init-plist :expose-p t ; expose w/o blink on instantiation :activate-p t ; activate on instantiation :save-bits ':delayed ; make save bits array on deexposure :process '(org-initial-function) :panes `((command-pane org-command-pane) (chart-pane chart-pane) (prompt-pane prompt-pane)) :constraints '((standard-configuration (command-pane chart-pane prompt-pane) ; top, middle, bottom ((command-pane :ask :pane-size)) ; As big as necessary ((prompt-pane 8 :lines)) ; 8 lines tall ((chart-pane :even)))))) ; Whatever's left over ; Look at the :PROCESS init-keyword above (in the defflavor). ; This function gets run as the "initial form" of that process. ; It gets an infinite loop running which maintains the window. ; (defun org-initial-function (window) (send window ':loop)) ; This is org's top level loop. ; It does all the work of getting input from the user, and doing ; something intelligent with it. ; (defmethod (chart-frame :loop) () (let* ((io (send self ':get-pane 'prompt-pane)) (chart (send self ':get-pane 'chart-pane)) (terminal-io io) (query-io io) (error-output io)) (loop for input = (send io ':any-tyi) do (cond ((atom input) (selectq input ((#\c-L #+LMI #\clear-screen) (send chart ':refresh) (send io ':refresh)) (#\c-F (send chart ':go-right)) (#\c-B (send chart ':go-left)) (#\c-P (send chart ':go-up)) (#\c-N (send chart ':go-down)) (#\c-A (send chart ':far-left)) (#\c-E (send chart ':far-right)) (#\m-F (send chart ':scroll-right)) (#\m-B (send chart ':scroll-left)) (#\m-P (send chart ':scroll-up)) (#\m-N (send chart ':scroll-down)) (#\h-F (send chart ':add-right)) (#\h-B (send chart ':add-left)) (#\h-P (send chart ':add-above)) (#\h-N (send chart ':add-below)) (#\c-D (send chart ':remove-current)) (#\m-D (send chart ':remove-below)) (#\c-O (send chart ':open)) (#\m-O (send chart ':open-below)) (#\c-C (send chart ':close)) (#\m-C (send chart ':close-below)) (#\c-S (send chart ':save-as-file)) (#\c-R (send chart ':restore-from-file)) (#\c-> (send chart ':make-bigger)) (#\c-< (send chart ':make-smaller)) (#\m-< (send chart ':go-to-top)) (#\c-X (send chart ':edit)) (#\end (send self ':bury)) (#\m-X (send chart ':change-parameters)))) ((listp input) (selectq (car input) (:menu (send (fourth input) ':execute (second input))) (t (beep)))))))) ;------------------------------------------------------------------------------ ; ; This is how you get ORG up and running in the first place ; Notice that this function always returns the same window, ; namely, *CHART-PANE. When you type System-O, it executes ; the function ORG, and selects whatever window is returned ; be ORG. (defun org () (if (and (boundp '*chart-frame) (not (eq ':unbound *chart-frame)) *chart-pane) *chart-pane ;else (setq *chart-frame (tv:make-window 'chart-frame)) (setq *chart-pane (send *chart-frame ':get-pane 'chart-pane)) (setq *command-pane (send *chart-frame ':get-pane 'command-pane)) (setq *prompt-pane (send *chart-frame ':get-pane 'prompt-pane)) *chart-pane)) ; This means you never have to type (org). Just type SYSTEM-O and ; it will find the chart-frame and display it for you. ; (tv:add-system-key #\O '(org) "Organization Chart") ; This will get printed whenever you load this file (format t "~%Org loaded. Type SYSTEM-O to begin.~2%")