;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: LAPIDARY; 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. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; -*- Mode: Lisp; Package: LAPIDARY -*-
;;;
#|
========================================================================
Change log:
         8/24/92 Andrew Mickish - Removed declaration of agg-name in
                   Make-Aggregate.
         5/22/92 Brad Vander Zanden - fixed ungroup so that it handles
                   single objects in a-formula-depends-on
========================================================================
|#
;;; This file provides the functions that handle aggregadgets in Lapidary
;;; Lapidary supports the operations "make aggregate", "ungroup", 
;;; "send-to-back", and "bring-to-front". Ungroup can only be applied
;;; to top-level aggregates. This simplifies the handling of link slots,
;;; as explained below.
;;;
;;; Handling of constraints: In lapidary, constraints alway reference
;;;   other objects indirectly through links. If a constraint references
;;;   an object in the same aggregadget, the link contains a formula that
;;;   traverses the aggregate hierarchy to find the object. If a constraint
;;;   references an object outside the aggregadget, the link directly points
;;;   at the object (no formula is involved). 
;;;
;;; Rationale for ungroup: By only permitting ungroup on top-level
;;;   aggregadgets, we can avoid having to translate formulas, which
;;;   is messy and error-prone. Ungroup changes the aggregate hierarchy,
;;;   and therefore, formulas which traverse the aggregate hierarchy must
;;;   be translated. The one exception is if the top-level aggregadget
;;;   is destroyed, since formulas that went through the componets of this
;;;   aggregadget should be destroyed rather than translated (the components 
;;;   are no longer part of the same aggregadget, and therefore, links should 
;;;   point directly at these objects). Destroying formulas is manageable,
;;;   so ungroup is permitted at the top level (indeed, it is necessary to
;;;   provide some sort of ungroup!).
;;;

(in-package "LAPIDARY" :use '("LISP" "KR"))
      
;;; =====================================================
;;; add a list of selected objects to an aggregate. if
;;; the aggregate does not already exist, create it. the
;;; objects must be at the top-level.
;;; =====================================================

(defun make-aggregate (&optional selected-objects)
  
  (when (not selected-objects)
	(setf selected-objects (remove-duplicates
				(g-value *selection-info* :selected))))

  (when (not selected-objects)
	(lapidary-error "You must have one or more objects selected
before creating an aggregate")
	(return-from make-aggregate))

  (let* (new-agg
	 (kr::*constants-disabled* t) 
	 (current-agg (g-value (car selected-objects) :parent))
	 (current-agg-components 
	  (copy-list (g-value current-agg :components))))
    
    ; determine if the selected-objects all belong to the top-level aggregate.
    ; if they don't, print an error message and return
  
    (if (not (top-level-aggregate-p selected-objects))
	(return-from make-aggregate))
    
    ;; compute the bounding box of the new aggregate--while computing the
    ;; bounding box, determine if all of its components have formulas in
    ;; the :left and :top slots. If they do, then leave the default formula
    ;; for computing the :left and :top slots in the aggregate
    (let ((left most-positive-fixnum)
	  (right 0)
	  (top most-positive-fixnum)
	  (bottom 0)
	  (formula-left-p t)
	  (formula-top-p t))
      (dolist (obj selected-objects)
	      (when (not (formula-p (get-value obj :left)))
		    (setf formula-left-p nil))
	      (when (not (formula-p (get-value obj :top)))
		    (setf formula-top-p nil))
	      (when (< (g-value obj :left) left)
		    (setf left (g-value obj :left)))
	      (when (> (opal:right obj) right)
		    (setf right (opal:right obj)))
	      (when (< (g-value obj :top) top)
		    (setf top (g-value obj :top)))
	      (when (> (opal:bottom obj) bottom)
		    (setf bottom (opal:bottom obj))))
	      
      ;; create the aggregate
      (with-constants-disabled
       (setf new-agg
	     (cond ((and formula-left-p formula-top-p)
		    (create-instance nil opal:aggregadget))
		   (formula-left-p
		    (create-instance nil opal:aggregadget
				     (:top top)))
		   (formula-top-p
		    (create-instance nil opal:aggregadget
				     (:left left)))
		   (t
		    (create-instance nil opal:aggregadget
		      (:left left)
		      (:top top)))))))
;		      (:width (- right left))
;		      (:height (- bottom top)))))

    ;; generate a name for the new object
    (name-lapidary-obj new-agg)

    ; add the aggregate to the children's previous parent
    (opal:add-component current-agg new-agg)

    ;; add the selected objects to the new aggregate in the order in
    ;; which they appear in the old aggregate.
    (dolist (item current-agg-components)
      (when (member item selected-objects)
	(lapidary-add-component new-agg item current-agg)))

    ;; if link slots in the selected objects point to each other, insert
    ;; formulas in the link slots that will reach these objects by 
    ;; traversing the aggregate hierarchy
    (install-formulas-in-links (get-all-objects selected-objects))

    ; deselect the selected items and select the new aggregate
    (deselect-objects)
    (primary-select new-agg)
    
    new-agg))

