;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: GARNET-GADGETS; 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. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;  BROWSER-GADGET
;;;
;;;    The browser-gadget is useful for examining hierarchical structures,
;;; such as Garnet object networks and directory trees.  The gadget is composed
;;; of a set of scrolling menus, where each title corresponds to an item
;;; specified in the top-level :items list, and each list of menu selections
;;; corresponds to the children of the item appearing in the title.  Clicking
;;; the left mouse button on one of the menu selections causes that selection
;;; to appear as the title of the next scrolling menu, with all of its children
;;; appearing as choices in the new menu.  Additionally, middledown over a menu
;;; selection causes a gray feedback object to appear over the selection.
;;;
;;;   1) :num-menus - The number of visible scrolling menus.  If this slot is
;;;          set at run time, opal:notice-items-changed must be called on the
;;;          :menu-list of the browser-gadget instance.
;;;   2) :num-rows - The number of items in each scrolling menu.
;;;   3) :menu-items-generating-function - The function used to generate menu
;;;          selections from the items appearing in the :items list.
;;;   4) :item-to-string-function - The function used to get a string from a
;;;          member of the :items list.
;;;   5) :additional-selection-coordinate - A two element list of the form
;;;          '(menu-rank choice-rank), where the ranks are zero-based according
;;;          according to the full length :items list and scrolling menu
;;;          choices.
;;;   6) :additional-selection-p - Whether to allow middledown gray selection
;;;   7) :additional-selection-function - Function executed when the additional
;;;          selection is selected.
;;;   8) :additional-selection - The item named by the additional selection
;;;          (do not set this slot; instead set the
;;;          :additional-selection-coordinate).
;;;   9) :selection-function - Function to be executed when the user selects
;;;          one of the menu choices.
;;;  10) :item-font - The font that the menu items will appear in.
;;;  11) :title-font - The font that the titles of the menus will appear in.
;;;
;;;  NOTE: This module requires schemata defined for the h-scroll-bar and
;;;  scrolling-menu gadgets.  These modules are automatically loaded when the
;;;  loader file "browser-gadget-loader" is used.
;;;
;;;  Browser-gadget demos:
;;;     Two demos of the browser-gadget appear in seperate garnet modules:
;;;     the DEMO-SCHEMA-BROWSER and the DEMO-FILE-BROWSER.
;;;
;;;  Designed by Brad Myers
;;;  Written by Andrew Mickish

;;;  Maintenance note:
;;;    In order to avoid a circularity which arises from the dependence of the
;;;  horizontal scroll bar width on the scrolling menu aggrelist width, a
;;;  complicated formula has been installed in the :width slot of the top-level
;;;  aggrelist.  When this formula is evaluated, it sets the :width of the
;;;  horizontal scroll bar and the proper pixel position of the indicator box
;;;  according to this new width.  In order to ensure that this formula is
;;;  evaluated as necessary, a dummy rectangle is included in the
;;;  browser-gadget's :parts list whose :left formula is computed whenever
;;;  the visible items change.

;;;
;;; CHANGE LOG:
;;;
;;; 12/14/92  Andrew Mickish - Added type and parameter declarations
;;; 09/15/92  Andrew Mickish - Fixed bug in computation of new :starts list.
;;; 04/30/92  Andrew Mickish - Called get-standard-font for italic font,
;;;             added (:constant T) to line-style definition
;;; 03/27/92  Andrew Mickish - Changed DOVALUES to DO in :notice-items-changed
;;;             method to eliminate CMUCL compiler warnings.
;;; 02/11/92  Andrew Mickish - Added :maybe-constant list
;;;

(in-package "GARNET-GADGETS" :use '("LISP" "KR"))

(export '(BROWSER-GADGET
	  BROWSER-MENU-FN PUSH-FIRST-ITEM PROMOTE-ITEM
	  SET-FIRST-ITEM BROWSER-MENU-SCROLL-FN BROWSER-SCROLL-FN))


