;;; -*- 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 support code for "selection.lisp", which handles
;;; selections in the graphical editor. 
;;;
;;; It also contains some code which is used by the constraint menu.
;;;
;;; ABOUT SELECTION:
;;;  Selection is indicated by the presence of feedback objects: either
;;; :p-feedback-obj or :s-feedback-obj.  These slots will be nil when there
;;; is no selection.  In addition, all selected objects are listed in the
;;; :p-selected and :s-selected slots of the corresponding editor-agg:
;;; "x is p-selected" is true iff
;;;     (member x (g-value x :window :editor-agg :p-selected))
;;;
;;; use (is-p-selected x) and (is-s-selected x) to test for selection

;;; CHANGE LOG
;;;
;;; 08/10/93 bvz - modified the selection routines so that
;;;                  they could support either the selection of leaves or
;;;                  top-level objects.

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

(defvar *test-debug* NIL)

;;; =================================================================
;;; return a feedback object of the specified type. first determine
;;; if a feedback object of the given type already exists and return
;;; it if possible; otherwise create a new feedback object
;;; =================================================================

(defun get-feedback-obj (feedback-list feedback-type
				       &optional filling-style
				       &key (size 7))
  (let ((feedback-obj (if (null feedback-list)
			  (if (null filling-style)
			      (create-instance nil feedback-type)
			      (make-grow-feedback-obj filling-style 
						      feedback-type
						      :size size))
			  (pop feedback-list))))
    ;; sometimes a destroyed feedback object will be left on the
    ;; feedback list
    (when (not (schema-p feedback-obj))
	  (loop
	   (setf feedback-obj 
		 (if (null feedback-list)
		     (if (null filling-style)
			 (create-instance nil feedback-type)
			 (make-grow-feedback-obj filling-style 
						 feedback-type
						 :size size))
		     (pop feedback-list)))
	   (when (schema-p feedback-obj)
		 (return))))
    (values feedback-obj feedback-list)))

;;; =================================================================
;;; decide what type of feedback object is required (line, aggregate,
;;; leaf, or undersized). If the well of appropriate feedback objects 
;;; has been exhausted, allocate a new feedback object and add it to 
;;; the feedback aggregate; otherwise, get a feedback object from the 
;;; well.
;;; =================================================================

(defun select-feedback-obj (obj filling-style)
  (declare (special *undersized-feedback-list* *line-feedback-list*
		    *leaf-feedback-list* *agg-feedback-list*))
  (let (feedback-obj)
    ;; first determine if the object is undersized. an undersized aggregate 
    ;; is one whose width or height is less than
    ;; ((2 * size of aggregate feedback selection circle) + 4) pixels
    ;; and an undersized graphics object is one whose width or height is
    ;; less than
    ;; ((2 * size of graphics feedback selection box) + 4) pixels
    (loop
     (cond ((cond ((is-a-line-p obj)
		   (< (sqrt 
		       (+ (expt (- (g-value obj :y2) (g-value obj :y1)) 2)
			  (expt (- (g-value obj :x2) (g-value obj :x1)) 2)))
		      *min-leaf-size*))
		  ((is-a-p obj opal:aggregate)
		   (or (< (g-value obj :width) *min-agg-size*)
		       (< (g-value obj :height) *min-agg-size*)))
		  (t
		   (or (< (g-value obj :width) *min-leaf-size*)
		       (< (g-value obj :height) *min-leaf-size*))))
	    (multiple-value-setq (feedback-obj *undersized-feedback-list*)
				 (get-feedback-obj *undersized-feedback-list*
						   undersized-feedback-obj)))
	   ((is-a-line-p obj)
	    (multiple-value-setq (feedback-obj *line-feedback-list*)
				 (get-feedback-obj *line-feedback-list*
						   line-selection-boxes)))
	   ((is-a-p obj opal:aggregate)
	    (multiple-value-setq (feedback-obj *agg-feedback-list*)
	      (get-feedback-obj *agg-feedback-list* opal:circle 
				filling-style :size *agg-sel-circle-size*)))
	   (t 
	    (multiple-value-setq (feedback-obj *leaf-feedback-list*)
	        (get-feedback-obj *leaf-feedback-list* opal:rectangle
		 	      filling-style))))

      ;; ensure we have a valid feedback object. Invalid feedback objects
      ;; may be left around when windows are destroyed
      (when (and feedback-obj (kr::schema-name feedback-obj))
	(return)))
    
    ;; set the feedback object's filling style
    (s-value feedback-obj :filling-style filling-style)

    ;; feedback objects can move between windows and right now the
    ;; feedback object could be attached to the wrong window. If it
    ;; is, move it to the correct window
    (when (not (eq (g-value obj :window)
		   (g-value feedback-obj :window)))
      (let ((parent (get-local-value feedback-obj :parent)))
	(when parent (opal:remove-component parent feedback-obj)))
      (opal:add-component (g-value obj :window :feedback-agg) feedback-obj))
    
    ; set the feedback's obj-over slot so it will orient itself correctly
    (s-value feedback-obj :obj-over obj)
    
    ;; add the feedback object to the feedback object list kept
    ;; in *selection-info*
    (s-value *selection-info* :feedback 
	     (push feedback-obj (g-value *selection-info* :feedback)))
    feedback-obj))

