;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: INTERACTORS; 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. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; This file contains the mouse and keyboard interactors to rotate objects
;;; and get angular measurements.
;;;
;;; It should be loaded after Interactor.lisp and after movegrowinter.lisp
;;;
;;; Designed and implemented by Brad A. Myers

#|
============================================================
Change log:
        4/1/92  Andrew Mickish - kr::*debug-switch* ---> #-garnet-debug
        10/11/90 Brad Myers - added explicit Stop-Interactor code
	10/5/89 Brad Myers - Change :obj-to-rotate to :obj-to-change;
			Don't set :visible of feedback object (use :obj-over)
			Fixed some small errors,
			Add Final-Function
	10/4/89 Roger Dannenberg - Change debugging output
        9/22/89 Brad Myers - Made more robust when :start-where = T
        8/14/89 Brad Myers - Fixed for multiple priority levels
        6/26/89 Brad Myers - Fixed to have quote for create-schema
        4/25/89 Ed Pervin  - updated to X11
	11/1/88 Brad Myers - changed to return angle delta
	9/26/88 Brad Myers - started 
============================================================
|#

(in-package "INTERACTORS" :use '("KR" "LISP") :nicknames '("INTER"))

;;;============================================================
;;;============================================================
;;;============================================================


;;;============================================================
;;; Angle-Interactor
;;;============================================================

;;;============================================================
;;; Helper procedures for the default procedures to go into the slots
;;;============================================================

(defun obj-or-feedback-rotate (feedback object-being-rotated angle inter)
  #-garnet-debug (declare (ignore inter))
    (if feedback
	(progn
	  (dbprint-feed :angle feedback angle inter)
	  (s-value feedback :angle angle))
	(when (schema-p object-being-rotated)
	  (dbprint :angle object-being-rotated angle inter)
	   (s-value object-being-rotated :angle angle))))

;; makes the feedback for interactor be visible if vis = T or invisible if
;; vis = NIL
(defun angle-feedback-visible (an-interactor feedback object-being-rotated vis)
  #-garnet-debug (declare (ignore an-interactor))
  (when feedback
    (dbprint-feed :obj-over feedback (if vis object-being-rotated NIL)
		  an-interactor)
    (s-value feedback :obj-over (if vis object-being-rotated NIL))))
  

