;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: OPAL; 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. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Changes:
;;; 07/06/93 Amickish - Adjusted scoping of gob-update-info in :add-component
;;;                     method to avoid UPDATE-INFO type error in CMUCL
;;; 03/10/93 Amickish - Now obj-in-rectangle, leaf-objects-in-rectangle, and
;;;                     components-in-rectangle only return visible objects
;;; 02/22/93 Koz/Amickish - Called do-all-components in :add-component method
;;;                     to invalidate all the children of the new component
;;; 12/31/92 Amickish - Called do-all-components in :remove-component method
;;;                     to remove components from the invalid objects list
;;; 04/29/92 Pervin   - Moved function to remove object from invalid objects
;;;			list to update-basics.lisp
;;; 04/28/92 Amickish - Wrapped destroy-slot call in with-constants-disabled
;;; 04/10/92 Amickish - Renamed FUNCTION variable in :do-components and
;;;                     :do-all-comonents methods to A-FUNCTION to avoid
;;;                     conflicts, added type checking when :do-components
;;;                     and :do-all-components call A-FUNCTION on SELF.
;;; 03/30/92 Amickish - Changed point-in-gob-method-view-object function calls
;;;                     to point-in-gob macro calls.
;;; 03/25/92 Amickish - Get-Values ---> G-Value;
;;;                     (setf (get-local-values ...)) ---> (s-value ...)
;;; 03/23/92 Pervin - In remove-component, make sure window exists before
;;;                   doing a get-local-value on it.
;;; 03/18/92 Amickish - Removed CMUCL compiler warnings: set-values--->s-value,
;;;                     bound a-window-update-info in :remove-component and
;;;                     :move-component methods.
;;; 03/16/92 Amickish - Adjusted scoping in add-component method
;;; 03/11/92 Pervin - Remove component, make sure window exists before
;;;		      doing a g-value on it.
;;; 03/05/92 Pervin - In remove-component, make sure last-inv-obj is
;;;                     still correct after ex-component is removed from
;;;			invalid objects list.
;;; 03/03/92 Amickish - s-value of :internally-parented ---> destroy-slot
;;; 02/28/92 Amickish - Removed kr::schema-slots check because schema-p works
;;;                     correctly now (returns NIL on destroyed objects)
;;; 02/27/92 Szekely - Changed :destroy-me method of aggregate to not do
;;;                    copy-list.
;;; 02/19/92 Amickish - Bound kr::*constants-disabled* in initialize method
;;;                     for aggregates.
;;; 02/06/92 Pervin - Added leaf-objects-in-rectangle, components-in-rectangle,
;;;		      and obj-in-rectangle.
;;; 02/01/92 Amickish - Added :internally-parented checks to opal:aggregate's
;;;                     add-component method.
;;; 11/23/91 Amickish&Koz - Rewrote move-component so now it does not call
;;;                         add-component.  Instead, both methods call
;;;                         install-component, a function whose code used
;;;                         to be part of add-component.
;;; 11/6/91 Pervin - Made move-component a method.
;;; 9/25/91 Pervin - Add-component takes a new optional argument :move
;;;		     which is only invoked if it is called from move-component
;;; 8/29/91 Pervin - In remove-component, make sure that object being
;;;                  removed from aggregate belongs to that aggregate!
;;; 3/25/91 Pervin - Added :local t to dovalues loop.
;;; 3/4/91  D'Souza - Removed nickname "MO" of package Opal.
;;; 7/11/90 Ed Pervin - new :destroy-me method
;;; 6/1/90   RBD Fixed add-component to accept :head :tail :after :before
;;; 5/20/90  BVZ Changed g-value to g-local-value in add-component
;;;              to fix window bug.
;;; 5/8/90   Mike Sannella
;;;              Do-Components and Do-All-Components were ignoring
;;;              their :type argument.
;;; 3/6/90   ECP Allowed point-to-leaf, point-to-component,
;;;              do-components, and do-all-components to accept
;;;              a list of types as the argument to :type.
;;; 1/2/90   RBD Corrected a misplaced parenthesis in add-component.
;;;              Also, in add-component, replaced calls to append-value
;;;		 by calls to nconc.
;;; 12/11/89 ECP Point-to-leaf was returning T when :type was aggregate.

