;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: GILT; Base: 10 -*-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;         The Garnet User Interface Development Environment.      ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; This code was written as part of the Garnet project at          ;;;
;;; Carnegie Mellon University, and has been placed in the public   ;;;
;;; domain.  If you are using this code or any part of Garnet,      ;;;
;;; please contact garnet@cs.cmu.edu to be put on the mailing list. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; The "Value Filter" extension to Gilt is a tool for assigning dependencies
;;; among objects.  With the Value Control module, the values returned by
;;; gadgets can be defined by a user-supplied expression.  The Enable Control
;;; module allows the user to define expressions that regulate whether gadgets
;;; are enabled and may be operated.  The Error Checking module allows the
;;; definition of an error handling routine, complete with a customized
;;; error dialog box.
;;;
;;; Designed by Brad Myers
;;; Implemented by Andrew Mickish

;;; CHANGE LOG:
;;; 12/31/92 Andrew Mickish - Removed an update call in Remove-Panel-If-Unused
;;;            


;;
;;  Error Control module
;;

(in-package "GILT" :use '("LISP" "KR" "KR-DEBUG"))

(proclaim '(special ERROR-CHECK))

;;; *** NOTES on the implementation ***
;;;   This menu gets error value/error message pairs.  That is, when
;;; the object has a particular value, then the Gilt error gadget
;;; should pop up and display the message corresponding to that value.
;;; The value/message pairs are kept in an association list in the
;;; :error-check-alist slot of the object itself.  The formula in the
;;; :filtered-value slot (which is generated by the Value Control menu) 
;;; then takes care of popping up the appropriate message when the
;;; :value slot has an error value.


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;  Utility functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; Searches through each labeled-box-panel in the menu and finds
;; the labeled box that the user is currently editing
;;
(defun Get-Active-Box (ec)
  (do ((e-gadgets (g-value (g-value ec :panel-list) :components)
		  (cdr e-gadgets)))
      ((when e-gadgets
	 (let* ((e-gadget (car e-gadgets))
		(text-inter (g-value e-gadget :error-string
				     :field-text :text-edit)))
	   (eq (g-value text-inter :current-state) :running)))
       (when e-gadgets
	 (g-value (car e-gadgets) :error-string)))))
	
;; Insert a "(gvl ... :filtered-value)" call into the given labeled box
;;
(defun EC-Insert-the-Ref (src-obj dest-obj l-box)
  (let ((ref `(gvl ,@(make-path src-obj dest-obj) :filtered-value)))
    (gg:insert-text-into-box l-box (write-to-string ref :case :downcase))))

;; Selection function for the "Use Value of Object" button
;;
(defun EC-Insert-Ref-Obj-Into-Str (gadget val)
  (declare (ignore val))
  (let* ((ec (g-value gadget :parent))
	 (sel-obj (g-value *Selection-Obj* :value))
	 (current-box (get-active-box ec))
	 (for-obj (g-value ec :for-object)))
    (cond ((null sel-obj)(gilt-error "Nothing selected"))
	  ((listp sel-obj) (dolist (o (reverse sel-obj))
			     (EC-Insert-The-Ref for-obj o current-box)))
	  ((null current-box)(gilt-error "No error string is
currently being edited."))
	  (t (EC-Insert-The-Ref for-obj sel-obj current-box)))))



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Final functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; Called by the "Another Error Check" button.
;; Adds a new labeled-box-panel to the menu.
;;
(defun Add-Panel (gadget val)
  (declare (ignore val))
  (let* ((main-gadget (g-value gadget :parent))
	 (error-list (g-value main-gadget :panel-list)))
    (opal:add-item error-list)
    (s-value (g-value error-list :window)
	     :height
	     (+ 10 (opal:bottom (g-value main-gadget :another-panel))))
    (opal:update (g-value error-list :window))))


;; Called by the Error Value labeled box.
;; Removes the labeled-box-panel from the aggrelist when the user erases
;; the strings from the scrolling labeled boxes.
;;
(defun Remove-Panel-If-Unused (box val)
  ; If this labeled box is empty...
  (when (equal "" val)
    (let* ((e-gadget (g-value box :parent))
	   (other-box
	    (car (remove box (list (g-value e-gadget :error-value)
				   (g-value e-gadget :error-string)))))
	   (error-list (g-value e-gadget :parent))
	   (win (g-value error-list :window)))
      ; And the other labeled box is empty...
      (when (and (equal "" (g-value other-box :value))
		 (> (g-value error-list :items) 1))
	; Then remove this panel of labeled boxes from the aggrelist.
	(opal::remove-nth-item error-list (g-value e-gadget :rank))
	;(opal:update win)
	(s-value win :height (+ 10 (opal:bottom (g-value error-list :parent
							 :another-panel)))))
      (opal:update win))))


;; Called by the OK-Cancel buttons in the Error-Check menu.
;; Sets the :error-check-alist slot in the actual object with the error
;; values and strings (the strings are really format statement expressions --
;; see documentation for Set-Format-Expr).  The VC menu is responsible
;; for creating the formula that makes use of these values.
;;
(defun Set-Error-Val-Alist (ec-menu vals)
  (declare (ignore vals))
  (let* ((box-panels (g-value ec-menu :panel-list :components))
	 (for-obj (g-value ec-menu :for-object))
	 (alist NIL))
    (dolist (bp box-panels)
      (let ((error-value (g-value bp :error-value :value))
	    (error-string (g-value bp :error-string :format-expr)))
	(unless (equal "" error-value)
	  (multiple-value-bind (val errorp)
	      (gg:Careful-Read-From-String error-value *error-gadget*)
	    (unless errorp
	      (push (cons val error-string) alist))))))
    (s-value for-obj :error-check-alist alist)))


;; This function takes the value of the error-string labeled box and
;; puts the strings and object references together into a format statement
;; that can be evaluated and given to the Gilt error gadget.  The format
;; statement (in expression form, not in string form) is set in the
;; :format-expr slot of the labeled box.
;;
;; The variable *format-args* is referenced in Set-Format-Expr but only
;; set in subroutines.  *format-args* is a list of references to objects that
;; must be evaluated at the time that the message appears (i.e., we can't
;; just evaluate the g-values now because the objects will have different
;; values later).  So, we have to create a format statement that contains
;; the error message with ~A's in it, e.g.,
;;   (format NIL "Object now has value ~A" (g-value object :value))
;; This format statement will be evaluated just before the error gadget
;; is displayed, so the message will contain the right values.
;;
(defvar *format-args* NIL)

(defun Set-Format-Expr (gadget value)
  (setf *format-args* NIL)  ; Set in decompose-aux
  (s-value gadget
	   :format-expr
	   (if (equal "" value)
	       ""
	       (append `(format NIL ,(decompose-string value))
		       (reverse *format-args*)))))

(defun decompose-string (str)
  (case (char str 0)
    (#\space (decompose-string (subseq str 1)))
    (t (decompose-aux str))))

(defun decompose-aux (str)
  (let (substr substr-len remainder)
    (multiple-value-setq
	(substr substr-len) (read-from-string str))
    (unless (stringp substr)
      (push substr *format-args*)
      (setq substr "~A"))
    (setq remainder (subseq str substr-len))
    (if (equal "" remainder)
	substr
	(concatenate 'string substr (decompose-string remainder)))))

;; The functions below, which compose strings from format statements, undo
;; the work of the functions above.  The input to compose-string is a
;; format expression, like (format NIL "value = ~A" (g-value obj :value)).
;;
(defun compose-string (expr)
  (let* ((format-string (caddr expr))
	 (format-args (cdddr expr))
	 (str-len (length format-string))
	 (leading-arg (string= "~A" (subseq format-string 0 2)))
	 (trailing-arg (string= "~A" (subseq format-string (- str-len 2)))))
    ;; Add quote to head of string unless it begins with a format arg.
    (unless leading-arg
      (setq format-string (concatenate 'string "\"" format-string))
      (incf str-len))
    ;; Likewise with the tail.
    (unless trailing-arg
      (setq format-string (concatenate 'string format-string "\""))
      (incf str-len))
    ;; Insert the args into the string, along with quotes as delimiters
    (do* ((i 0 (1+ i))
	  (c (char format-string i) (char format-string i)))
	 ((= i (- str-len 1))
	  (let* ((right-trimmed-string 
		  (if trailing-arg
		      (string-right-trim '(#\" #\space) format-string)
		      format-string))
		 (left-trimmed-string
		  (if leading-arg
		      (string-left-trim '(#\" #\space) right-trimmed-string)
		      right-trimmed-string)))
	    left-trimmed-string))
      (when (and (char= #\~ c)
		 (char= #\A (char format-string (1+ i))))
	; found a ~A, replace with an arg
	(let* ((arg-string (prin1-to-string (car format-args)))
	       (arg-string-len (length arg-string)))
	  (setq format-string (concatenate 'string
				(subseq format-string 0 i)
				"\" " arg-string " \""
				(subseq format-string (+ 2 i))))
	  (incf i (+ 2 arg-string-len))
	  (incf str-len (+ 2 arg-string-len))
	  (setq format-args (cdr format-args)))))))
  

(defun Error-Check-OK-Fn (ec vals)
  (Set-Error-Val-Alist ec vals)
  (Close-EC-Win ec))

(defun Error-Check-Cancel-Fn (ec vals)
  (declare (ignore vals))
  (Close-EC-Win ec))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Top-level function to display an error check menu for an object
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


(defparameter *Used-EC-Wins* NIL)

(defun Close-EC-Win (ec)
  (setf *vc-position-list* (remove (g-value ec :position-index)
				   *vc-position-list*))
  (push (g-value ec :window) *Used-EC-Wins*))

;; Use the *vc-position-list* defined in motif-value-control.lisp so that
;; the error-check menus are aligned with the value control menus.
;;
(defun Pop-Used-EC-Win (for-obj title)
  (let ((ec-win (pop *Used-EC-Wins*)))
    (when ec-win
      (let ((ec (car (g-value ec-win :aggregate :components)))
	    (i (next-vc-position-index)))
	(push i *vc-position-list*)
	(s-value ec-win :left (next-vc-left i))
	(s-value ec-win :top (next-vc-top i))
	(s-value ec :position-index i)
	(s-value ec-win :title title)
	(s-value ec :for-object for-obj)
	(s-value ec-win :visible T)
	ec))))

(defun Create-EC-Win (for-obj title)
  (let* ((i (next-vc-position-index))
	 (ec (create-instance NIL ERROR-CHECK
		(:position-index i)
		(:for-object for-obj)
		(:window-title title))))
    (show-in-window ec (next-vc-left i) (next-vc-top i))
    ec))


(defun Destroy-Error-Check-Wins ()
  (dolist (ec (g-value ERROR-CHECK :is-a-inv))
    (opal:destroy (g-value ec :window)))
  (setf *Used-EC-Wins* NIL))


;; Set up the labeled-box panels with the error values that have already
;; been defined for the object.  The error values are in an association
;; list, i.e., ((val . string) ...).  Iterate over the list of pairs in
;; the association list and set the strings of the labeled boxes in the
;; error check panel.
;;
(defun Init-Error-Check (ec ec-vals)
  (let* ((panel-agglist (g-value ec :panel-list))
	 (old-items-length (g-value panel-agglist :items))
	 (new-items-length (max 1 (length ec-vals))))

    (unless (eql old-items-length new-items-length)
      (s-value panel-agglist :items new-items-length))

    (if ec-vals
	;; Install error check values and strings
	(do* ((panels (g-value panel-agglist :components) (cdr panels))
	      (panel (car panels) (car panels))
	      (panel-vals ec-vals (cdr panel-vals))
	      (panel-val (car panel-vals) (car panel-vals)))
	     ((null panels) T)
	  (let ((error-value (g-value panel :error-value))
		(error-string (g-value panel :error-string)))
	    (s-value error-value :value (prin1-to-string (car panel-val)))
	    (s-value error-string :value (compose-string (cdr panel-val)))
	    ;; Initialize :format-expr value, also
	    (Set-Format-Expr error-string (g-value error-string :value))))
	;; Clear the panel
	(let* ((panel (car (g-value panel-agglist :components)))
	       (error-value (g-value panel :error-value))
	       (error-string (g-value panel :error-string)))
	  (s-value error-value :value "")
	  (s-value error-string :value "")
	  (Set-Format-Expr error-string "")))
    (s-value (g-value ec :window)
	     :height
	     (+ 5 (opal:bottom (g-value ec :another-panel))))))


(defun Show-Error-Check (ec-button value)
  (declare (ignore value))
  (let* ((for-obj (g-value ec-button :parent :for-object))
	 (ec-vals (g-value for-obj :error-check-alist))
	 (title (concatenate 'string
			     "Error Check:  " (name-for-schema for-obj)))
	 (ec (or (Pop-Used-EC-Win for-obj title)
		 (Create-EC-Win for-obj title))))
    (Init-Error-Check ec ec-vals)
    (opal:update (g-value ec :window))
    ec))




;;
;;  Motif Error Control objects
;;


;; Make this an instance of a Motif-gadget-prototype, so it will get
;; the appropriate colors automatically.
;;
(create-instance 'LABELED-BOX-PANEL garnet-gadgets::MOTIF-GADGET-PROTOTYPE
  (:width (o-formula (gvl :parent :width)))
  (:height 54)
  (:right (o-formula (+ (gvl :left)(gvl :width) -3)))
  (:bottom (o-formula (+ (gvl :top)(gvl :height) -3)))
  (:parts `(
    (:top-left ,opal:polyline
      (:line-style ,(o-formula (gvl :parent :shadow-line-style)))
      (:point-list ,(o-formula (let* ((x1 (gvl :parent :left))
				      (y1 (gvl :parent :top))
				      (x2 (gvl :parent :right))
				      (y2 (gvl :parent :bottom)))
				 (list x1 y2 x1 y1 x2 y1)))))
    (:bot-right ,opal:polyline
      (:line-style ,(o-formula (gvl :parent :highlight-line-style)))
      (:point-list ,(o-formula (let* ((x1 (gvl :parent :left))
				      (y1 (gvl :parent :top))
				      (x2 (gvl :parent :right))
				      (y2 (gvl :parent :bottom)))
				 (list (1- x1) y2 x2 y2 x2 (1- y1))))))
    (:error-value ,GARNET-GADGETS:MOTIF-SCROLLING-LABELED-BOX
      (:left ,(o-formula (+ 4 (gvl :parent :left))))
      (:top ,(o-formula (+ 4 (gvl :parent :top))))
      (:width ,(o-formula (- (gvl :parent :width) 13)))
      (:label-string "If value is")
      (:value "")
      (:selection-function Remove-Panel-If-Unused))
    (:error-string ,GARNET-GADGETS:MOTIF-SCROLLING-LABELED-BOX
      (:left ,(o-formula (+ (gvl :parent :left) 18)))
      (:top ,(o-formula (+ (gvl :parent :top) 25)))
      (:width ,(o-formula (- (gvl :parent :width) 27)))
      (:label-string "Error String or Function")
      (:value "")
      (:active-p ,(o-formula (not (string-equal
				   (gvl :parent :error-value :value) ""))))
      (:selection-function ,#'(lambda (g v)
				(Set-Format-Expr g v)
				(Remove-Panel-If-Unused g v)))))))


(create-instance 'ERROR-CHECK opal:aggregadget
   (:window-left 400) (:window-top 400)
   (:window-width 490) (:window-height 161)
   (:window-title "Error Check") (:package-name "GILT")
   (:window-background-color opal:motif-gray)
   (:left 4) (:top 10)
   (:width 505)
   (:height (o-formula (- (opal:gv-bottom (gvl :another-panel)) (gvl :top))))
   (:For-Object NIL)
   (:function-for-ok 'Error-Check-OK-Fn)
   (:function-for-cancel 'Error-Check-Cancel-Fn)
   (:parts
    `((:title ,opal:text
       (:left ,(o-formula (gvl :parent :left)))
       (:top ,(o-formula (gvl :parent :top)))
       (:string ,(o-formula (concatenate 'simple-string
			     "Error Check for \""
			     (Get-User-Name (gvl :parent :for-object))
			     "\"")))
       (:font ,(opal:get-standard-font NIL :bold-italic :large)))
      (:ok-cancel ,garnet-gadgets:motif-text-button-panel
       (:left 300) ;(:left 360)
       (:top ,(o-formula (+ 5 (opal:gv-bottom (gvl :parent :title)))))
       (:direction :horizontal)
       (:items ("OK" "Apply" "Cancel"))
       (:selection-function gilt:okcancel-function))
      (:insert-ref ,GARNET-GADGETS:MOTIF-TEXT-BUTTON
       (:selection-function EC-Insert-Ref-Obj-Into-Str)
       (:string "Use Value of Object")
       (:left ,(o-formula (+ 10 (gvl :parent :left))))
       (:top ,(o-formula (+ 5 (opal:gv-bottom (gvl :parent :title)))))
       (:interactors
	((:press :modify
		 (:waiting-priority ,inter:running-priority-level)))))
      (:panel-list ,opal:aggrelist
       (:left ,(o-formula (+ 10 (gvl :parent :left))))
       (:top ,(o-formula (+ 10 (opal:gv-bottom (gvl :parent :insert-ref)))))
       (:width 465)
       (:direction :vertical)
       (:v-spacing 7)
       (:items 1)
       (:item-prototype ,LABELED-BOX-PANEL))
      (:another-panel ,GARNET-GADGETS:MOTIF-TEXT-BUTTON
       (:left ,(o-formula (+ 10 (gvl :parent :left))))
       (:top ,(o-formula (+ 5 (opal:gv-bottom (gvl :parent :panel-list)))))
       (:string "Another Error Check")
       (:selection-function ,#'Add-Panel)))))

