;;; -*- Mode:LISP; Package:SITE-DATA-EDIT; Base:10; Readtable: CL -*- ;;; Copyright (c) Lisp Machine Inc., 1986. ;;; Used by the :PRINT-ITEM-LINE method of the edit windows; this puts out one line, letting the ;;; line itself take care of most of the hair. (defun print-data-line (item -arg- window item-number) (declare (ignore -arg- item-number)) ; we don't use the PRINT-FUNCTION-ARG instance variable (send item :output window)) ;;; Note that there's not neccessarily a one-to-one mapping between flavors and classes of ;;; attributes. The flavors are mostly for sorting out the data typing cruft. (defflavor attribute ((key nil) ; can be used for anything you want... (name "Attribute") ; string for printing (ok-if-void-p nil) (prompt-string nil) (new-p nil) (delete-action nil) ; If something can be deleted, and this is non-NIL ; the value is SENT to attribute, with the editing ; window as an argument, instead of deleting ; the attribute from the list. This is used to keep at ; at least one multiple attribute around for editting. old-value) () (:required-methods :output-value ; (WINDOW) output value onto the editor window, being mousable :cue-string ; a string which documents the format of the attribute :value ; returns the actual top-level value of the attribute :set-value ; sets the value :edit ; frob the data, guiding the user (CASE method combination) ) (:inittable-instance-variables key name ok-if-void-p prompt-string new-p delete-action old-value) (:settable-instance-variables delete-action) :gettable-instance-variables :abstract-flavor (:required-init-keywords :value) (:method-combination (:case :base-flavor-last :edit :documentation-string) (:and :base-flavor-first :verify) (:append :base-flavor-first :copy-init-plist)) (:documentation "The basic attribute flavor. It sets up the right method combination, and provides useful instance variables and default methods.")) ;;; Note that OLD-VALUE is canonicalized by the time anyone sees it. (defmethod (attribute :after :init) (plist) (declare (ignore plist)) (setq old-value (if new-p nil (copy-tree (send self :value))))) (defmethod (attribute :print-self) (stream print-depth escape-p) (declare (ignore print-depth)) (format stream "#<") (write (type-of self) :stream stream :escape escape-p) (format stream " Attribute [~A], " name) (send self :print-value stream escape-p) (write-string ">" stream)) (defmethod (attribute :print-value) (stream escape-p) (write-string "value " stream) (write (send self :value) :stream stream :escape escape-p)) (defmethod (attribute :void-p) () "If non-NIL true, this attribute can be considered empty." (not (send self :value))) ;;; :multiple-p ; can this type appear more than once ? ;;; :verify ; returns non-NIL if the line is semantically OK (AND method combination) (defmethod (attribute :verify) () t) ;;; The :EDIT method is used is follows: the command loop gets a blip of the form ;;; (item-type item); if the item is an instance, (send item :edit item-type) happens; ;;; otherwise (lexpr-send (car item) :edit item-type (cdr item)) happens. ;;; This is how multiple-choice (like the CVV :ASSOC type) frobs are done. ;;; * Is the item well-formed ? (if possible for user only to complete part) (defmethod (attribute :complete-p) () (not (send self :void-p))) (defmethod (attribute :output-attribute) (window) (send window :set-current-font attribute-font) (write-string name window)) (defmethod (attribute :default-string) () (if (send self :multiple-p) "New" "Default")) (defmethod (attribute :output-empty-attribute) (window) (send self :output-void-item window)) ;;; Output one mouseable thing. (defmethod (attribute :output-void-item) (window) (send window :set-current-font unhighlighted-choice-font) (send window :item1 self 'void #'(lambda (ignore window default-string) (write-string default-string window)) (if ok-if-void-p (send self :default-string) "Fill in"))) (defun write-cue-string (string window) "Write a string in the cue font. Does NOT restore the current font." (send window :set-current-font cue-font) (write-string string window)) ;;; The method called by the window's top-level item output function. (defmethod (attribute :output) (window) (send window :item1 self 'attribute #'(lambda (x window) (send x :output-attribute window))) (write-string ": " window) (write-cue-string (send self :cue-string) window) (write-string " " window) (send window :clear-rest-of-line) ; Grumble, :REDISPLAY on the window doesn't seem to do this. (if (send self :void-p) (send self :output-empty-attribute window) (progn (send window :set-current-font default-font) (send self :output-value window)))) (defmethod (attribute :documentation-string) (type) (declare (ignore type)) "Click left to edit the value") (defmethod (attribute :case :documentation-string attribute) () (if (send self :multiple-p) "Click left to add a line, middle to delete it, right to revert it" "Click right to revert the value")) (defmethod (attribute :case :edit attribute) () (mousecase (:l (send self :edit-new *window*)) (:m (send self :edit-delete *window*)) (:r (send self :edit-revert *window*)))) (defmethod (attribute :case :documentation-string void) () "Click to start a new one") (defmethod (attribute :prompt-string) () (or prompt-string (send self :name))) (defmethod (attribute :editing-type) () (type-of self)) (defmethod (attribute :case :edit void) () (send self :edit (send self :editing-type))) (defmethod (attribute :multiple-p) () nil) ;;; This should return either NIL or a string to be edited (defmethod (attribute :value-string) () ()) ;;; The following three methods make good values for DELETE-ACTION. :DELETE-FROM-WINDOW is the ;;; default. The method should return an attribute to redisplay (usually self) or NIL. ;;; If the second value is non-NIL, then mark the attribute as changed. It is thus possible ;;; for a changed attribute to be void, if it held a real value when it was created. (defmethod (attribute :reset-value) (ignore) (send self :set-value nil) (values self (not new-p))) (defmethod (attribute :do-not-delete) (ignore) ()) (defmethod (attribute :delete-from-window) (window) (send window :delete-item (send window :number-of-item self)) (send window :note-deleted-attribute self) ()) (defmethod (attribute :edit-delete) (window) (when ok-if-void-p (send self (or delete-action :delete-from-window) window))) (defmethod (attribute :copy-init-plist) () '()) (defmethod (attribute :edit-new) (window) (when (send self :multiple-p) (let ((new (apply #'make-instance (type-of self) :value nil :name name :ok-if-void-p t :key key :new-p t (send self :copy-init-plist)))) (let ((index (send window :number-of-item self))) (if (= index (+ 1 (send window :number-of-items))) (send window :append-item new) (send window :insert-item (+ index 1) new))) (values new nil)))) (defmethod (attribute :after :edit) (&rest ignore) (send self :note-edit *window*)) ;;; This should do anything out of the ordinary to update the display, other objects, etc. ;;; It is called by the :AFTER :EDIT method of attributes. (defmethod (attribute :note-edit) (window) (declare (ignore window)) nil) (defmethod (attribute :edit-revert) (window) (unless new-p (send self :set-value (copy-tree old-value)) (send self :note-edit window) (send window :note-reverted-attribute self) (values self ()))) (defflavor value-variable-mixin ((value nil)) () (:required-flavors attribute) :inittable-instance-variables :gettable-instance-variables :settable-instance-variables :abstract-flavor (:documentation "Mix this in if you want the instance variable VALUE")) (defmethod (value-variable-mixin :void-p) () (null value)) (defflavor value-method-mixin () () (:required-flavors attribute) (:required-methods :value :set-value) (:init-keywords :value) :abstract-flavor (:documentation "Use this, and define :VALUE and :SET-VALUE; initialization is taken care of by :SET-VALUE")) (defmethod (value-method-mixin :before :init) (plist) (send self :set-value (get plist :value))) ;;; Pretty names: a few words on one line, case preserved (defflavor one-line-string-attribute () (value-variable-mixin attribute) :abstract-flavor (:method-combination (:pass-on (:base-flavor-last string) :verify-string)) (:required-methods :legal-character-bit-vector)) (defmethod (one-line-string-attribute :cue-string) () "Token") (defun make-initial-character-bit-vector (lo upto) "Make a bit map that allows all characters except LO and UPTO The character codes serve as the indices in the vector; non-zero elements mark the legal characters." (let ((s (zl:make-array char-code-limit :element-type '(mod 2) :area permanent-storage-area))) (loop for i from (char-code lo) below (char-code upto) do (setf (aref s i) 1)) s)) (defun verify-string (string vector) "Non-NIL if STRING does not contain any characters deemed illegal by the bit vector." (dotimes (i (string-length string) t) (when (zerop (aref vector (zl:aref string i))) (return nil)))) (defmethod (one-line-string-attribute :and :verify) () (or (null value) (verify-string value (send self :legal-character-bit-vector)))) (defmethod (one-line-string-attribute :value-string) () (send self :value)) ;;; The edit method is passed the typeout item type as the first argument. (defmethod (one-line-string-attribute :case :edit name) () (if (send self :operation-handled-p :choose) (send self :choose) (send self :line-edit :initial-input (send self :value-string)))) (defmethod (one-line-string-attribute :verify-string) (string) string) ;;; This should either return a string or abort out with the ABORT-EDIT function. (defmethod (one-line-string-attribute :pass-on :verify-string) (string) (if (verify-string string (send self :legal-character-bit-vector)) string (abort-edit))) (defmethod (one-line-string-attribute :line-edit) (&rest args) "The standard way of editing a string attribute." (setq value (send self :verify-string (lexpr-send self :line-edit-string args)))) (defmethod (one-line-string-attribute :line-edit-string) (&key initial-input) "The standard way of editing a string attribute." (let ((me self)) ;> FMH ! (condition-case (result) (with-input-editing (*query-io* `((:initial-input ,initial-input))) (prompt-and-read :string-or-nil "~&~A: " (send me :prompt-string))) ;> FMH ! ((sys:abort) (abort-edit)) (:no-error (or result (abort-edit)))))) (defmethod (one-line-string-attribute :output-value) (window) (send window :item1 self 'name #'(lambda (x window) (send x :write-string window)))) (defmethod (one-line-string-attribute :write-string) (window) (send window :string-out value)) (defflavor pretty-name () (one-line-string-attribute)) (defparameter pretty-name-bit-vector (make-initial-character-bit-vector #\Center-Dot (make-char #o200))) (defmethod (pretty-name :legal-character-bit-vector) () pretty-name-bit-vector) (defmethod (pretty-name :cue-string) () "String") (defflavor ascii-string () (one-line-string-attribute) (:documentation "Good defense against braindamaged network software")) (defparameter ascii-string-bit-vector (make-initial-character-bit-vector #\Space #\Integral)) (defmethod (ascii-string :legal-character-bit-vector) () ascii-string-bit-vector) (defmethod (ascii-string :cue-string) () "String") (defflavor atomic-name () (one-line-string-attribute) (:default-init-plist :name "Name") (:documentation "For an `atomic' string of alphanumerics, and some other characters like - and _ and .")) (defmethod (atomic-name :cue-string) () "Token") (defmethod (atomic-name :pass-on :verify-string) (string) (string-upcase string)) (defun make-atomic-name-bit-vector () (let ((v (make-initial-character-bit-vector #\- #\`))) ; get upper case and some punctuation ;; Nuke other random characters in that range (map () #'(lambda (c) (setf (aref v (char-code c)) 0)) "/:;<>=@?") v)) (defparameter atomic-name-bit-vector (make-atomic-name-bit-vector)) (defmethod (atomic-name :legal-character-bit-vector) () atomic-name-bit-vector) ;;; For usually restricted choices, but still allowing other choices (defflavor choice-or-any (parse-function) (value-variable-mixin attribute) :inittable-instance-variables :gettable-instance-variables :abstract-flavor (:required-methods :choices :output-choice) (:documentation "Use this to choose from a set of objects, or from the keyboard. If the keyboard is used, the input is parsed (from a string) by PARSE-FUNCTION.")) (defmethod (choice-or-any :append :copy-init-plist) () (list :parse-function parse-function)) ;;; This really ought to be in the system. (defwindow-resource temporary-menu () :make-window (temporary-menu) :initial-copies 1 :reusable-when :deexposed) (defun choose-or-read (choices parse-function prompt-string) (using-resource (menu temporary-menu) (send menu :set-item-list (append choices '(("" :no-select t) ("From the keyboard" :value .read. :font :menu-standout) ("Abort" :value .abort. :font :menu-standout)))) (send menu :set-label prompt-string) (unwind-protect (progn (tv:expose-window-near menu '(:mouse)) (let ((result (send menu :choose))) (case result (.read. (let ((string (zwei:typein-line-readline-near-window menu "~40A" (string-append prompt-string #\:)))) (when string (funcall parse-function string)))) (.abort. (abort-edit)) (otherwise result)))) (send menu :deactivate)))) (defmethod (choice-or-any :editing-type) () 'choice-or-any) (defmethod (choice-or-any :output-value) (window) (send window :item1 self (send self :editing-type) #'(lambda (x window) (send x :output-choice window)))) (defmethod (choice-or-any :case :documentation-string choice-or-any) () "Click left for a menu, or to use the keyboard") (defmethod (choice-or-any :case :edit choice-or-any) () (setq value (choose-or-read (send self :choices) parse-function (send self :prompt-string)))) (defun string-to-keyword (string) (let ((*package* (find-package 'keyword))) (read-from-string string))) (defvar *restriction-alist* ()) (defun get-restriction (class) (cdr (assoc class *restriction-alist*))) (defun add-restrictions (class &rest suggestions) "Associates CLASS (a symbol) with other keywords that are good values for this class." (let ((entry (assoc class *restriction-alist*))) (if entry (setf (cdr entry) (union suggestions (cdr entry))) (push (cons class (copy-list suggestions)) *restriction-alist*)))) (defflavor restricted-keyword (restriction) (choice-or-any) (:default-init-plist :parse-function 'string-to-keyword) :inittable-instance-variables (:documentation "Allows choices from a menu of a keyword, but the user can still type one in. The RESTRICTION is a symbol that is the name of the class; see the function ADD-RESTRICTIONS.")) (defmethod (restricted-keyword :append :copy-init-plist) () (list :restriction restriction)) (defmethod (restricted-keyword :choices) () (get-restriction restriction)) (defmethod (restricted-keyword :cue-string) () "Keyword") (defmethod (restricted-keyword :void-p) () (not value)) (defmethod (restricted-keyword :output-choice) (window) (send window :string-out (symbol-name value))) ;;; CHOICE is alist of strings and values. (defflavor choice (cached-choices) (value-variable-mixin attribute) (:init-keywords :choices) (:documentation "This is much like the :ASSOC type of choose-variable. The CHOICES are an alist of strings and objects to choose.")) (defmethod (choice :cue-string) () "Choice") (defmethod (choice :append :copy-init-plist) () (list :choices (mapcar #'second cached-choices))) (defmethod (choice :before :init) (plist) (setq cached-choices (mapcar #'(lambda (c) (list self c)) (get plist :choices)))) (defmethod (choice :output-value) (window) (do* ((choices cached-choices (cdr choices)) (c (first choices) (first choices))) ((null choices)) (send window :set-current-font (if (eql (cdr (second c)) value) highlighted-choice-font unhighlighted-choice-font)) (send window :item1 c 'choice #'(lambda (x window) (send window :string-out (car (second x))))) (when (rest choices) (send window :tyo #\Space)))) (defmethod (choice :case :edit choice) (new-item) (setq value (cdr new-item))) (defmethod (choice :case :documentation-string choice) () "Click here to select this value") (defflavor boolean () (choice) (:default-init-plist :choices '(("Yes" . t) ("No" . nil)))) (defmethod (boolean :void-p) () nil) ;;; Random tuple data-type -- Not complete yet. (defflavor n-tuple (n cue-strings item-types) (value-variable-mixin attribute) :inittable-instance-variables :gettable-instance-variables (:required-init-keywords :n :cue-strings :item-types) (:method-combination (:case :base-flavor-last :output-element))) (defmethod (n-tuple :before :init) (ignore) (check-type n (integer 1)) (unless (= n (length cue-strings) (length item-types)) (error "Length of element descriptors are mismatched: ~S and ~S" cue-strings item-types))) (defmethod (n-tuple :after :init) (ignore) (unless value (setq value (make-list n :initial-element nil)))) (defmethod (n-tuple :cue-string) () "Items") (defmethod (n-tuple :void-p) () (some #'identity value)) (defmethod (n-tuple :complete-p) () (every #'identity value)) (defmethod (n-tuple :output-element) (type element window) (declare (ignore type)) (princ element window)) (defmethod (n-tuple :output-value) (window) window) (defflavor user-property () (value-variable-mixin attribute) (:default-init-plist :ok-if-void-p t :name "Property") (:documentation "Represents an arbitrary property. This consists of a keyword (stored in the KEY) instance variable and a value, which can be any Lisp object.")) (defmethod (user-property :cue-string) () "Pair") (defmethod (user-property :multiple-p) () t) (defmethod (user-property :void-p) () (null key)) (defmethod (user-property :output-value) (window) (send window :set-current-font cue-font) (write-string "Indicator " window) (send window :set-current-font default-font) (send window :item1 self 'property-indicator #'(lambda (ignore w i) (princ i w)) key) (write-char #\Space window) (send window :set-current-font cue-font) (write-string "Value " window) (send window :set-current-font default-font) (send window :item1 self 'property-value #'(lambda (ignore w v) (prin1 v w)) value)) (defmethod (user-property :property) () key) (defun call-with-standard-lisp-io-parameters (thunk) (let ((*print-level* nil) (*print-length* nil) (*print-base* 10) (*read-base* 10) (*print-pretty* t) (*print-array* t) (*print-circle* t) (*print-case* :upcase)) (funcall thunk))) (defmacro with-standard-lisp-io-parameters (&body body) `(call-with-standard-lisp-io-parameters #'(lambda () (declare (sys:downward-function)) ,@body))) (defun read-lisp-object (prompt &key (type t) default) (fresh-line *query-io*) (with-standard-lisp-io-parameters (let ((rh-options (when default `((:initial-input ,(prin1-to-string default)))))) (condition-case (value) (flet ((input () (with-input-editing (*query-io* rh-options) (prompt-and-read :read "~A: " prompt)))) (do ((x (input) (input))) ((and (typep x type) (let ((*print-length* 5) (*print-level* 3)) (y-or-n-p "The object is ~S, OK ?" x))) x) (format *query-io* "~&Please supply ~A.~%" (si::type-pretty-name type)))) ((sys:abort) (abort-edit)))))) (defmethod (user-property :case :documentation-string property-indicator) () "Click to edit the property name") (defmethod (user-property :case :edit property-indicator) () (setf key (intern (symbol-name (read-lisp-object "Indicator" :type 'symbol :default key)) 'keyword))) (defmethod (user-property :case :edit property-value) () (setf value (read-lisp-object "Value (a Lisp object)" :default value))) (defmethod (user-property :case :documentation-string property-indicator) () "Click to edit the name of the property") (defmethod (user-property :case :edit user-property) () (send self :edit 'property-indicator) (send self :edit 'property-value)) (defun make-list-of-attributes (flavor values &rest init-options) "This makes a list of attribute lines of FLAVOR, for each VALUE. INIT-OPTIONS can also be passed along. If VALUES is NIL, one new attribute is made." (if values (let ((list (mapcar #'(lambda (value) (apply #'make-instance flavor :value value init-options)) values))) (send (car list) :set-delete-action :reset-value) list) (ncons (apply #'make-instance flavor :value nil :ok-if-void-p t :new-p t :delete-action :reset-value init-options)))) (compile-flavor-methods pretty-name ascii-string atomic-name restricted-keyword choice boolean user-property)