(in-package "OPAL" :use '("LISP" "KR"))

;;; Aggregate objects

;;; Aggregates allow for a group of graphical-objects to be associated
;;; together to form a new, more complex object.
;;; 
;;; An implementation detail:
;;; The children of a gob are stored in a list from bottom most to top
;;; most, since we want to redraw fastest and redraws occur from bottom to
;;; top.

;;; Methods on aggregates:

;;; Initialize
;;; 
;;; The :aggregate-p slot is used by the update algorithm for efficiency
(define-method :initialize opal:aggregate (a-aggregate)
  (call-prototype-method a-aggregate)
  (let ((components (g-local-value a-aggregate :components)))
    (let ((kr::*constants-disabled* T))
      (s-value a-aggregate :components NIL))
    (dolist (child components)
      (unless (g-local-value child :parent)
	(add-component a-aggregate child :where :front)))
    (setf (update-info-aggregate-p
	   (the UPDATE-INFO (g-local-value a-aggregate :update-info)))
	  T)))

;;; Destroy method
;;; 
;;; If top-level-p is true, then you must erase the aggregate (carefully!).
;;; Otherwise, we can presume that the calling party has already gone
;;; through the trouble of erasing the aggregate.
;;; If, while trying to erase the aggregate, we hit illegal values, then the
;;; window will be updated fully (after the destroy).

(define-method :destroy-me opal:aggregate (a-aggregate &optional (top-level-p T))
  (if a-aggregate
      (let* ((the-window (g-local-value a-aggregate :window))
             (erase-p (and top-level-p the-window))
             (parent  (g-local-value a-aggregate :parent))
             total-update-p)
        (if erase-p;; If at top-level, then erase...
            (if (null parent)
                (if (eq a-aggregate (g-value the-window :aggregate))
                    (s-value the-window :aggregate NIL)
                    (progn
                      (format t "~%Warning in Destroy: aggregate '~A' has no parent,~%" a-aggregate)
                      (format t   "        is in window '~A', but is not that window's:aggregate.~%"
                              the-window)
                      (setq erase-p NIL)))
                (setq total-update-p (not (carefully-erase a-aggregate the-window)))))
        (when (and top-level-p parent)
          (s-value parent :components
		   (delete a-aggregate (g-local-value parent :components)))
          (mark-as-changed parent :components))
        (let ((components (g-local-value a-aggregate :components)))
          (with-constants-disabled (destroy-slot a-aggregate :components))
          (dolist (component components)
            (when (schema-p component)
              (destroy component NIL))))
        (destroy-schema a-aggregate)
        (if erase-p
            (update the-window total-update-p)))))



;;; Install-Component is called from Add-Component and Move-Component.  It
;;; does all the weird placement stuff for getting the gob into the right
;;; location of the :components list of the aggregate.  It does nothing else.
(defun install-component (a-aggregate gob args)
  (let (locator where)
    (cond ((eq (first args) :where)
                (setq where (second args))
                (setq locator (third args)))
          ((first args)
                (setq where (first args))
                (setq locator (second args)))
          (t (setq where :front)))
    (case where
      ((:front :tail)
       (s-value a-aggregate :components
		(nconc (g-local-value a-aggregate :components)
		       (list gob))))
      ((:behind :before)
       (let ((components (g-local-value a-aggregate :components)))
	 (do ((smash-slot components (cdr smash-slot))
	      (pre-splice nil smash-slot))
	     ((or (eq (car smash-slot) locator)
		  (null smash-slot))
	      (cond ((null smash-slot)
		     (error "Locator for :where :behind ~S is not in aggregate ~S."
			    locator a-aggregate))
		    (pre-splice
		     (setf (cdr pre-splice) (cons gob smash-slot))
		     (s-value a-aggregate :components components))
		    (t (s-value a-aggregate
				:components (cons gob smash-slot))))))))
      ((:in-front :after)
       (let* ((components (g-local-value a-aggregate :components))
	      (remainder (member locator components))
	      (splice (cons gob (cdr remainder))))
	 (cond (remainder
		(setf (cdr remainder) splice)
		(s-value a-aggregate :components components))
	       (t (error "Locator for :where :in-front ~S is not in aggregate ~S"
			 locator a-aggregate)))))
      (:at
       (let* ((components (g-local-value a-aggregate :components))
	      (remainder (unless (zerop locator)
			   (nthcdr (1- locator) components))))
	 (s-value a-aggregate :components
		  (if (zerop locator)
		      (cons gob components)
		      (progn
			(setf (cdr remainder) (cons gob (cdr remainder)))
			components)))))
      ((:back :head)
       (s-value a-aggregate :components
		(cons gob (g-local-value a-aggregate :components))))
      (otherwise
       (s-value a-aggregate :components
		(nconc (g-local-value a-aggregate :components)
		       (list gob)))
       (warn (format nil "Bad where option in add-component: ~S." where))))))

;;; Add-Component adds gob to aggregate covering according to the arguments of
;;; the where keyword argument.  Note that the :where keyword is now OPTIONAL.
;;; 
;;; Options for the :where keyword argument:
;;;    :front, :back, :behind, :in-front, or :at
;;;    :tail,  :head, :before, :after
;;; Interpretation of arg:
;;;  - if (member '(:behind :in-front :before :after) where) 
;;;    gob is positioned relative to (third arg)
;;;  - if (eq where :at) gob is positioned at (third arg) positions from
;;; the front of the children
;;;  - otherwise it is ignored.
;;; 
;;; This is really a lot less complicated than it seems, you can say things
;;; like:
;;; (add-component foo :where :front)       == (add-component foo :front)
;;; (add-component foo :where :behind bar)  == (add-component foo :behind :bar)
;;; (add-component foo :where :at 2)        == (add-component foo :at 2)

(define-method :add-component opal:aggregate (a-aggregate gob &rest args)
  (if (eq gob (g-local-value gob :window))		;; Is this a window?
      (error "*** ~A is a WINDOW, and was not added to ~A~%"
	     gob a-aggregate))
  (let ((parent (g-local-value gob :parent))
	(internally-parented (g-local-value gob :internally-parented)))
    (if (and parent (not internally-parented))
	;; The object already has a parent which wasn't assigned by
	;; aggregadgets
	(error "Graphical-object ~S has :parent ~S already." gob parent))
    (install-component a-aggregate gob args)
    ;; Set up the reverse pointer from child to aggregate
    (if internally-parented
	(destroy-slot gob :internally-parented)
	(s-value gob :parent a-aggregate)))

  ;; Propagate window and dirty bit to children
  (let ((a-window (g-local-value a-aggregate :window)))
    (when a-window
      (let ((gob-update-info (the UPDATE-INFO (g-local-value gob :update-info))))
	(set-display-slots gob a-window t)
	
	;; Place the object on its window's invalid-objects list
	(make-object-invalid gob gob-update-info a-window)
   
	;; Invalidate all of the aggregate's children (recursively)
	(if (update-info-aggregate-p gob-update-info)
	    (do-all-components gob
	      #'(lambda (c)
		  (let ((c-update-info (g-value c :update-info)))
		    (make-object-invalid c c-update-info a-window)))))

	;; Indicate that the old-bbox is not valid now since gob was
	;; not visible in this window previously...
	(setf (bbox-valid-p (update-info-old-bbox gob-update-info)) NIL))))

  ;; Signal we have changed components list
  (mark-as-changed a-aggregate :components)

  ;; Return gob
  gob)

;;; Add multiple components at the same time
;;; 
(defun add-components (agg &rest components)
  (dolist (component components)
    (add-component agg component))
  (car (last components)))

;;; Remove-component deletes the topmost occurance of gob in aggregate
;;; 
(define-method :remove-component opal:aggregate (a-aggregate gob)

 (if (not (eq (g-local-value gob :parent) a-aggregate))
  (warn (format nil "Cannot remove-component ~A from ~A" gob a-aggregate))
  ;; add the gob's bounding box to the clipping region of the topmost
  ;; overlapping aggregate that contains it (or to its parent if no
  ;; such aggregate is found), and clear the gob's drawable, display,
  ;; and dirty slots, and recursively clear the same slots in its children
  ;; as well

  (let* ((gob-update-info (the UPDATE-INFO (g-local-value gob :update-info)))
         (a-window (update-info-window gob-update-info))
	 (a-window-update-info 
	    (when a-window (g-local-value a-window :update-info))))
    (when a-window-update-info
      (let* ((win-update-info (g-value a-window :win-update-info))
	     (window-bbox (update-info-old-bbox a-window-update-info))
	     (bbox (update-info-old-bbox gob-update-info)))

	; since the object is no longer in the window, it should be
	; removed from the window's invalid objects list.
	; (this is defined in update-basics.lisp).
	(if (update-info-aggregate-p gob-update-info)
	    (do-all-components gob #'(lambda (c)
				       (remove-from-invalid-objects-list
					c win-update-info))
			       :self T)
	    (remove-from-invalid-objects-list gob win-update-info))

	(merge-bbox window-bbox bbox)
        (set-display-slots gob nil nil)))

    (setf (update-info-invalid-p gob-update-info) NIL)
    (s-value a-aggregate :components
	     (delete gob (g-local-value a-aggregate :components)
		     :from-end t :count 1))
    (s-value gob :parent NIL)

    ;; signal we have changed components list
    (mark-as-changed a-aggregate :components))))

