;;; -*- 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 -*-
;;;  This file contains the code to either MOVE or GROW objects inside
;;;  the editor drawing window

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


(export '(Move-Grow-Do-Go Move-Grow-Do-Stop))

;;; make sure that slots which are going to be changed by the move
;;; interactor do not have formulas
(defun check-move-slots (obj inter)
  (let ((attach-point (g-value inter :attach-point))
	constrained-slots)
    (if (is-a-line-p obj)
	(dolist (slot '(:x1 :y1 :x2 :y2))
		(when (formula-p (get-value obj slot))
		      (push slot constrained-slots)))
        (progn
	  (when (and (member attach-point '(:nw :ne :se :sw :center :e :w))
		     (formula-p (get-value obj :left)))
		(push :left constrained-slots))
	  (when (and (member attach-point '(:nw :ne :se :sw :center :s :n))
		     (formula-p (get-value obj :top)))
		(push :top constrained-slots))))
    (reverse constrained-slots)))

(define-method :lapidary-check-grow-slots opal:view-object (obj inter)
  (let ((attach-point (g-value inter :attach-point))
	constrained-slots)
    (when (member attach-point '(:nw :ne :se :sw :e :w))
	  (when (formula-p (get-value obj :left))
		(push :left constrained-slots))
	  (when (formula-p (get-value obj :width))
		(push :width constrained-slots)))
    (when (member attach-point '(:nw :ne :se :sw :n :s))
	  (when (formula-p (get-value obj :top))
		(push :top constrained-slots))
	  (when (formula-p (get-value obj :height))
		(push :height constrained-slots)))
    (reverse constrained-slots)))

#|
(define-method :lapidary-check-grow-slots opal:circle (obj inter)
  (let ((attach-point (g-value inter :attach-point))
	constrained-slots)
    (when (member attach-point '(:nw :ne :se :sw :e :w))
	  (when (formula-p (get-value obj :left))
		(push :left constrained-slots))
	  (when (formula-p (get-value obj :suggested-width))
		(push :width constrained-slots)))
    (when (member attach-point '(:nw :ne :se :sw :n :s))
	  (when (formula-p (get-value obj :top))
		(push :top constrained-slots))
	  (when (formula-p (get-value obj :suggested-height))
		(push :height constrained-slots)))
    (reverse constrained-slots)))
|#

(defun check-line-grow-slots (obj inter)
  (let ((attach-point (g-value inter :attach-point))
	constrained-slots)
    (if (eq attach-point 2)
	(progn
	  (when (formula-p (get-value obj :x2))
		(push :x2 constrained-slots))
	  (when (formula-p (get-value obj :y2))
		(push :y2 constrained-slots)))
        (progn
	  (when (formula-p (get-value obj :x1))
		(push :x1 constrained-slots))
	  (when (formula-p (get-value obj :y1))
		(push :y1 constrained-slots))))
    (reverse constrained-slots)))

(define-method :lapidary-check-grow-slots opal:line (obj inter)
  (check-line-grow-slots obj inter))

(define-method :lapidary-check-grow-slots 
  garnet-gadgets:arrow-line (obj inter)
  (check-line-grow-slots obj inter))

(define-method :lapidary-check-grow-slots 
  garnet-gadgets:double-arrow-line (obj inter)
  (check-line-grow-slots obj inter))

(define-method :lapidary-check-grow-slots opal:aggrelist (obj inter)
  (let ((attach-point (g-value inter :attach-point))
	constrained-slots)
    (when (and (member attach-point '(:nw :ne :se :sw :e :w))
	       (formula-p (get-value obj :left)))
	  (push :left constrained-slots))
    (when (and (member attach-point '(:nw :ne :se :sw :n :s))
	       (formula-p (get-value obj :top)))
	  (push :top constrained-slots))
    (reverse constrained-slots)))

;;; ==========================================================
;;; grow-obj inserts the appropriate formulas into the
;;; selected object so that it is resized. 
;;; ==========================================================
        
;;; standard grow function for non-line objects

(define-method :lapidary-grow opal:view-object (inter obj box)
  (let ((attach-point (g-value inter :attach-point)))
    ;; change width if any of the grow boxes on the corners or left
    ;; or right sides are chosen
    (when (member attach-point '(:nw :ne :se :sw :e :w))
	  (gg::cg-destroy-constraint obj :left)
	  (gg::cg-destroy-constraint obj :width)
	  (s-value obj :left (first box))
	  (s-value obj :width (third box)))
    ;; change height if any of the grow boxes on the corners or top
    ;; or bottom sides are chosen
    (when (member attach-point '(:nw :ne :se :sw :n :s))
	   (gg::cg-destroy-constraint obj :top)
	   (gg::cg-destroy-constraint obj :height)
	   (s-value obj :top (second box))
	   (s-value obj :height (fourth box)))))

(defun grow-line (inter obj points)
  (let ((attach-point (g-value inter :attach-point)))
    (case attach-point
	  ((1 :center)
	   (gg::cg-destroy-constraint obj :x1)
	   (gg::cg-destroy-constraint obj :y1)
	   (s-value obj :x1 (first points))
	   (s-value obj :y1 (second points)))
	  (2
	   (gg::cg-destroy-constraint obj :x2)
	   (gg::cg-destroy-constraint obj :y2)
	   (s-value obj :x2 (third points))
	   (s-value obj :y2 (fourth points))))))

(define-method :lapidary-grow opal:line (inter obj points)
  (grow-line inter obj points))

(define-method :lapidary-grow 
  garnet-gadgets:arrow-line (inter obj points)
  (grow-line inter obj points))

(define-method :lapidary-grow 
  garnet-gadgets:double-arrow-line (inter obj points)
  (grow-line inter obj points))

;;; aggrelists have to be handled as a special case. When their
;;; width or height is changed, new items must be added to the
;;; aggrelist and the constraints on the width and height are
;;; not changed (we presume that they are the standard formulas
;;; for computing an aggregate's width and height, and if they're
;;; not, we presume the designer has used formulas that will correctly
;;; compute the width and height, even with the changed number of items)

(define-method :lapidary-grow opal:aggrelist (inter obj box)
(let ((attach-point (g-value inter :attach-point)))
    ;; change left if any of the grow boxes on the corners or left
    ;; or right sides are chosen
    (when (member attach-point '(:nw :ne :se :sw :e :w))
	  (gg::cg-destroy-constraint obj :left)
	  (s-value obj :left (first box)))
    ;; change top if any of the grow boxes on the corners or top
    ;; or bottom sides are chosen
    (when (member attach-point '(:nw :ne :se :sw :n :s))
	   (gg::cg-destroy-constraint obj :top)
	   (s-value obj :height (fourth box)))

    ;; change the number of items in a list
    (s-value obj :items (g-value inter :feedback-obj :items))
    (s-value obj :rank-margin (g-value inter :feedback-obj :rank-margin))
    (opal:notice-items-changed obj)
    (s-value (g-value inter :feedback-obj) :visible nil)))

#|
;;; circles must be handled as a special case since their width and
;;; height must always be the same. if a corner is moved, select the
;;; minimum of the changed height and width.

(define-method :lapidary-grow opal:circle (inter obj box)
  (let* ((attach-point (g-value inter :attach-point))
	 (diameter (case attach-point
			 ((:nw :sw :ne :se) (min (third box) (fourth box)))
			 ((:e :w) (third box))
			 ((:n :s) (fourth box)))))
    ;; it's ok to try to destroy a constraint twice
    (when (member attach-point '(:nw :sw :ne :se :e :w))
	  (gg::cg-destroy-constraint obj :suggested-width)
	  (s-value obj :suggested-width (third box)))

    (when (member attach-point '(:nw :sw :ne :se :n :s))
	  (gg::cg-destroy-constraint obj :suggested-height)
	  (s-value obj :suggested-height (fourth box)))
    
    ;; set the :diameter slot
    (s-value obj :diameter diameter)))
|#
;;; ==========================================================
;;; move-obj inserts the appropriate formulas into the
;;; selected object so that it is moved, and updates
;;; the constraint menu info to reflect this new state of
;;; affairs
;;; ==========================================================

(defun move-obj (obj attach-point)
  (if (is-a-line-p obj)
      ;; move a line
      (let ((points (g-value obj :points)))
	(gg::cg-destroy-constraint obj :x1)
	(gg::cg-destroy-constraint obj :y1)
	(gg::cg-destroy-constraint obj :x2)
	(gg::cg-destroy-constraint obj :y2)
	(s-value obj :x1 (first points))
	(s-value obj :y1 (second points))
	(s-value obj :x2 (third points))
	(s-value obj :y2 (fourth points)))
      ;; move a non-line
      (let ((box (g-value obj :box)))
	(when (member attach-point '(:nw :ne :se :sw :center :e :w))
	      (gg::cg-destroy-constraint obj :left)
	      (s-value obj :left (first box)))
	(when (member attach-point '(:nw :ne :se :sw :center :s :n))
	      (gg::cg-destroy-constraint obj :top)
	      (s-value obj :top (second box))))))


(defun Move-Grow-Do-Go ()
  (create-instance 'move-inter multi-win-interactor
   (:window (o-formula (gv *selection-info* :window)))
   (:active (o-formula (gv editor-menu :build-p)))
   (:continuous T)
   (:waiting-priority inter:high-priority-level)
   (:start-where (o-formula (list :list-leaf-element-of
				  *selection-info* :feedback)))
   (:outside NIL) ; goes back to original position if go outside
   (:running-where t)
   (:feedback-obj 
    (o-formula (if (gvl :line-p)
		   (gvl :current-window :move-grow-line-feedback)
		   (gvl :current-window :move-box-feedback))))
;; the next field causes the object to be changed to be the object that the
;; feedback boxes are defined over, rather than the feedback object itself.
   (:obj-to-change (o-formula (gvl :first-obj-over :parent :obj-over)))
   (:line-p (o-formula (is-a-line-p (gvl :obj-to-change))))
   (:attach-point (o-formula (cond ((is-a-p (gvl :first-obj-over :parent) 
					    undersized-feedback-obj)
				    (if (gvl :line-p) :center :nw))
				   (t
				    (gvl :first-obj-over :where-attach)))))
   (:grow-p nil)
   (:start-event *move-button*)
   (:start-action
    #'(lambda (interactor objbeingchanged newsize)
	(let (constrained-slots)
	(cond ((not (eq (g-value objbeingchanged :parent)
			(g-value objbeingchanged :window :editor-agg)))
	       (lapidary-error 
		"This object is part of an aggregate and thus cannot be moved")
	       (inter:abort-interactor interactor))
	      ((setf constrained-slots
		     (check-move-slots objbeingchanged interactor))
	       (lapidary-error 
		(format nil "To move this object you must first unconstrain
the ~S slots" constrained-slots))
	       (inter:abort-interactor interactor))
	      (t
	       (kr::call-prototype-method interactor objbeingchanged newsize)
	       (s-value (g-value interactor :feedback-obj) :where-attach
			(g-value interactor :attach-point))
	       (s-value (g-value interactor :first-obj-over) :visible NIL))))))
   (:abort-action
    #'(lambda (interactor objbeingchanged)
	(let ((first-obj-over (g-value interactor :first-obj-over)))
	  (when first-obj-over (s-value first-obj-over :visible T)))
	(when objbeingchanged
	  (kr::call-prototype-method interactor objbeingchanged))))
   (:stop-action
    #'(lambda (interactor objbeingchanged newsize)
	(let ((obj-to-change (g-value interactor :obj-to-change)))
	(s-value (g-value interactor :first-obj-over) :visible T)
	(kr::call-prototype-method interactor objbeingchanged newsize)
	;; save slot values for undo
	(reset-undo)
	(undo-save objbeingchanged :left)
	(undo-save objbeingchanged :top)
	;; if the object belongs in another window, move it to its new window
	(when (not (eq (g-value interactor :current-window)
		       (g-value obj-to-change :window)))
	  (let ((new-window (g-value interactor :current-window))
		(old-parent (g-value obj-to-change :parent))
		(p-feedback (g-value obj-to-change :p-feedback-obj))
		(s-feedback (g-value obj-to-change :s-feedback-obj)))
	      (opal:remove-component old-parent obj-to-change)
	      (opal:add-component (g-value new-window :editor-agg)
				  obj-to-change)
	      (when p-feedback
		    (opal:remove-component (g-value p-feedback :parent)
					   p-feedback)
		    (opal:add-component (g-value new-window :feedback-agg)
					p-feedback))
	      (when s-feedback
		    (opal:remove-component (g-value s-feedback :parent)
					   s-feedback)
		    (opal:add-component (g-value new-window :feedback-agg)
					s-feedback))))
	;; move the object
	(move-obj obj-to-change (g-value interactor :attach-point))
	))))

  (create-instance 
   'grow-inter inter:Move-Grow-Interactor
   (:window (o-formula (gv *selection-info* :window)))
   (:active (formula `(gv ',editor-menu :build-p)))
   (:continuous T)
   (:waiting-priority inter:high-priority-level)
   (:start-where 
    (o-formula (list :list-leaf-element-of
		     *selection-info* :feedback)))
   (:outside NIL) ; goes back to original position if go outside
   (:running-where t)
   (:feedback-obj 
    (o-formula (cond ((gvl :line-p)
		      (gvl :current-window :move-grow-line-feedback))
		     ((is-a-p (gvl :obj-to-change) opal:aggrelist)
		      (gvl :current-window :aggrelist-feedback))
		     (t (gvl :current-window :grow-box-feedback)))))
;; the next field causes the object to be changed to be the object that the
;; feedback boxes are defined over, rather than the feedback object itself. 
   (:obj-to-change (o-formula (gvl :first-obj-over :parent :obj-over)))
   (:attach-point (o-formula (cond ((is-a-p (gvl :first-obj-over :parent) 
					    undersized-feedback-obj)
				    (if (gvl :line-p) 1 :nw))
				   (t
				    (if (eq (gvl :first-obj-over :where-attach)
					    :center)
					1
				        (gvl :first-obj-over :where-attach))))))
    (:grow-p t)
    (:line-p (o-formula (is-a-line-p (gvl :obj-to-change))))
    (:min-width (o-formula (if (is-a-p (gvl :feedback-obj) opal:aggrelist)
			       (gvl :feedback-obj :item-prototype :width)
			       0)))
    (:min-height (o-formula (if (is-a-p (gvl :feedback-obj) opal:aggrelist)
			       (gvl :feedback-obj :item-prototype :height)
			       0)))
    (:start-event *grow-button*)
    (:start-action
     #'(lambda (interactor objbeingchanged newsize)
	 (let (constrained-slots)
	 (cond ((not (eq (g-value objbeingchanged :parent)
			 (g-value objbeingchanged :window :editor-agg)))
		(lapidary-error 
		"This object is part of an aggregate and thus cannot be grown")
		(inter:abort-interactor interactor))
	       ((is-a-p objbeingchanged opal:text)
		(lapidary-error
		 "You cannot change the size of a text object")
		(inter:abort-interactor interactor))
	      ((setf constrained-slots
		     (kr-send objbeingchanged :lapidary-check-grow-slots
			      objbeingchanged interactor))
	       (lapidary-error 
		(format nil "To resize this object you must first unconstrain
the ~S slots" constrained-slots))
	       (inter:abort-interactor interactor))
	       (t
		(if (is-a-p (g-value interactor :feedback-obj) opal:aggrelist)
		    (initialize-list-feedback interactor objbeingchanged
					      newsize)
		    (kr::call-prototype-method interactor objbeingchanged 
					       newsize))
		(s-value (g-value interactor :feedback-obj) :where-attach
			 (g-value interactor :attach-point))
		(s-value (g-value interactor :first-obj-over) :visible NIL))))))
    (:running-action
     #'(lambda (interactor objbeingchanged newsize)
	 (if (is-a-p (g-value interactor :feedback-obj) opal:aggrelist)
	     (calculate-howmany interactor objbeingchanged newsize)
	     (kr::call-prototype-method interactor objbeingchanged newsize))))
    (:abort-action
     #'(lambda (interactor objbeingchanged)
	 (let ((first-obj-over (g-value interactor :first-obj-over)))
	   (when first-obj-over (s-value first-obj-over :visible T)))
	 (when objbeingchanged
	   (kr::call-prototype-method interactor objbeingchanged)
	   (when (is-a-p objbeingchanged opal:aggrelist)
		 (s-value (g-value interactor :feedback-obj) :visible nil)))))
    (:stop-action
     #'(lambda (interactor objbeingchanged newsize)
	(let ((obj-to-change (g-value interactor :obj-to-change)))
	  (s-value (g-value interactor :first-obj-over) :visible T)
	  (kr::call-prototype-method interactor objbeingchanged newsize)
	  ;; save slot values for undo
	  (reset-undo)
	  (undo-save objbeingchanged :left)
	  (undo-save objbeingchanged :top)
	  (undo-save objbeingchanged :width)
	  (undo-save objbeingchanged :height)
	  ;; grow the object
	  (kr-send obj-to-change :lapidary-grow interactor obj-to-change 
		   newsize)
	 )))
   ))


(defun Move-Grow-Do-Stop ()
  (when (boundp 'grow-inter) (opal:destroy grow-inter))
  (when (boundp 'move-inter) (opal:destroy move-inter))
  )