;;; =================================================================
;;; return a feedback object to its feedback list and make it 
;;; invisible by setting its :obj-over slot to nil
;;; =================================================================

(defun return-feedback-to-list (obj feedback-obj)
  (declare (special *undersized-feedback-list* *line-feedback-list*
		    *leaf-feedback-list* *agg-feedback-list* *selection-info*))
  (cond ((is-a-p feedback-obj line-selection-boxes)
	 (push feedback-obj *line-feedback-list*))
	((is-a-p feedback-obj undersized-feedback-obj)
	 (push feedback-obj *undersized-feedback-list*))
	((is-a-p obj opal:aggregate)
	 (push feedback-obj *agg-feedback-list*))
	(t 
	 (push feedback-obj *leaf-feedback-list*)))
  (s-value feedback-obj :obj-over nil)
  ;; remove the feedback object from the feedback object list kept
  ;; in *selection-info*
  (s-value *selection-info* :feedback 
	   (delete feedback-obj (g-value *selection-info* :feedback))))

;;; ==========================================
;;; this function creates a feedback object
;;; with eight "grow" boxes
;;; ========================================

(defun make-grow-feedback-obj (filling-style obj-type &key (size 7))

  (let (main obj
	(sizeD2 (floor size 2)))

    (setq main
	  (kr:create-instance nil opal:aggregate
	      (:name "Top level selection aggregate")
	      (:left (o-formula (- (gvl :obj-over :left) sizeD2) 0))
	      (:top (o-formula (- (gvl :obj-over :top) sizeD2) 0))
	      (:width (o-formula (if (is-a-p (gvl :obj-over) opal:circle)
				     (+ (gvl :min-width-height) size)
				     (+ (gvl :obj-over :width) size))
				 0))
	      (:height (o-formula (if (is-a-p (gvl :obj-over) opal:circle)
				      (+ (gvl :min-width-height) size)
				      (+ (gvl :obj-over :height) size))
				  0))
	      (:visible (o-formula (gvl :obj-over)))
	      (:obj-over NIL)
	      (:min-width-height (o-formula (min (gvl :obj-over :width)
						 (gvl :obj-over :height))))
;	      (:fast-redraw-p t)
	      (:draw-function :xor)
	      (:filling-style filling-style)
	      (:line-style (o-formula (if (eql (gvl :filling-style) opal:black-fill)
					  nil
					  opal:thin-line)))))

    (do ((x '(l m r r r m l l) (cdr x))
	 (y '(t t t m b b b m) (cdr y))
	 (attach '(:nw :n :ne :e :se :s :sw :w) (cdr attach)))
	((null x)) ; end test
      (setq obj
	    (kr:create-instance nil obj-type
		  (:name "Selection box")
	          (:left (case (car x)
			   (l (o-formula (gv main :left)))
			   (m (o-formula (opal:gv-center-x-is-center-of main)))
			   (r (o-formula (- (opal:gv-right main) size)))))
	          (:top (case (car y)
			  ((t) (o-formula (gv main :top)))
			  (m (o-formula (opal:gv-center-y-is-center-of main)))
			  (b (o-formula (- (opal:gv-bottom main) size)))))
		  (:width size) (:height size)
		  (:draw-function :xor)
		  (:fast-redraw-p t)
		  (:where-attach (car attach))
		  (:filling-style (o-formula (gvl :parent :filling-style)))
		  (:line-style (o-formula (gvl :parent :line-style)))))
      (opal:add-components main obj))
    main))

;;; ============================================
;;; deselect the selected objects on the display
;;; ============================================

(defun deselect-objects (&optional (window nil))
    
  (primary-deselect-objects :none window)
  (secondary-deselect-objects :none window))

;;; ============================================
;;; deselect the designated primary selection
;;; object. if no object is designated, deselect
;;; everything
;;; ============================================

(defun primary-deselect-objects (selection &optional (window nil))
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null selection)
	(return-from primary-deselect-objects))
  (let ((selection-list 
	  (if (eql selection :none)
	      (if window
		  (g-value window :editor-agg :p-selected)
		  (g-value *selection-info* :p-selected))
	      (g-value selection :window :editor-agg :p-selected)))
    obj)

    (cond ((eq selection :none)
	   (dolist (item selection-list)
	     ; return the object's feedback obj to the feedback list
	     (primary-deselect item)))
	  
	   ; otherwise only one object should be deselected -- see if we
	   ; can find it:
	   
	  ((setf obj (leaf-to-selection selection :p-feedback-obj 
					(g-value selection :window :editor-agg)))
	   ; return the object's feedback obj to the feedback list
	   (primary-deselect obj))
	  (t
	   nil))))


;;; ============================================
;;; deselect the designated secondary selection
;;; object. if no object is designated, deselect
;;; everything
;;; ============================================

(defun secondary-deselect-objects (selection &optional (window nil))
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null selection)
	(return-from secondary-deselect-objects))
  (let ((selection-list 
	  (if (eql selection :none)
	      (if window
		  (g-value window :editor-agg :s-selected)
		  (g-value *selection-info* :s-selected))
	      (g-value selection :window :editor-agg :s-selected)))
	obj)

    (cond ((eql selection :none) 
	   (setf selection-list (g-value *selection-info* :s-selected)))
	  (t (setf selection-list 
		   (g-value selection :window :editor-agg :s-selected))))

    (cond ((eql selection :none)
	   (dolist (item selection-list)
	     ; return the object's feedback obj to the feedback list
	     (secondary-deselect item)))
	  
	  ; otherwise only one object should be deselected -- see if we
	  ; can find it
	   
	  ((setf obj (leaf-to-selection selection :s-feedback-obj 
					(g-value selection :window :editor-agg)))
	   ; return the object's feedback obj to the feedback list
	   (secondary-deselect obj))
	  (t
	   nil))))