;;; Similar to a call to remove-component followed by a call to
;;; add-component, but faster because it does not call set-display-slots
;;; or perform any of the other overhead.  It merely sets the windows old-bbox,
;;; manually removes the object from the :components list, then reinstalls it

(define-method :move-component opal:aggregate (a-aggregate gob &rest args)
  (let* ((gob-update-info (g-local-value gob :update-info))
         (a-window (update-info-window gob-update-info))
	 (a-window-update-info (g-local-value a-window :update-info)))
    (and a-window a-window-update-info
      (let ((window-bbox (update-info-old-bbox a-window-update-info))
            (bbox (update-info-old-bbox gob-update-info)))
	(merge-bbox window-bbox bbox))))
  (s-value a-aggregate :components
	   (delete gob (g-local-value a-aggregate :components)))
  (install-component a-aggregate gob args))
  
;;; Remove multiple components at the same time
(defun remove-components (agg &rest components)
  (dolist (component components)
    (remove-component agg component)))

;;; Remove all components
(defun remove-all-components (agg)
  (dolist (component (copy-list (g-local-value agg :components)))
    (remove-component agg component)))

;;; Like kr:is-a-p, but types can be a list.
(defun my-is-a-p (child types)
  (when types
    (if (listp types)
	(or (is-a-p child (car types))
	    (my-is-a-p child (cdr types)))
	(is-a-p child types))))

