;;; -*- 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. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; gestureinter.lisp
;;;
;;; This file contains the mouse interactors to handle gestures.
;;; It should be loaded after Interactor.lisp
;;;
;;; Designed and implemented by James A. Landay 

#|
========================================================================
Change log:
    05/25/94 Andrew Mickish - New Mac and X versions of draw-gesture-line
    04/04/94 James Landay - added a :line-style slot for gesture trace
    04/04/94 James Landay - use gem stuff to draw instead of xlib and mac --
                            also reset clip region in start-action
    04/02/94 James Landay - changed :xwindow slot to :drawable
    04/02/94 James Landay - added a new slot to the interactor, :erase.  
                            Don't erase the gesture trace if :erase is true
    04/01/94 James Landay - don't use XOR -- instead invalidate and update
                            bbox of gesture (so works on Mac also)
    03/16/94 Andrew Mickish - #@(x y) ---> (ccl:make-point x y) because
               Allegro parses it anyway, even though it's in a #+apple switch?!
    02/16/94 Andrew Mickish - Fixed graphics routines for Mac
    06/15/93 James Landay - use HP-XOR-Hack instead of explicit xor handling
    04/03/92 James Landay - don't erase a point passed to stop-action,
                            since we never draw this point.
    02/19/92 James Landay - added gesture-fix-gc () to the 
                            opal::*auxilliary-reconnect-routines* so
                            that if user switches monitors, get a new gc
    02/13/92 James Landay - special case for color monitors
                            to make xor drawing work properly
    01/31/92 James Landay - moved all export declarations
                            for gestures here.
    12/21/91 James Landay - started

========================================================================
|#

(in-package "INTERACTORS")

(eval-when (eval load compile)
  (export '(gesture-interactor

	    gest-classify                 ;; functions in classify.lisp
	    gest-new-classifier
	    make-gest-class
	    gest-class-name
	    gest-class-examples

	    gest-attributes-minx          ;; functions in features.lisp 
	    gest-attributes-maxx
	    gest-attributes-miny
	    gest-attributes-maxy
	    gest-attributes-initial-sin    
	    gest-attributes-initial-cos
	    gest-attributes-startx     
	    gest-attributes-starty
	    gest-attributes-endx       
	    gest-attributes-endy
	    gest-attributes-dx2           
	    gest-attributes-dy2          
	    gest-attributes-magsq2      
	    gest-attributes-path-r
	    gest-attributes-path-th
	    gest-attributes-abs-th
	    gest-attributes-sharpness

	    gest-classifier-read          ;; functions in fileio.lisp
	    gest-classifier-write
	    gest-classifier-convert)))

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


;;;============================================================
;;; Utility Procedures 
;;;============================================================

(defconstant POINT-ARRAY-SIZE 80)   ; initial size of point array
(defconstant EXTENTION-SIZE 30)     ; amount to extend by when full

;; global variables definitions
                                    ; the points gestured....