;; starting from a leaf object, go up the parent links until you find
;; a selected object
;;  sel-type is either :p-feedback-obj :s-feedback-obj
;;  top is the editor window's top-level aggregate
(defun leaf-to-selection (leaf sel-type top)
  (cond ((eq leaf top) nil)
	((get-local-value leaf sel-type) leaf)
	(t
	 (leaf-to-selection (get-local-value leaf :parent) sel-type top))))


;; primary-deselect removes a primary feedback object from an object
;;
(defun primary-deselect (obj)
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null obj)
	(return-from primary-deselect))
  (let ((feedback-obj (get-local-value obj :p-feedback-obj))
	(editor-agg (g-value obj :window :editor-agg)))
    (return-feedback-to-list obj feedback-obj)
    (s-value obj :p-feedback-obj nil)
    (s-value editor-agg :p-selected
	     (delete obj (g-value editor-agg :p-selected)))
    (s-value *selection-info* :p-selected
	     (delete obj (g-value *selection-info* :p-selected)))  ))


;; secondary-deselect removes a secondary feedback object from an object
;;
(defun secondary-deselect (obj)
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null obj)
	(return-from secondary-deselect))
  (let ((feedback-obj (get-local-value obj :s-feedback-obj))
	(editor-agg (g-value obj :window :editor-agg)))
    (return-feedback-to-list obj feedback-obj)
    (s-value obj :s-feedback-obj nil)
    (s-value editor-agg :s-selected
	     (delete obj (g-value editor-agg :s-selected)))
    (s-value *selection-info* :s-selected
	     (delete obj (g-value *selection-info* :s-selected)))  ))


;; primary-select installs a primary feedback object on an object
(defun primary-select (obj)
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null obj)
	(return-from primary-select))
  (let ((feedback-obj (select-feedback-obj obj opal:black-fill)))
    (s-value obj :p-feedback-obj feedback-obj)
    (push obj (g-value (g-value obj :window :editor-agg) :p-selected))
    (push obj (g-value *selection-info* :p-selected))))
    
	