;;; Do-Components applies function to all children of aggregate

(define-method :do-components opal:aggregate (a-aggregate a-function 
					     &key (type t) (self nil))
  (let ((children (g-local-value a-aggregate :components)))
    (dolist (child children)
      (when (or (eq type t)
		(is-a-p child type))
        (funcall a-function child)))
    (when (and self
	       (or (eq type t)
		   (is-a-p a-aggregate type)))
      (funcall a-function a-aggregate))))


;;; Do-All-Components is like do-components, except it continued through all
;;; aggregates to their children

(define-method :do-all-components opal:aggregate (a-aggregate a-function
						 &key (type t) (self nil))
  (let ((children (g-local-value a-aggregate :components)))
    (dolist (child children)
      (if (is-a-p child opal:aggregate)
	  (do-all-components child a-function :type type :self t)
	  (when (or (eq type t)
		    (is-a-p child type))
	    (funcall a-function child))))
    (when (and self
	       (or (eq type t)
		   (is-a-p a-aggregate type)))
      (funcall a-function a-aggregate))))


;;; Point-To-Component queries the aggregate for first generation children
;;; at point (x,y). If :type is specified only children of the specified
;;; type will be returned.

(defun point-to-component-recur (component-list x y type)
  (and component-list
       (or (point-to-component-recur (cdr component-list) x y type)
	   (let ((child (car component-list)))
	     (when (and (or (eq type t)
			    (my-is-a-p child type))
			(point-in-gob child x y))
	       child)))))
  
(define-method :point-to-component opal:aggregate
	       (a-aggregate x y &key (type t))
  (when (point-in-gob a-aggregate x y)
    (point-to-component-recur (g-local-value a-aggregate :components) x y type)))


;;; Point-To-Leaf is similar to Point-To-Component except that the query
;;; continues to the deepest children.