;;  BROWSER-MENU-FN:  Called when an item is selected or deselected.
;;  Updates information lists in the top level browser object.
;;
(defun BROWSER-MENU-FN (menu sm-item)
  (let* ((sm-item-rank (g-value sm-item :real-rank))
	 (browser (g-value menu :parent :parent))
	 (starts (g-value browser :starts))
	 (items (g-value browser :items))
	 (all-items (g-value browser :all-items))
	 (item (g-value sm-item :item))
	 (menu-rank (g-value menu :rank))
	 (real-menu-rank (+ menu-rank (g-value browser :start)))
	 (new-menu-rank (+ 1 real-menu-rank))
	 (selected (g-value browser :selected-ranks)))

    (if (g-value sm-item :highlighted)
	; if item has just become selected, update the info lists in
	; browser to include a new menu

	(let ((generated-items (kr-send browser
					:menu-items-generating-function item)))
	  (if generated-items
	      (let*  ; ADD A MENU
		((new-items (append (subseq items 0 new-menu-rank) (list item)))
		 (new-all-items (append (subseq all-items 0 new-menu-rank)
					(list generated-items)))
		 (new-selected (append (subseq selected 0 real-menu-rank)
				       (list (list sm-item-rank))))
		 (saved-starts (subseq starts 0 new-menu-rank))
		 (free-menus (- (g-value browser :num-menus)
				(length saved-starts)))
		 (new-starts (append saved-starts
				     (if (< 0 free-menus)
					 (make-list free-menus
						    :initial-element 0)
					 (list 0)))))
				     
		; discard items that were beyond the rank of the current menu
		; and add the new item to the end of the :items list
		(s-value browser :items new-items)
		(s-value browser :all-items new-all-items)

		; discard selected-indices beyond and including the current
		; menu, then add the new selection to the end of the list
		(s-value browser :selected-ranks new-selected)
			 
		; discard starts that were beyond the rank of the current menu,
		; and add a new start for the new menu
		(s-value browser :starts new-starts))

	      ;else, make sure there are no menus to the right
	      (let ((new-items (subseq items 0 new-menu-rank)))
		(unless (equal items new-items)
		  (let ((new-all-items (subseq all-items 0 new-menu-rank))
			(new-selected (append (subseq selected 0 real-menu-rank)
					      (list (list sm-item-rank))))
			(new-starts (subseq starts 0 new-menu-rank)))
		  (s-value browser :items new-items)
		  (s-value browser :all-items new-all-items)
		  (s-value browser :selected-ranks new-selected)
		  (s-value browser :starts new-starts)))))
	  ; execute selection-function
	  (kr-send browser :selection-function browser item))

	;else, item has just become deselected
	(progn  ; DELETE MENUS
	  (let ((new-items (subseq items 0 new-menu-rank)))
	    (unless (equal items new-items)
	      (let* ((new-all-items (subseq all-items 0 new-menu-rank))
		     (new-selected (subseq selected 0 real-menu-rank))
		     (saved-starts (subseq starts 0 new-menu-rank))
		     (new-starts (append saved-starts
				   (make-list (- (length starts)
						 (length saved-starts))
					      :initial-element 0))))
		(s-value browser :items new-items)
		(s-value browser :all-items new-all-items)
		(s-value browser :selected-ranks new-selected)
		(s-value browser :starts new-starts))))))
		      

    ; Remove the gray feedback if it is to the right of the item just selected
    (let* ((gray-menu-rank (car (g-value browser
					 :additional-selection-coordinate))))
      (when (and gray-menu-rank (> gray-menu-rank real-menu-rank))
	(s-value browser :additional-selection-coordinate NIL)))

    ; Make sure the browser is scrolled all the way to the right
    (let* ((scroll-bar (g-value browser :scroll-bar))
	   (val-2 (g-value scroll-bar :val-2)))
      (unless (eq val-2 (g-value scroll-bar :value))
	(s-value scroll-bar :value val-2)))

    ; Set all the menus with the appropriate values for their :items lists
    ; and scroll-bar :value's
    (update-menus browser)))