;; secondary-select installs a secondary feedback object on an object
(defun secondary-select (obj)
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null obj)
	(return-from secondary-select))
  (let ((feedback-obj (select-feedback-obj obj opal:white-fill)))
    (s-value obj :s-feedback-obj feedback-obj)
    (push obj (g-value (g-value obj :window :editor-agg) :s-selected))
    (push obj (g-value *selection-info* :s-selected))))
    

;;; ===============================================
;;; this function makes the next selection
;;; be the parent of the currently selected item.
;;; If the next selection would be the editor
;;; aggregate, then cycle back and select the
;;; provided leaf object.
;;; ===============================================

(defun toggle-selection (current-selection leaf-obj)
  (let* ((parent (get-local-value current-selection :parent))
	 (window (get-local-value current-selection :window))
	 (editor-agg (g-value window :editor-agg))
	 (next-selection (if (equal editor-agg parent)
			     leaf-obj
			     parent)))

    ; remove the current selection from the list and change the slots:
    (primary-deselect current-selection)

    ; select the object pointed at by next-selection if it is not already
    ; selected
    (cond ((not (is-p-selected next-selection))
	   (primary-select next-selection)))))

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



;;; ===============================================
;;; this function makes the next selection
;;; be the parent of the currently selected item.
;;; ===============================================

(defun toggle-secondary-selection (current-selection leaf-obj)
  (let* ((parent (get-local-value current-selection :parent))
	 (window (get-local-value current-selection :window))
	 (editor-agg (g-value window :editor-agg))
	 (next-selection (if (equal editor-agg parent)
				 leaf-obj
				 parent)))

    ; remove current selection:
    (secondary-deselect current-selection)

    ; select the object pointed at by next-selection if it is not already
    ; selected
    (cond ((not (is-s-selected next-selection))
	   (secondary-select next-selection)))))


;;; ===============================================
;;; primarily select an object. if the object or an 
;;; ancestor has been selected, cycle through the
;;; aggregate hierarchy; otherwise allocate a
;;; feedback object and tie it to the selected
;;; object.  When cycling, make sure the new
;;; selection is not already selected! If
;;; requested, deselect any other primary
;;; selections
;;; ===============================================

(defun make-p-selection (object &optional (deselect-p nil))
  (declare (special *selection-info*))
  ;; safety check--should never be called with a null object but it happens
  (when (null object)
	(return-from make-p-selection))
  (let ((current-selection
	 (leaf-to-selection object :p-feedback-obj
			    (g-value object :window :editor-agg))))
    (cond (current-selection
	   ; if the object has already been selected, toggle
	   ; the selection
	   (toggle-selection current-selection object))
	  (t
	   ; the object has not been selected;
	   ; if requested, deselect other primary selections
	   (when deselect-p
	     (primary-deselect-objects :none))

	   ; set the object up with a feedback object--determine whether
	   ; the object should be selected or whether the object's top-level
	   ; aggregate should be selected
	   (if (g-value *selection-info* :leaf)
	       (primary-select object)
	       (primary-select (get-root object)))))

    ; cause the constraint, line, filling, and
    ; object menus to be updated
;    (opal:update-all)
))



;;; ===============================================
;;; secondarily select an object. if the object has
;;; already been selected, cycle through the
;;; aggregate hierarchy; otherwise allocate a
;;; feedback object and tie it to the selected
;;; object. If requested, deselect any other
;;; secondary selections.
;;; ===============================================

(defun make-s-selection (object &optional (deselect-p nil))
  ;; safety check--should never be called with a null object but it happens
  (when (null object)
	(return-from make-s-selection))
  (let ((current-selection
	 (leaf-to-selection object :s-feedback-obj
			    (g-value object :window :editor-agg))))
    (cond (current-selection
	   ; if the object has already been selected, toggle
	   ; the selection
	   (toggle-secondary-selection current-selection object))
	  (t
	   ; the object has not been selected;
	   ; if requested, deselect other secondary selections
	   (when deselect-p
	     (secondary-deselect-objects :none))

	   ; set the object up with a feedback object--determine whether
	   ; the object should be selected or whether the object's top-level
	   ; aggregate should be selected
	   (if (g-value *selection-info* :leaf)
	       (secondary-select object)
	       (secondary-select (get-root object)))))

    ; cause the constraint, line, filling, and
    ; object menus to be updated
;    (opal:update-all)
))