(defun point-to-leaf-recur (component-list x y type)
  (and component-list
       (or (point-to-leaf-recur (cdr component-list) x y type)
	   (let ((child (car component-list)))
	     (cond ((and (is-a-p child opal:aggregate)
			 (not (g-value child :pretend-to-be-leaf)))
		    (point-to-leaf child x y :type type))
		   ((or (eq type t)
			(my-is-a-p child type))
		    (when (point-in-gob child x y) child)))))))

(define-method :point-to-leaf opal:aggregate
	       (a-aggregate x y &key (type t))
  (when (point-in-gob a-aggregate x y)
    (or (and (not (eq type t))
	     (my-is-a-p a-aggregate type)
             a-aggregate)
	(point-to-leaf-recur (g-local-value a-aggregate :components)
			     x y type))))



;; Returns T if two rectangles intersect
(defun rectintersect (left1 top1 right1 bottom1
		      left2 top2 right2 bottom2)
  (and left1 top1 right1 bottom1
      (<= left1 right2)
       (<= left2 right1)
       (<= top1 bottom2)
       (<= top2 bottom1)))

;; Returns T if rectangle1 is inside rectangle2
(defun rectsubset (left1 top1 right1 bottom1
		   left2 top2 right2 bottom2)
  (and left1 top1 right1 bottom1
       (<= left2 left1)
       (<= right1 right2)
       (<= top2 top1)
       (<= bottom1 bottom2)))

;;; Returns a list of leafs of obj that intersect the rectangle
;;; bounded by top, left, width, height  (unless :intersect in nil,
;;; in which case the leafs must be completely inside the rectangle).
;;;
(defun leaf-objects-in-rectangle (obj top left bottom right
				 &key (type t) (intersect t))
  (if (g-value obj :visible)
      (let* ((obj-left (g-value obj :left))
	     (obj-top (g-value obj :top))
	     (obj-right (+ obj-left -1 (g-value obj :width)))
	     (obj-bottom (+ obj-top -1 (g-value obj :height))))
	(cond ((and (is-a-p obj aggregate)
		    (not (g-value obj :pretend-to-be-leaf)))
	       (when (rectintersect obj-left obj-top obj-right obj-bottom
				    left top right bottom)
		 (let (leafs)
		   (dolist (c (g-value obj :components))
		     (setq leafs (nconc leafs
					(leaf-objects-in-rectangle
					 c top left bottom right
					 :type type
					 :intersect intersect))))
		   leafs)))
	      ((or (eq type t) (my-is-a-p obj type))
	       (when (funcall (if intersect #'rectintersect #'rectsubset)
			      obj-left obj-top obj-right obj-bottom
			      left top right bottom)
		 (list obj)))))))


;;; Returns T or NIL depending if "obj" intersects rectangle
;;; bounded by "top", "left", "width", "height".
;;; (If intersect = NIL, then "obj" must be completely inside the rectangle.
;;; (If type <> T, then "obj" must be of type "type".)
(defun obj-in-rectangle (obj top left bottom right &key (type t) (intersect t))
  (if (g-value obj :visible)
      (let* ((obj-left (g-value obj :left))
	     (obj-top (g-value obj :top))
	     (obj-right (+ obj-left -1 (g-value obj :width)))
	     (obj-bottom (+ obj-top -1 (g-value obj :height))))
	(and (or (eq type t) (my-is-a-p obj type))
	     (funcall (if intersect #'rectintersect #'rectsubset)
		      obj-left obj-top obj-right obj-bottom
		      left top right bottom)))))

  
;;; Like leaf-objects-in-rectangle, but only returns top-level components
;;; instead of leafs.
(defun components-in-rectangle (agg top left bottom right
				&key (type t) (intersect t))
  (if (g-value agg :visible)
      (let (leafs)
	(dolist (obj (g-value agg :components))
	  (when (obj-in-rectangle obj top left bottom right
				  :type type :intersect intersect)
	    (setq leafs (nconc leafs (list obj)))))
	leafs)))



;;; This routine sets the :hit-threshold slot of the aggregate
;;; agg to be the maximum of the hit-thresholds of its components.
(defun set-aggregate-hit-threshold (agg)
  (when (is-a-p agg opal:aggregate)
    (let ((max-hit 0))
      (dovalues (c agg :components :local t)
		(set-aggregate-hit-threshold c)
		(setq max-hit (max max-hit (g-value c :hit-threshold))))
      (s-value agg :hit-threshold max-hit))))