(defvar *points* (make-array POINT-ARRAY-SIZE :adjustable t 
                             :fill-pointer 0 :element-type 'integer))
(defvar *last-point* (list 0 0))    ; use these to avoid cons-ing
(defvar *cur-point*  (list 0 0))


;;;============================================================
;;; Graphics routines
;;;============================================================

;; Draw-Gesture-Line uses gem to quickly draw a line between the given points
;; with the given linestyle.  Both the Mac and X versions draw directly to the
;; window's actual drawable, not ever to its backing store.
;; 
;; Parameters:
;;    window     - window to draw in
;;    x1, y1     - point to draw from
;;    x2, y2     - point to draw to
;;    line-style - style to draw in
;;
(defmacro draw-gesture-line (window x1 y1 x2 y2 line-style)
  #-apple
  `(let ((the-window ,window))
    (gem:draw-line the-window ,x1 ,y1 ,x2 ,y2 :copy ,line-style
                   (g-value the-window :drawable)))

  #+apple
  `(let ((the-window ,window))
     (ccl:with-focused-view (g-value the-window :drawable)
       (gem:draw-line the-window ,x1 ,y1 ,x2 ,y2 :copy ,line-style))))


;; gesture-bbox returns the bounding box for the given array of points
;; 
;; Parameters:
;;    points - array of points to get the bounding box of (needs fill-pointer)
;;
(defun gesture-bbox (points)
  (let ((maxx -99999) (maxy -99999)
        (minx 99999) (miny 99999)
        (x 0) (y 0)
        (size (fill-pointer points)))
    (do* ((index 0 (+ index 2)))
         ((>= index size))       ;; exit condition

      (setf x (aref points index))
      (setf y (aref points (1+ index)))

      ;; check X coord
      (if (> x maxx)
        (setf maxx x))
      (if (< x minx)
        (setf minx x))
        	
      ;; check Y coord
      (if (> y maxy)
        (setf maxy y))
      (if (< y miny)
        (setf miny y)))
	
    ;; return the bounding box
    (opal::make-bbox :x1 minx :y1 miny :x2 maxx :y2 maxy :valid-p T)))

      
;; erase-path erases the lines drawn between the given points in the 
;; given window. Calculates the bbox, which can be repetitive (since it 
;; is USUALLY computed by the gesture recognizer, but NOT always.)
;;
;; Parameters:
;;    points - array of points to erase
;;    window - window to erase in
;;
(defun erase-path (points window)
  (let ((bbox (gesture-bbox points)))
    (opal::invalidate-bbox window 
                           (1- (opal::bbox-x1 bbox)) (1- (opal::bbox-y1 bbox))
                           (1+ (opal::bbox-x2 bbox)) (1+ (opal::bbox-y2 bbox)))
    (opal:update window)))
 	

;;;============================================================
;;; Gesture-Interactor
;;;============================================================


;;;============================================================
;;; Default Procedures to go into the slots
;;;============================================================


(declaim (special Gesture-Interactor))


(defun Gesture-Interactor-Initialize (new-Gesture-schema)
    (if-debug new-Gesture-schema 
              (format T "Gesture initialize ~s~%" new-Gesture-schema))

    (Check-Interactor-Type new-Gesture-schema inter:gesture-interactor)
    (Check-Required-Slots new-Gesture-schema)
    (Set-Up-Defaults new-Gesture-schema)
) ;end initialize procedure


;; Draws the latest point in the path
(defun Gesture-Int-Running-Action (an-interactor new-obj-over point)
  (if-debug an-interactor 
	    (format T "Gesture int-running, new-obj-over= ~s, point= ~s~%"
		    new-obj-over point))

  ;; draw the new point if trace is true and if didn't go outside 
  (when (and (g-value an-interactor :show-trace)
	     (not (g-value an-interactor :went-outside)))
    (draw-gesture-line (g-value an-interactor :current-window)
		       (first *last-point*) (second *last-point*)
		       (first point) (second point)
		       (g-value an-interactor :line-style)))
  
  ;; set the new last point without cons-ing 
  (setf (first *last-point*) (first point)) 
  (setf (second *last-point*) (second point)) 
  
  ;; add point to array of points
  (vector-push-extend (first point) *points* EXTENTION-SIZE) 
  (vector-push-extend (second point) *points*  EXTENTION-SIZE))


;; Executes the running action for the first point
(defun Gesture-Int-Start-Action (an-interactor obj-under-mouse point)
  (if-debug an-interactor 
	    (format T "Gesture int-start over ~s~%, point = ~s~%" 
		    obj-under-mouse point))

  ;; set the initial object that we started over
  (s-value an-interactor :first-obj-over obj-under-mouse) 

  ;; not outside...
  (s-value an-interactor :went-outside NIL)

  ;; reset the clip mask to the window size (may need to process lock this)
  (let* ((current-window (g-value an-interactor :current-window))
	 (clip-mask (list 0 0 (g-value current-window :width)
			  (g-value current-window :height)))
	 (display-info (g-value current-window :display-info))
	 (line-style-gc (opal::display-info-line-style-gc display-info))
	 (filling-style-gc (opal::display-info-filling-style-gc display-info)))

    (gem:set-clip-mask current-window clip-mask 
		       line-style-gc filling-style-gc))
    
  ;; set the initial last-point and make the set of points empty
  (setf (first *last-point*) (first point)) 
  (setf (second *last-point*) (second point)) 
  (setf (fill-pointer *points*) 0)

  ;; do the running action
  (kr-send an-interactor :running-action 
	   an-interactor obj-under-mouse point) )


;; Try to recognize the gesture and then erase the path
(defun Gesture-Int-Stop-Action (an-interactor final-obj-over point)
  (if-debug an-interactor 
	    (format T "Gesture int-stop over ~s~%" final-obj-over))

  ;; don't call final function or erase (already erased) if went outside
  (unless (g-value an-interactor :went-outside)

    ;; send the points to the classifier
    (let ((class-name nil)
	  (attributes nil)
	  (nap nil)
	  (dist nil))

      ;; add the latest point (if there is one) to the array 
      (when point
	(vector-push-extend (first point) *points* EXTENTION-SIZE) 
	(vector-push-extend (second point) *points*  EXTENTION-SIZE))
            
      ;; draw the new point if trace is true
      (when (g-value an-interactor :show-trace)
	(draw-gesture-line (g-value an-interactor :current-window)
			   (first *last-point*) (second *last-point*)
			   (first point) (second point)
			   (g-value an-interactor :line-style)))

      ;; classify the gesture
      (multiple-value-setq (class-name attributes nap dist)
	(gest-classify *points* 
		       (g-value an-interactor :classifier)
		       (g-value an-interactor :min-non-ambig-prob)
		       (g-value an-interactor :max-dist-to-mean)))
      
      ;; erase the line if :show-trace is true 
      ;; If :erase is false and not recognized, erase anyways
      (when (and (g-value an-interactor :show-trace) 
		 (or (g-value an-interactor :erase) (not class-name)))
	(erase-path *points* (g-value an-interactor :current-window)))

      (if-debug an-interactor
		(format T "Gesture classified as ~s~%" class-name))
      (if-debug an-interactor
		(format T "with probability ~s and distance ~s~%" 
			nap dist))
      
      (kr-send an-interactor :final-function an-interactor 
	       (g-value an-interactor :first-obj-over) 
	       class-name attributes *points* nap dist))))


;; don't do anything.... we want to wait for mouse up
(defun Gesture-Int-Back-Inside-Action (an-interactor new-obj-over)
  (if-debug an-interactor
	    (format T "Gesture int-back-inside, obj= ~s~%" new-obj-over)))


;; beep and erase the line if :show-trace is true
(defun Gesture-Int-Outside-Action (an-interactor prev-obj-over)
  (if-debug an-interactor 
	    (format T "Gesture int-outside, old = ~s~%" prev-obj-over))
  (inter:beep)
  (when (g-value an-interactor :show-trace)
    (erase-path *points* (g-value an-interactor :current-window)))
  (s-value an-interactor :went-outside T))


;; erase the gesture if it was visible and haven't been outside 
(defun Gesture-Int-Abort-Action (an-interactor)
  (if-debug an-interactor (format T "Gesture int-abort over ~%"))
  (when (and (g-value an-interactor :show-trace)
	     (not (g-value an-interactor :went-outside)))
    (erase-path *points* (g-value an-interactor :current-window))))


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


;;; if continuous: (remove from start level, add to stop and abort
;;;             levels, change state to running
;;;             *ALSO* fix running where to be the object started over)
;;; save object over, call start procedure.
(defun gesture-do-start (an-interactor new-obj-over event)
    (if-debug an-interactor 
              (format T "Gesture starting over ~s~%" new-obj-over))
        
    (setf (first *cur-point*) (event-x event))
    (setf (second *cur-point*) (event-y event)) 

    (if (g-value an-interactor :continuous)  ;then go to running state
        (progn
            (Fix-Running-Where an-interactor new-obj-over)
            (GoToRunningState an-interactor T)
            (kr-send an-interactor :start-action an-interactor 
                     new-obj-over *cur-point*)
        )
        ;; else call stop-action
        (progn
            (kr-send an-interactor :stop-action an-interactor 
                     new-obj-over *cur-point*)
            (GoToStartState an-interactor NIL)
        )
    )
)


(defun gesture-do-abort (an-interactor become-inactive event)
    (declare (ignore event become-inactive))
    (if-debug an-interactor (format T "Gesture aborting~%"))

    (GoToStartState an-interactor T)
    (kr-send an-interactor :abort-action an-interactor)
)


(defun gesture-do-outside (an-interactor)
    (if-debug an-interactor (format T "Gesture outside~%"))

    (s-value an-interactor :current-state :outside)
    (kr-send an-interactor :outside-action an-interactor
             (g-value an-interactor :outside))
)


; call abort
(defun gesture-do-outside-stop (an-interactor event)
    (if-debug an-interactor (format T "Gesture stop outside~%"))
    (gesture-do-abort an-interactor NIL event)
)


; call back-inside procedure, change state to running
(defun gesture-do-back-inside (an-interactor new-obj-over event)
    (declare (ignore event))
    (if-debug an-interactor 
              (format T "Gesture back-inside over ~s~%" new-obj-over))

    (s-value an-interactor :current-state :running)
    (kr-send an-interactor :back-inside-action an-interactor new-obj-over)
)


; get the new point and pass it to the running-action
(defun gesture-do-running (an-interactor new-obj-over event)
    (if-debug an-interactor 
              (format T "Gesture running over ~s~%" new-obj-over))

    (setf (first *cur-point*) (event-x event))
    (setf (second *cur-point*) (event-y event)) 
    (kr-send an-interactor :running-action an-interactor 
             new-obj-over *cur-point*)
)


;;; Will be inside
;;; Remove from running level, add to start level
;;; unless :self-deactivate, change state to start, call stop procedure
(defun gesture-do-stop (an-interactor new-obj-over event)
    (if-debug an-interactor 
              (format T "Gesture stop over ~s~%" new-obj-over))

    (setf (first *cur-point*) (event-x event))
    (setf (second *cur-point*) (event-y event)) 
    (GoToStartState an-interactor T)
    (kr-send an-interactor :stop-action an-interactor 
             new-obj-over *cur-point*)
)


;;; This is used if explicitly call Stop-Interactor.  
(defun gesture-explicit-stop (an-interactor)
    (if-debug an-interactor (format T "Gesture explicit stop~%"))

    (GoToStartState an-interactor T)
    (kr-send an-interactor :stop-action an-interactor NIL NIL)
)


;;;============================================================
;;; Gesture schema
;;;============================================================

(Create-Schema 'inter:gesture-interactor
        (:is-a inter:interactor)
        (:name :First-Gesture-interactor)
        (:start-action 'Gesture-Int-Start-Action)
        (:running-action 'Gesture-Int-Running-Action)
        (:stop-action 'Gesture-Int-Stop-Action)
        (:abort-action 'Gesture-Int-Abort-Action)
        (:outside-action 'Gesture-Int-Outside-Action)
        (:back-inside-action 'Gesture-Int-Back-Inside-Action)
        (:abort-event '(:control-g :control-\g))
        (:running-where T)             
        (:classifier NIL)              ; classifier to use
        (:show-trace T)                ; show trace of gesture?
        (:erase T)		       ; if showing trace, erase if recognized?
	(:line-style opal:default-line-style)
        (:min-non-ambig-prob nil)      ; non-ambiguity probability
        (:max-dist-to-mean nil)        ; distance to class mean 
        (:went-outside NIL)            ; set in outside action  
        (:first-obj-over NIL)          ; object started on
        (:Go 'General-Go)  ; proc executed when events happen
        (:Do-Start 'Gesture-Do-Start)     ; these are
        (:Do-Running 'Gesture-Do-Running) ;   called by GO
        (:Do-Explicit-Stop 'Gesture-Explicit-Stop) ;for stop-interactor
        (:Do-Stop 'Gesture-Do-Stop)       ;   to do
        (:Do-Abort 'Gesture-Do-Abort)     ;   the real work.
        (:Do-Outside 'Gesture-Do-Outside) ;   They call the
        (:Do-Back-Inside 'Gesture-Do-Back-Inside)  ; appropriate
        (:Do-Outside-Stop 'Gesture-Do-Outside-Stop); -action procedures
        (:initialize 'Gesture-Interactor-Initialize)) ;proc to call
                                               ; when created