;;; ===================================================== 
;;; if an object's left, top, width, or height slots
;;; are unconstrained, constrain them to the aggregate's
;;; slots so that they maintain their current offsets
;;; with respect to the aggregate's left, top, width
;;; and height slots. also recompute the bounding boxes 
;;; of the components' old and new aggregates 
;;; ===================================================== 

(defun attach-agg-constraints (aggregate item)
  ;; only constrain the components if their new aggregate is
  ;; not the editor aggregate
  (when (not (eq aggregate (g-value aggregate :window :editor-agg)))
    (if (is-a-line-p item)
	(progn
	  (when (not (formula-p (get-value item :x1)))
		(s-value item :x1-over (o-formula (gvl :parent)))
		(s-value item :y1-over (o-formula (gvl :parent)))
		(s-value item :x1-offset (- (g-value item :x1)
					    (g-value aggregate :left)))
		(s-value item :y1-offset (- (g-value item :y1)
					    (g-value aggregate :top)))
		(s-value item :x1 (kr::make-into-o-formula
				   (formula gg::*x1-to-box-left* 0
					   (:links (list :x1-over))
					   (:offset :x1-offset))))
		(s-value item :y1 (kr::make-into-o-formula
				   (formula gg::*y1-to-box-top* 0
					   (:links (list :y1-over))
					   (:offset :y1-offset)))))

	  (when (not (formula-p (get-value item :x2)))
		(s-value item :x2-over (o-formula (gvl :parent)))
		(s-value item :y2-over (o-formula (gvl :parent)))
		(s-value item :x2-offset (- (g-value item :x2)
					    (g-value aggregate :left)))
		(s-value item :y2-offset (- (g-value item :y2)
					    (g-value aggregate :top)))
		(s-value item :x2 (kr::make-into-o-formula
				   (formula gg::*x2-to-box-left* 0
					   (:links (list :x2-over))
					   (:offset :x2-offset))))
		(s-value item :y2 (kr::make-into-o-formula
				   (formula gg::*y2-to-box-top* 0
					   (:links (list :y2-over))
					   (:offset :y2-offset))))))

      (progn
	(when (not (formula-p (get-value item :left)))
	      (s-value item :left-over (o-formula (gvl :parent)))
	      (s-value item :left-offset
		       (- (g-value item :left) (g-value aggregate :left)))
	      (s-value item :left (kr::make-into-o-formula
				   (formula gg::*left-inside-formula* 0
					   (:links (list :left-over))
					   (:offset :left-offset)))))
	(when (not (formula-p (get-value item :top)))
	      (s-value item :top-over (o-formula (gvl :parent)))
	      (s-value item :top-offset
		       (- (g-value item :top) (g-value aggregate :top)))
	      (s-value item :top (kr::make-into-o-formula
				  (formula gg::*top-inside-formula* 0
					  (:links (list :top-over))
					  (:offset :top-offset)))))))))
#|
	(when (not (formula-p (get-value item :width)))
	      (s-value item :width-over (formula `(gvl :parent)))
	      (s-value item :width-scale 1.0)
	      (s-value item :width-difference
		       (- (g-value item :width) (g-value aggregate :width)))
	      (s-value item :width (formula gg::*width-formula*)))
	(when (not (formula-p (get-value item :height)))
	      (s-value item :height-over (formula `(gvl :parent)))
	      (s-value item :height-scale 1)
	      (s-value item :height-difference
		       (- (g-value item :height) (g-value aggregate :height)))
	      (s-value item :height (formula gg::*height-formula*)))))))
|#
;;; ===================================================== 
;;; remove the components from an aggregate and destroy
;;; the aggregate
;;; =====================================================