(defun PUSH-FIRST-ITEM (browser new-item)
  (let* ((old-browser-items (g-value browser :items))
	 (old-all-items (g-value browser :all-items))
	 (generated-items (kr-send browser :menu-items-generating-function
				   new-item)))
    (s-value browser :items (cons new-item old-browser-items))
    (s-value browser :all-items (cons generated-items old-all-items))
    (let* (;; Determine which menu item in the new menu should be highlighted
	   (item-rank (position (car old-browser-items) generated-items
				:test #'equal))
	   (additional-selection-coordinate
	    (g-value browser :additional-selection-coordinate))
	   ;; If there are more than :num-menus items already, then just add
	   ;; a 0 to the front of the :starts list.  Otherwise, add a 0 to the
	   ;; front and strip the 0 off the end
	   (new-starts
	    (if (>= (length old-browser-items) (g-value browser :num-menus))
		(cons 0 (g-value browser :starts))
		(cons 0 (butlast (g-value browser :starts))))))

      (when old-browser-items
	(s-value browser :selected-ranks (cons (list item-rank)
					 (g-value browser :selected-ranks))))
      (s-value browser :starts new-starts)
      (if additional-selection-coordinate
	  (s-value browser :additional-selection-coordinate
		   (list (+ 1 (car additional-selection-coordinate))
			 (second additional-selection-coordinate))))
      (s-value (g-value browser :scroll-bar) :value 0)
      (update-menus browser))))


(defun PROMOTE-ITEM (browser coordinate)
  (let* ((real-menu-rank (car coordinate))
	 (real-obj-rank (cadr coordinate))
	 ;; KEEP-P is T if the item is both gray-selected and highlighted;
	 ;;   Used to decide whether to retain all of the items in the
	 ;;   scrolling-menus to the right of ITEM
	 (keep-p (equal (list real-obj-rank)
			(nth real-menu-rank (g-value browser :selected-ranks))))
	 (item (nth real-obj-rank
		    (nth real-menu-rank (g-value browser :all-items)))))
    (s-value (g-value browser :scroll-bar) :value 0)
    (if keep-p
	; Retain items in scrolling menus to the right: chop off the
	; left side of the :items, :starts, and :selected-ranks lists and
	; add fillers if necessary
	(let* ((rank+1 (+ 1 real-menu-rank))
	       (new-items (if item (nthcdr rank+1 (g-value browser :items))))
	       (new-all-items
		(if item (nthcdr rank+1 (g-value browser :all-items)))))
	  (s-value browser :items new-items)
	  (s-value browser :all-items new-all-items)
	  (let* ((num-menus (g-value browser :num-menus))
		 (new-items-length (length new-items))
		 (scroll-p (< new-items-length num-menus))
		 (shifted-starts (nthcdr rank+1 (g-value browser :starts)))
		 (extra-starts (if scroll-p
				   (make-list (- num-menus
						 (length shifted-starts))
					      :initial-element 0)))
		 (new-starts (append shifted-starts extra-starts)))
	    (s-value browser :starts new-starts))
	  (s-value browser
		   :selected-ranks
		   (nthcdr rank+1 (g-value browser :selected-ranks))))

	; Don't keep any of the current items -- zero out the lists
	(progn
	  (s-value browser :items (if item (list item)))
	  (s-value browser
		   :all-items
		   (if item
		       (list (kr-send browser :menu-items-generating-function
				      item))))
	  (s-value browser :selected-ranks NIL)
	  (s-value browser :starts (make-list (g-value browser :num-menus)
					      :initial-element 0))))
    (update-menus browser)))


(defun SET-FIRST-ITEM (browser item)
  (let* ((menu-list (g-value browser :menu-list))
	 (all-items (kr-send browser :menu-items-generating-function item)))

    ; Validate several slots containing formulas before they are destroyed
    ; by s-value's
    (g-value browser :scroll-bar :value)
    (opal:do-components menu-list
       #'(lambda (menu)
	   (g-value menu :scroll-bar :value)))

    ; Discard the current :items list and install the new item in it
    (s-value browser :items (if item (list item)))
    (s-value browser :all-items (if all-items (list all-items)))
    ; Reinitialize information lists
    (s-value browser :starts (make-list (g-value browser :num-menus)
					:initial-element 0))
    (s-value browser :selected-ranks NIL)
    (s-value browser :additional-selection-coordinate NIL)
    (update-menus browser)))


;;    This function sets the menus to have the correct :items list and correct
;; :value for the scroll bar.
;;
(defun UPDATE-MENUS (browser)
  (let ((start (g-value browser :start))
	(starts (g-value browser :starts))
	(items (g-value browser :items))
	(all-items (g-value browser :all-items)))
    (opal:do-components (g-value browser :menu-list)
      #'(lambda (menu)
	  (let* ((real-rank (+ start (g-value menu :rank)))
		 (title-item (nth real-rank items))
		 (items (nth real-rank all-items))
		 (value (nth real-rank starts))
		 (scroll-bar (g-value menu :scroll-bar)))
	    (unless (equal title-item (g-value menu :title-item))
	      (s-value menu :title-item title-item))
	    (unless (equal items (g-value menu :items))
	      (s-value menu :items items))
	    (unless (equal value (g-value scroll-bar :value))
	      ; Ensure that no current invalidations will override the new value
	      (g-value scroll-bar :value)
	      (s-value scroll-bar :value (if value value 0))))))))