;;;============================================================
;;; Default Procedures to go into the slots
;;;============================================================
(proclaim '(special Angle-Interactor))

(defun Angle-Interactor-Initialize (new-Angle-schema)
  (if-debug new-Angle-schema (format T "Angle initialize ~s~%" new-Angle-schema))
  (Check-Interactor-Type new-Angle-schema inter:Angle-Interactor)
  (Check-Required-Slots new-Angle-schema)
  (Set-Up-Defaults new-Angle-schema)
  ) ;end initialize procedure

(defun Angle-Int-Start-Action (an-interactor object-being-rotated first-angle)
  (if-debug an-interactor (format T "Angle int-start moving ~s firstangle=~s~%"
				  object-being-rotated first-angle))
  (s-value an-interactor :saved-original-angle
	   ;; ** BUG ** This won't work if angle clipped, or whatever.
	   ;; select-move gets around this problem by reading top,left
	   ;; rather than box, but that doesn't work here.
	   (g-value object-being-rotated :angle))
  ;;move feedback or object first so no flicker when turned visible
  (let ((feedback (g-value an-interactor :feedback-obj)))
    (obj-or-feedback-rotate feedback object-being-rotated first-angle an-interactor)
    (when feedback (angle-feedback-visible an-interactor feedback
					      object-being-rotated T))))

(defun Angle-Int-Running-Action (an-interactor object-being-rotated
					    new-angle angle-delta)
  (declare (ignore angle-delta))
  (if-debug an-interactor (format T "Angle int-running, obj = ~s, angle=~s~%"
				  object-being-rotated new-angle))
  (obj-or-feedback-rotate (g-value an-interactor :feedback-obj)
			object-being-rotated new-angle an-interactor))

(defun Angle-Int-Outside-Action (an-interactor outside-control
						  object-being-rotated) 
  (if-debug an-interactor (format T "Angle int-outside, obj = ~s~%"
				  object-being-rotated))
  (unless (eq :last outside-control)
    (let ((feedback (g-value an-interactor :feedback-obj))
	  val)
      (if feedback (angle-feedback-visible an-interactor feedback
					      object-being-rotated NIL)
	  (when (schema-p object-being-rotated)
	    (setq val (g-value an-interactor :saved-original-angle))
	    (dbprint :angle object-being-rotated val an-interactor)
	    (s-value object-being-rotated :angle val))))))

(defun Angle-Int-Back-Inside-Action (an-interactor outside-control
					       object-being-rotated
					       new-angle) 
  (if-debug an-interactor 
	    (format T "Angle int-back-inside, obj = ~s new angle=~s~%" 
		    object-being-rotated new-angle))
  ;;first move the feedback or object to the new position, and then make it
  ;; visible, if necessary
  (let ((feedback (g-value an-interactor :feedback-obj)))
    (obj-or-feedback-rotate feedback object-being-rotated
			  new-angle an-interactor)
    (when (and feedback
	       (null outside-control))
      (angle-feedback-visible an-interactor feedback object-being-rotated T))))

(defun Angle-Int-Stop-Action (an-interactor object-being-rotated final-angle
					 angle-delta)
  #-garnet-debug (declare (ignore angle-delta))
  (if-debug an-interactor (format T "Angle int-stop obj ~s final-pos=~s delta=~s~%"
				  object-being-rotated final-angle angle-delta))
  ;;turn off feedback
  (angle-feedback-visible an-interactor (g-value an-interactor :feedback-obj)
			     object-being-rotated NIL)
  ;;set object to final position
  (when (schema-p object-being-rotated)
    (dbprint :angle object-being-rotated final-angle an-interactor)
    (s-value object-being-rotated :angle final-angle))
  (KR-Send an-interactor :final-function an-interactor final-angle))

(defun Angle-Int-Abort-Action (an-interactor object-being-rotated)
  (if-debug an-interactor (format T "Angle int-abort moving ~s~%"
				  object-being-rotated))
  (let ((feedback (g-value an-interactor :feedback-obj))
	val)
    (if feedback (angle-feedback-visible an-interactor feedback
					    object-being-rotated NIL)
	(when (schema-p object-being-rotated)
	    (setq val (g-value an-interactor :saved-original-angle))
	    (dbprint :angle object-being-rotated val an-interactor)
	    (s-value object-being-rotated :angle val)))))
  
;;;============================================================
;;; Go procedure helpers
;;;============================================================

(defconstant 2PI (* 2 PI))

(defun Calc-Center (object)
  (list (+ (g-value object :left) (floor (g-value object :width) 2))
	(+ (g-value object :top) (floor (g-value object :height) 2))))

(defun Calc-Angle (an-interactor center x y)
  #-garnet-debug (declare (ignore an-interactor))
  (let ((angle (atan (- (second center) y) (- x (first center)))))
    (setq angle (if (< angle 0) (+ 2PI angle) angle))
    (if-debug an-interactor
	      (format T "Center=~s x=~s y=~s angle=~s~%" center x y angle))
    angle))

;;; assume only small increments, so if too large, assume have crossed the zero
;;; line (in general, don't really know which way the user has moved between points,
;;; Could keep the old delta and check if in same direction, but that is
;;; too hard.
(defun Calc-Delta (an-interactor oldangle newangle)
  #-garnet-debug (declare (ignore an-interactor))
  (let ((delta (- newangle oldangle)))
    (cond ((> delta PI)(setq delta (- delta 2PI)))
	  ((< delta (- PI))(setq delta (+ delta 2PI)))) 
    (if-debug an-interactor (format T "Old angle=~s, new=~s, delta angle= ~s~%"
				    oldangle newangle delta))
    delta))

;;;============================================================
;;; Go procedure utilities
;;;============================================================

;;; if continuous: (remove from start level, add to stop and abort
;;; 		    level, change state to running)
;;; save object over, call start procedure.
(defun Angle-do-start (an-interactor new-obj-over event)
  (if-debug an-interactor (format T "Angle starting over ~s~%" new-obj-over))
  	;; if obj-to-change supplied, then use that, otherwise use whatever was
	;; under the mouse when started
  (let ((obj (or (g-local-value an-interactor :obj-to-change) new-obj-over))
	(x (event-x event))
	(y (event-y event))
	center angle)
    (s-value an-interactor :obj-being-rotated obj)
    (setq center (or (g-value an-interactor :center-of-rotation)
		     (Calc-Center obj)))
    (s-value an-interactor :center-to-use center)
    (setq angle (calc-angle an-interactor center x y))
    (if (g-value an-interactor :continuous)  ;then will go to running state
	(progn
	  (Fix-Running-Where an-interactor new-obj-over)
	  (s-value an-interactor :saved-last-angle angle)
	  (GoToRunningState an-interactor T)
	  (kr-send an-interactor :start-action an-interactor obj angle)
	  )
	;; else call stop-action
	(progn
	  (kr-send an-interactor :stop-action an-interactor obj angle)
	  (GoToStartState an-interactor NIL)))))

(defun Angle-do-outside (an-interactor)
  (if-debug an-interactor (format T "Angle outside~%"))
  (s-value an-interactor :current-state :outside)
  (kr-send an-interactor :outside-action an-interactor
	       (g-value an-interactor :outside)
	       (g-value an-interactor :obj-being-rotated)))
;;;filtering based on :last is handled by the :outside-action procedure
;;;  (unless (eq :last (g-value an-interactor :outside))
;;;    (s-value an-interactor :remembered-last-object NIL)))

(defun Angle-do-back-inside (an-interactor obj event)
  #-garnet-debug (declare (ignore obj))
  (let ((x (event-x event))
	(y (event-y event)))
    (if-debug an-interactor (format T "Angle back-inside over ~s~%" obj))
    (s-value an-interactor :current-state :running)
    (let* ((moving-obj (g-value an-interactor :obj-being-rotated))
	   (angle (Calc-Angle an-interactor
			      (g-value an-interactor :center-to-use) x y)))
      (kr-send an-interactor :back-inside-action an-interactor
		   (g-value an-interactor :outside) moving-obj angle)
      (s-value an-interactor :saved-last-angle angle)
      )))

(defun Angle-do-running (an-interactor obj event)
  #-garnet-debug (declare (ignore obj))
  (let ((x (event-x event))
	(y (event-y event)))
    (if-debug an-interactor (format T "Angle running over ~s~%" obj))
    (let* ((moving-obj (g-value an-interactor :obj-being-rotated))
	   (angle (Calc-Angle an-interactor
			      (g-value an-interactor :center-to-use) x y)))
      (kr-send an-interactor :running-action an-interactor
		   moving-obj angle
		   (Calc-Delta an-interactor
			       (g-value an-interactor :saved-last-angle) angle))
      (s-value an-interactor :saved-last-angle angle))))

(defun Angle-do-stop-helper (an-interactor angle angle-delta)
  (GoToStartState an-interactor T)
  (kr-send an-interactor :Stop-Action an-interactor 
	       (g-value an-interactor :obj-being-rotated) angle angle-delta))

(defun Angle-Explicit-Stop (an-interactor)
  (if-debug an-interactor (format T "Angle explicit stop~%"))
  (let ((angle (g-value an-interactor :saved-last-angle)))
    (Angle-do-stop-helper an-interactor angle 0))) ; angle delta is zero

(defun Angle-do-stop (an-interactor obj event)
  #-garnet-debug (declare (ignore obj))
  (let ((x (event-x event))
	(y (event-y event)))
    (if-debug an-interactor (format T "Angle stop over ~s at~%" obj))
    (let ((angle (Calc-Angle an-interactor
			     (g-value an-interactor :center-to-use) x y)))
      (Angle-do-stop-helper an-interactor angle
			    (Calc-Delta an-interactor
					(g-value an-interactor :saved-last-angle)
					angle)))))

(defun Angle-do-abort (an-interactor become-inactive event)
  (declare (ignore become-inactive event))
  (if-debug an-interactor (format T "Angle aborting~%"))
  (GoToStartState an-interactor T)
  (kr-send an-interactor :Abort-Action an-interactor
	       (g-value an-interactor :obj-being-rotated)))

;;;check to see if need to stop or abort based on whether :outside = :last
(defun Angle-do-outside-stop (an-interactor event)
  (if-debug an-interactor (format T "Angle stop outside~%"))
  (if (eq :last (g-value an-interactor :outside))
      (Angle-do-stop-helper an-interactor
			   (g-value an-interactor :saved-last-angle) 0.0)
      (Angle-do-abort an-interactor NIL event)))

;;;============================================================
;;; Angle schema
;;;============================================================

(Create-Schema 'inter:Angle-Interactor
		     (:is-a inter:Interactor)
		     (:name :First-Angle-interactor)
		     (:start-action 'Angle-Int-Start-Action)
		     (:running-action 'Angle-Int-Running-Action)
		     (:stop-action 'Angle-Int-Stop-Action)
		     (:abort-action 'Angle-Int-Abort-Action)
		     (:outside-action 'Angle-Int-Outside-Action)
		     (:back-inside-action 'Angle-Int-Back-Inside-Action)
		     (:obj-to-change NIL)  ;supplied by application program
		     (:center-of-rotation NIL) ; where rotate around
		     (:saved-original-angle NIL) ; used for ABORT
		     (:saved-last-angle NIL) ; used if stop and outside and
					     ; outside control is :last
		     (:obj-being-rotated NIL) ; saved object under the mouse
		     (:Go 'General-Go)  ; proc executed when events happen
		     (:Do-Start 'Angle-Do-Start)     ; these are
		     (:Do-Running 'Angle-Do-Running) ;   called by GO
		     (:Do-Explicit-Stop 'Angle-Explicit-Stop);for stop-interactor
		     (:Do-Stop 'Angle-Do-Stop)       ;   to do
		     (:Do-Abort 'Angle-Do-Abort)     ;   the real work.
		     (:Do-Outside 'Angle-Do-Outside) ;   They call the
		     (:Do-Back-Inside 'Angle-Do-Back-Inside)  ; appropriate
		     (:Do-Outside-Stop 'Angle-Do-Outside-Stop); -action
		     					      ; procedures
		     (:initialize 'Angle-Interactor-Initialize))