(defun ungroup (&optional (aggregate nil))

  ; determine the aggregates that must be destroyed
  (let ((selected-objects (if aggregate
			      (cons aggregate nil)
			      (g-value *selection-info* :selected)))
	depends-on)
    
    (when (not aggregate) (deselect-objects))
    (dolist (aggregate selected-objects)
      (cond ((is-a-p aggregate opal:aggregate)
	     (if (eq (g-value aggregate :window :editor-agg)
		     (g-value aggregate :parent))
		 
		 (let ((new-agg (g-value aggregate :parent))
		       (components (copy-list 
				    (g-value aggregate :components))))
		 
		   ;; first destroy all instances of this aggregate
		   (dovalues (inst aggregate :is-a-inv :local t)
			     (destroy-lapidary-object inst))

		   ;; smash any formulas that are in link slots that 
		   ;; point to objects which will no longer be in the
		   ;; same aggregadget. these formulas must go through
		   ;; the :parent slot of one of the top-level
		   ;; components--note this would be automatically
		   ;; accomplished by destroy-lapidary-object, but
		   ;; lapidary-add-component is executed first and it
		   ;; needs to have these link slots cleaned out
		   (dolist (item components)
		     (dolist (formula (kr::get-dependents item :parent))
			   (gg:cg-destroy-constraint (kr::on-schema formula)
					       (kr::on-slot formula)))
		   
		   ;; smash any formulas in the position or size slots that
		   ;; depend on the parent
		     (dolist (slot (if (is-a-line-p item)
				     '(:x1 :x2 :y1 :y2)
				     '(:left :top :width :height)))
			     (let ((value (get-value item slot)))
			       (when (and (formula-p value)
					  (or (eq aggregate
						  (setf depends-on 
							(kr::a-formula-depends-on value)))
					      (and (listp depends-on)
						   (member aggregate depends-on))))
				   (gg:cg-destroy-constraint item slot)))))
		   
		   ;; remove all formulas in the aggregate's interactors
		   ;; that depend on the aggregate
		   (dolist (item (g-value aggregate :behaviors))
		     (dolist (formula (kr::get-dependents item :operates-on))
			   (gg:cg-destroy-constraint (kr::on-schema formula)
					       (kr::on-slot formula))))


		   ;; now move the components to the top-level editor aggregate
		   (dolist (item components)
		     (lapidary-add-component new-agg item aggregate
					     :behind aggregate))

		   ;; destroy the aggregate
		   (destroy-lapidary-object aggregate))
	       ; else 
	       ;; signal an error--object is not a top-level aggregate and thus
	       ;; cannot be ungrouped
	       (format t "~% object ~S is not a top-level aggregate and thus cannot be ungrouped" aggregate)))
	    (t
	     (lapidary-beeps 1)
	     (format t "~% object ~S is not an aggregate--it cannot be ungrouped ~%"
		     aggregate))))))
					 

;;; ===================================================== 
;;; bring a component to the front or back of an aggregate
;;; =====================================================

(defun change-covering-order (where)

  (let ((selected-objects (g-value *selection-info* :selected)))
    (when (null selected-objects)
	  (lapidary-error "You must first select one or more objects")
	  (return-from change-covering-order))
    
    (let* ((aggregate (g-value (car selected-objects) :parent))
	   (aggregate-components (copy-list (g-value aggregate :components))))
    
    ; if there is only one selected object, we can simply move the
    ; object to the front of the aggregate; otherwise we have to
    ; move objects in the order in which they appear in the aggregate
    
    (cond ((null (cdr selected-objects))
	     (opal:move-component aggregate (car selected-objects) 
				  :where where))
	  
	  (t
	   ; determine if the selected-objects all belong to the same aggregate.
	   ; if they don't, print an error message and return
	   
	   (when (not (same-aggregate-p aggregate selected-objects))
	       (return-from change-covering-order))
	   
	   ; send the selected objects to the front of the aggregate in the
	   ; order that they appear in the aggregate
	   
	   (dolist (item (if (eq where :front)
			     aggregate-components
			     (reverse aggregate-components)))
	     (when (member item selected-objects)
	       (opal:move-component aggregate item :where where))))))))

#|
;;; =====================================================
;;; add a list of selected objects to an aggregate.
;;; =====================================================

(defun lapidary-add-components ()
  
    (let* ((s-selected (g-value *selection-info* :s-selected))
	   (new-agg (car s-selected))
           (selected-objects (g-value *selection-info* :p-selected))
	   (current-agg (g-value (car selected-objects) :parent))
	   (current-agg-components 
	    (copy-list (g-value current-agg :components))))
      
      ; if the secondary selection is not an aggregate, raise an error
      ; condition

      (when (not (is-a-p new-agg opal:aggregate))
	(lapidary-beeps 1)
	(format t "~% the secondary selection must be an aggregate but is not ~%")
	(return-from lapidary-add-components))

      ; determine if the selected-objects all belong to the top-level 
      ; aggregate. if they don't, print an error message and return
      
      (if (not (top-level-aggregate-p selected-objects))
	  (return-from lapidary-add-components))
    
      ; if there
      ; are no secondary selections or more than one, raise an error 
      ; condition, because in this case, we don't know which aggregate
      ; to use
      
      (when (not (eql (list-length s-selected) 1))
	(lapidary-beeps 1)
	(format t "~% please increase or decrease the number of
	secondary selections so that there is only one secondary
	selection ~%")
	(return-from lapidary-add-components))

      ; add the selected objects to the new aggregate in the order in
      ; which they appear in the old aggregate. objects that appear
      ; before the new aggregate should be placed at the beginning of
      ; the new aggregate; objects that appear after the new aggregate
      ; should be placed at the end of the new aggregate. the same
      ; strategy is used in changing the objects' positions in the
      ; selection order

      (let ((where :back))
	(dolist (item current-agg-components)
	  (cond ((member item selected-objects)
		 (lapidary-add-component new-agg item :where where))
		((eql item new-agg)
		 (setf where :front)))))

      ;; if link slots in either the original or new components of the
      ;; aggregate point to each other, insert
      ;; formulas in the link slots that will reach these objects by 
      ;; traversing the aggregate hierarchy
      (install-formulas-in-links (get-all-objects 
				  (g-value new-agg :components)))

      ; deselect the components but leave the aggregate selected
      (primary-deselect-objects :none)
      new-agg))

;;; =====================================================
;;; delete a group of components from an aggregate and
;;; remove any constraints that these components may
;;; have had that depended on the aggregate.
;;; ===================================================== 

(defun lapidary-remove-components (components)
  (let ((aggregate (g-value (car components) :parent)))
    
    ; it is an error to try to remove an object from the top level
    ; aggregate
    
    (when (eql aggregate (g-value aggregate :window :editor-agg))
      (lapidary-beeps 1)
      (format t "~% cannot remove objects from the top-level aggregate ~%")
      (return-from lapidary-remove-components))
    
    ; determine if the components all belong to the same aggregate.
    ; if they don't, print an error message and return
    
    (if (and (cdr components)
	     (not (same-aggregate-p aggregate components)))
	(return-from lapidary-remove-components))

    ; find the top-level aggregadget that these items belong to. the
    ; removed components will be placed behind this aggregadget
    (let ((top-level-agg aggregate)
	  (editor-agg (g-value aggregate :window :editor-agg)))
      (loop
	(when (eq editor-agg (g-value top-level-agg :parent))
	  (return))
	(setf top-level-agg (g-value top-level-agg :parent)))

      ; if there is only one component, we can simply remove the
      ; object from the aggregate; otherwise we have to
      ; remove objects in the order in which they appear in the aggregate.
  
      (cond ((null (cdr components))
	     (lapidary-add-component editor-agg (car components)
				      :behind top-level-agg))
	    (t
	 
	     ; remove the objects from the aggregate in the order in which
	     ; they appear in the aggregate 
	 
	     (let ((aggregate-components 
		    (copy-list (g-value aggregate :components))))
	       (dolist (item (reverse aggregate-components))
		 (when (member item components)
		   (lapidary-add-component editor-agg item 
					   :behind top-level-agg))))))

      ; if the aggregate from which the components were removed is now 
      ; empty, destroy it
      (when (not (g-value aggregate :components))
	    ;; destroy the link to this aggregate in the aggregate's parent, 
	    ;; then destroy the aggregate
	    (destroy-slot (g-local-value aggregate :parent)
			  (g-local-value aggregate :known-as))
	    (opal:destroy aggregate)))
))
|#

;;; ===================================================== 
;;; determine if a group of items belong to the top-level
;;; aggregate. If they don't, beep three times and print
;;; an error message.
;;; =====================================================

(defun top-level-aggregate-p (objects)
  (let ((agg (g-value (car objects) :window :editor-agg)))
    (cond ((not (subsetp objects (g-value agg :components)))
	 (lapidary-error "The selected objects must all
belong to the top-level aggregate but they do not")
	 nil)
	(t t))))

;;; ===================================================== 
;;; determine if a group of items belong to the same
;;; aggregate. If they don't, beep three times and print
;;; an error message.
;;; =====================================================

(defun same-aggregate-p (aggregate objects)
  (cond ((not (subsetp objects (g-value aggregate :components)))
	 (lapidary-error "The selected objects must all
belong to the same aggregate but they do not")
	 nil)
	(t t)))

;;; =========================================================
;;; move a component from the aggregate it currently
;;; belongs to to the specified aggregate. The object's old
;;; aggregate must be passed in because if the object
;;; is an instance of a prototype, and the prototype and
;;; the object both are being added to the new aggregate, and
;;; the prototype is removed from the old aggregate before
;;; the object, than the object will be removed when the
;;; prototype is removed and thus will no longer belong
;;; to the old aggregate. Thus to ensure that we know which
;;; aggregate an object belonged to, we must pass in the
;;; old aggregate.
;;; =========================================================

(defun lapidary-add-component (agg item old-agg &key (where nil) (behind nil))

  ;; remove all slots and formulas in the item that reference
  ;; the item's aggregate
  (remove-agg-formulas old-agg item)

  (when (g-local-value item :parent)
	(opal:remove-component old-agg item))

  ;; add the item to its new aggregate. this must be done before
  ;; constraints that tie it to the aggregate are installed, since
  ;; the constraints may fire immediately, and the link through
  ;; :parent must be set

  (cond (where (opal:add-component agg item :where where))
	(behind (opal:add-component agg item :behind behind))
	(t (opal:add-component agg item)))

  ;; attach constraints to the items that cause them to resize or
  ;; reposition themselves when their new aggregate is resized or 
  ;; repositioned
  (attach-agg-constraints agg item))


;;; =========================================================
;;; remove any links in item that point to agg (the links 
;;; old parent)
;;; =========================================================

(defun remove-agg-formulas (agg item)
  (doslots (slot item)
	   (when (and (eq (g-cached-value item slot) agg)
		      (not (eq slot :parent)))
		 (destroy-slot item slot))))


;;; =================================================================
;;; return a list of all components in the passed objects, including 
;;; the objects themselves
;;; =================================================================
(defun get-all-objects (objs)
  (let (components)
    (dolist (obj objs)
	    (when (is-a-p obj opal:aggregate)
	        (setf components (append components (transitive-closure obj)))
	        ;; get interactors
	        (setf components (append (g-value obj :behaviors) components)))
	    (push obj components))
    components))

;;; =================================================================
;;; Examine the link slots in each of the passed objects. If a link
;;; slot references another of the passed objects, place a formula
;;; in the slot that traverses the aggregate hierarchy in order to 
;;; reach the object it references
;;; =================================================================

(defun install-formulas-in-links (objs)
  (declare (special save-time-do-not-dump-slots))
  (dolist (obj objs)
    ;; find all link slots and establish a formula so that they still
    ;; refer to the object they currently point at. A link slot is a
    ;; slot that contains a view object, is not on the 
    ;; save-time-do-not-dump-slots list, and is not a :parent slot
    ;; or a link slot for an aggregadget
    
    (let ((links (g-value obj :links)))
      (doslots (slot obj)
       (let ((value (get-value obj slot)))
	 ;; determine if value is atomic or a formula. if it's a
	 ;; formula, it could be a by-demo formula. if the formula
	 ;; contains an object that has to be converted, the object
	 ;; will be found in either the true or false branches of
	 ;; the if statement. 
	 (when (or (member slot '(:left-over :top-over :width-over :height-over
				  :x1-over :x2-over :y1-over :y2-over))
		   (member slot links))
	       (if (formula-p value)
		   (progn
		     (let ((form (extract-formula value)))
		       (when (eq (first form) 'if)
			     (let ((true-expr (third form))
				   (false-expr (fourth form))
				   changed-p)
			       (when (member true-expr objs)
				     (setf (third form)
					   `(gvl ,@(gilt:make-path obj true-expr)))
				     (setf changed-p t))
			       (when (member false-expr objs)
				     (setf (fourth form)
					   `(gvl ,@(gilt:make-path obj false-expr)))
				     (setf changed-p t))
			       (when changed-p
				     (change-formula obj slot form))))))
		   (when (member value objs)
		       (s-value obj slot 
				(formula `(gvl ,@(gilt:make-path obj value))))
		       ;; immediately evaluate the formula to set up the
		       ;; dependencies
		       (g-value obj slot)))))))))