;; This function is called when the user moves a scroll bar on one of the
;; scrolling menus.  The function changes the :starts index list so that the
;; :start of the scrolling menu will be recorded in the list.
;;
(defun BROWSER-MENU-SCROLL-FN (scroll-bar value)
  (let* ((menu (g-value scroll-bar :parent))
	 (browser (g-value menu :parent :parent))
	 (rank (+ (g-value menu :rank) (g-value browser :start))))
    (setf (nth rank (g-value browser :starts)) value)))


;; This function is called when the user moves the horizontal scroll bar
;; in order to scroll the items in the menus.  The function iterates over
;; the scrolling menus and sets the :start of each one according to the value
;; of its element in the :starts list.
;;
(defun BROWSER-SCROLL-FN (scroll-bar value)
  (declare (ignore value))
  (let* ((browser (g-value scroll-bar :parent)))
    (update-menus browser)))



(create-instance 'BROWSER-GADGET opal:aggregadget
   :declare ((:parameters :left :top :num-menus :num-rows :item-font
			  :title-font :menu-items-generating-function
			  :item-to-string-function :selection-function
			  :additional-selection-p
			  :additional-selection-coordinate
			  :additional-selection-function)
	     (:type ((integer 0) :num-menus :num-rows)
		    ((or function symbol) :menu-items-generating-function
		     :item-to-string-function :menu-function)
		    ((or null function symbol)
		     :additional-selection-function :selection-function)
		    (list :additional-selection-coordinate)
		    ((or (is-a-p opal:font) (is-a-p opal:font-from-file))
		     :font :title-font))
	     (:maybe-constant :left :top :num-menus :num-rows
			      :menu-items-generating-function :menu-function
			      :item-to-string-function :additional-selection-p
			      :item-font :title-font :visible))

   ;; Browser parameters
   (:left 0)
   (:top 0)
   (:num-menus 3)
   (:num-rows 5)
   (:menu-items-generating-function
    #'(lambda (arg)
	(declare (ignore arg))
	(error "You must supply a function in the :menu-items-generating-funciton
slot of your instance of the BROWSER-GADGET")))
   (:item-to-string-function #'identity)
   (:menu-function #'BROWSER-MENU-FN)

   ;; Additional selection parameters
   (:additional-selection-coordinate NIL)
   (:additional-selection-p T)
   (:additional-selection-function NIL)
   (:additional-selection
    (o-formula
     ; Returns the item appearing in the gray-selected scrolling-menu-item
     (let ((coordinate (gvl :additional-selection-coordinate)))
       (when coordinate
	 (let* ((real-menu-rank (car coordinate))
		(real-obj-rank (cadr coordinate)))
	   (nth real-obj-rank (nth real-menu-rank (gvl :all-items))))))))
   (:item-font opal:default-font)
   (:title-font (opal:get-standard-font NIL :italic NIL))
   (:selection-function NIL)

   ; Non-customizable slots
   (:items NIL)
   (:starts (o-formula (make-list (gvl :num-menus) :initial-element 0)))
   (:start (o-formula (if (gvl :scroll-p) (gvl :scroll-bar :value) 0)))
   (:scroll-p (o-formula (> (length (gvl :items)) (gvl :num-menus))))
   (:end (o-formula (+ (gvl :start) (gvl :num-menus))))
   (:visible-items (o-formula (subseq (gvl :items) (gvl :start) (gvl :end))))
   (:parts
    `((:menu-list ,opal:aggrelist
        (:constant (:direction :h-spacing :pixel-margin :rank-margin
		    :fixed-width-p :fixed-height-p))
        (:left ,(o-formula (gvl :parent :left)))
	(:top ,(o-formula (gvl :parent :top)))
	(:direction :horizontal)
	(:h-spacing 10)
	;; This complicated :width formula not only computes the width of the
	;; aggrelist but also sets the correct pixel position for the indicator
	;; box, according to the new width.
	(:width ,(o-formula
		  (let ((max-x 0))
		    (gv (first (gvl :components)) :items)
		    (opal:do-components KR::*schema-self*
		      #'(lambda (menu)
			  (setf max-x (MAX max-x (opal:gv-right menu)))))
		    (let* ((left (gvl :left))
			   (new-width (MAX 0 (- max-x left)))
			   (bound-left (+ left 40)) ; width of trill boxes = 40
			   (bound-right (- max-x 40))
			   (scroll-bar (g-value KR::*schema-self* :parent
						:scroll-bar))
			   (value (g-value scroll-bar :value))
			   (val-1 (g-value scroll-bar :val-1))
			   (val-2 (g-value scroll-bar :val-2))
			   (indicator (g-value scroll-bar :indicator))
			   (ind-width (g-value indicator :width))
			   (new-box (list (inter:clip-and-map value val-1 val-2
					         bound-left
						 (- bound-right ind-width))
					  0 0 0)))
		      (s-value scroll-bar :width new-width)
		      (s-value indicator :box new-box)
		      new-width))))
	(:items ,(o-formula (gvl :parent :num-menus)))
	(:item-prototype
	 (,SCROLLING-MENU
	  (:constant (:indicator-text-p :int-scroll-feedback-p :multiple-p
		      :int-scroll-feedback-p :scroll-on-left-p :scr-incr
		      :min-scroll-bar-width :scr-trill-p :page-trill-p
		      :indicator-font :min-frame-width :v-spacing :h-align
		      :int-menu-feedback-p :final-feedback-p :text-offset))
	  (:height ,(o-formula
		     (let ((new-height (+ (* (gvl :num-visible)
					     (gvl :item-height))
					  (gvl :title-height))))
		       (let* ((top (g-value KR::*schema-self* :top))
			      (bound-top (+ top 40)) ; height of boxes = 40
			      (bound-bottom (- (+ top new-height) 40))
			      (scroll-bar (g-value kr::*schema-self*
						   :scroll-bar))
			      (value (g-value scroll-bar :value))
			      (val-1 (g-value scroll-bar :val-1))
			      (val-2 (g-value scroll-bar :val-2))
			      (indicator (g-value scroll-bar :indicator))
			      (ind-height (g-value indicator :height))
			      (new-box 
			       (list 0 (inter:clip-and-map value val-1 val-2
					      bound-top
					      (- bound-bottom ind-height))
				     0 0)))
			 (s-value scroll-bar :height new-height)
			 (s-value indicator :box new-box)
			 new-height))))
	  (:num-visible ,(o-formula (gvl :parent :parent :num-rows) 5))
	  (:page-incr ,(o-formula (gvl :parent :parent :num-rows)))
	  (:indicator-text-p NIL)
	  (:int-scroll-feedback-p NIL)
	  (:scroll-selection-function BROWSER-MENU-SCROLL-FN)
	  (:multiple-p NIL)
	  (:min-frame-width 100)
	  (:item-to-string-function ,(o-formula (gvl :parent :parent
						     :item-to-string-function)))
	  (:item-font ,(o-formula (gvl :parent :parent :item-font)
				  opal:default-font))
	  (:title-font ,(o-formula (gvl :parent :parent :title-font)))
	  (:items ,(make-list 5))
	  (:title-item NIL)
	  (:title ,(o-formula
		    (let* ((item (gvl :title-item)))
		      (if item
			  (let* ((p (gvl :parent :parent))
				 (fn (gv p :item-to-string-function)))
			    (declare (ignore fn))
			    (kr-send p :item-to-string-function item))
			  ""))))
	  (:selected-ranks ,(o-formula (let ((p (gvl :parent :parent)))
					 (nth (+ (gvl :rank) (gv p :start))
					      (gv p :selected-ranks)))))
	  (:menu-selection-function ,(o-formula (gvl :parent :parent
						     :menu-function)))
	  (:parts
	   (:frame :feedback-obj :menu-title :menu-item-list
	     (:scroll-bar :modify
		(:height 120)))))))  ; the height of a menu with 5 items

      (:scroll-bar ,H-SCROLL-BAR
        (:constant (:min-height :val-1 :scr-trill-p :page-trill-p
		    :indicator-text-p :scr-incr :int-feedback-p
		    :format-string :indicator-font))
	(:left ,(o-formula (gvl :parent :left)))
	(:top ,(o-formula (+ (gvl :parent :top)
			     (gvl :parent :menu-list :height) 10)))
	(:width 380)
	(:val-2 ,(o-formula (MAX (- (length (gvl :parent :items))
				    (gvl :parent :num-menus))
				 1)))
	(:scroll-p ,(o-formula (gvl :parent :scroll-p)))
	(:indicator-text-p NIL)
	(:page-incr ,(o-formula (gvl :parent :num-menus)))
	(:int-scroll-feedback-p T)
	(:selection-function BROWSER-SCROLL-FN))

      (:gray-feedback ,opal:rectangle
       (:left ,(o-formula (+ 1 (gvl :obj-over :left))))
       (:top ,(o-formula (if (gvl :obj-over :prev)
			     (gvl :obj-over :top)
			     (+ 1 (gvl :obj-over :top)))))
       (:width ,(o-formula (- (gvl :obj-over :width) 2)))
       (:height ,(o-formula (if (and (gvl :obj-over :prev)
				     (gvl :obj-over :next))
				(gvl :obj-over :height)
				(- (gvl :obj-over :height) 1))))
       (:draw-function :xor)
       (:fast-redraw-p T)
       (:filling-style NIL)
       (:line-style ,(create-instance NIL opal:line-style
		        (:constant T)
		        (:line-thickness 4)
			(:stipple opal::gray-fill-bitmap)))
       (:visible ,(o-formula (gvl :obj-over)))
       (:obj-over
	,(o-formula
	  (let ((coordinate (gvl :parent :additional-selection-coordinate)))
	    (when coordinate
	      (let ((real-menu-rank (car coordinate))
		    (real-obj-rank (cadr coordinate))
		    (browser-start (gvl :parent :start)))
		;; When the coordinate indicates a visible position
		(when (and (>= real-menu-rank browser-start)
			   (< real-menu-rank (+ browser-start
						(gvl :parent :num-menus))))
		  (let* ((menu-rank (- real-menu-rank browser-start))
			 (menu (nth menu-rank
				    (gvl :parent :menu-list :components)))
			 (menu-start (gv menu :start)))
		    (when (and (>= real-obj-rank menu-start)
			       (< (+ menu-start (gv menu :num-visible))))
		      (nth (- real-obj-rank (gv menu :start))
			   (gv menu :menu-item-list :components))
		      )))))))))

      (:dummy-rect ,opal:rectangle
		   (:left ,(o-formula (progn (gvl :parent :menu-list :width)
					-1)))
		   (:top -1) (:width 0) (:height 0))))
   (:interactors
    `((:gray-inter ,inter:menu-interactor
       (:window ,(o-formula (gv-local :self :operates-on :window)))
       (:active ,(o-formula (gvl :operates-on :additional-selection-p)))
       (:start-where
	,(o-formula (list :leaf-element-of (gvl :operates-on)
			  :type SCROLLING-MENU-ITEM)))
       (:start-event :middledown)
       (:feedback-obj ,(o-formula (gvl :operates-on :gray-feedback)))
       (:start-action
	,#'(lambda (interactor first-obj-over)
	     (let ((browser (g-value interactor :operates-on))
		   (feedback-obj (g-value interactor :feedback-obj)))
	       (if (equal (g-value feedback-obj :obj-over) first-obj-over)
		   (s-value browser :additional-selection-coordinate NIL)
		   (let ((obj-rank (g-value first-obj-over :real-rank))
			 (menu-rank (+ (g-value first-obj-over :parent
						:parent :rank)
				       (g-value browser :start))))
		     (s-value browser :additional-selection-coordinate
			      (list menu-rank obj-rank)))))))
       (:running-action
	,#'(lambda (interactor prev-obj-over new-obj-over)
	     (declare (ignore prev-obj-over))
	     (let ((browser (g-value interactor :operates-on)))
	       (if new-obj-over
		   (let ((obj-rank (g-value new-obj-over :real-rank))
			 (menu-rank (+ (g-value new-obj-over :parent :parent
						:rank)
				       (g-value browser :start))))
		     (s-value browser :additional-selection-coordinate
			      (list menu-rank obj-rank)))))))
       (:stop-action
	; Compute the :additional-selection through the formula in case it has
	; just become deselected (send NIL instead of the item)
	,#'(lambda (interactor final-obj-over)
	     (declare (ignore final-obj-over))
	     (let ((browser (g-value interactor :operates-on)))
	       (kr-send (g-value interactor :operates-on)
			:additional-selection-function
			browser (g-value browser :additional-selection)))))))))


#|
(define-method :notice-items-changed BROWSER-GADGET
               (gadget &optional no-propagation)
  (declare (ignore no-propagation))
  (opal:notice-items-changed (g-value gadget :menu-list))
  (do ((components (g-value gadget :menu-list :components)
		   (cdr components)))
      ((null components) T)
    (opal:notice-items-changed (car components))))
|#