;;; -*- 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. ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;  Pull Down Menus
;;;
;;;
;;;  Customizable slots:
;;;    1)  Left, top
;;;    2)  Title-font, item-font
;;;    4)  Selection-function - a function to be executed when any item is
;;;           selected.  Takes the parameters (gadget menu-item submenu-item).
;;;    5)  Items - A list with the format
;;;           '(("m1" m1func ("m1,1"..."m1,N"))
;;;             ("m2" m2func (("m2,1" m2,1func)...("m2,N" m2,Nfunc)))
;;;             ...)
;;;           Where "mN" is a string or atom that is the title of a menu,
;;;        "mX,Y" is a string or atom in menu X, row Y,
;;;        mNfunc is executed when any item in menu N is selected,
;;;        mX,Yfunc is executed when item mX,Y is selected.
;;;           These two functions take the same parameters as the
;;;        selection-function.
;;;
;;;  Programming interface (Garnet way):
;;;    1) Create an instance of menubar with a value for the :items slot
;;;    2) Use opal:add-component to put the instance in a window
;;;
;;;  Caveats:
;;;     New bar-items should be created with the :enable slot set to NIL in
;;;  order to keep their windows from being updated before they are added to
;;;  a menubar.

;;;  Pull Down Menus Demo:
;;;    The function below creates a window containing a pull down menu and
;;;    some text objects.  Choosing an item from the pull-down menu will
;;;    change the font of the text objects.
;;;    To run it, enter (GARNET-GADGETS:menubar-go).
;;;    To stop, enter (GARNET-GADGETS:menubar-stop).
;;;
;;;  Written by Pavan Reddy and Andrew Mickish

;;;
;;; CHANGE LOG:
;;;
;;; 06/22/93  Andrew Mickish - Set :rank slot in :add-item methods
;;; 06/15/93  Andrew Mickish - Called Check-Bar-Item in Make-Bar-Item;
;;;             Added :do-not-dump-slots for BAR-ITEM; Used g-local-value
;;;             instead of g-value when referencing :submenu-window slots
;;;             Added :fix-update-slots method for BAR-ITEM
;;; 06/11/93  Andrew Mickish - Removed references to :number-of-comps
;;; 05/13/93  Andrew Mickish - :prev-visible ---> :prev
;;; 04/23/93  Andrew Mickish - Added :do-not-dump-slots
;;; 04/18/93  Andrew Mickish - Removed ,@ from BAR-ITEM's :add-item method
;;; 04/13/93  Andrew Mickish - Fixed bugs in change of 03/08/93
;;; 03/17/93  Brad Myers - add special-popdown functions for gilt, etc.
;;; 03/08/93  Andrew Mickish - Fixed :left and :top formulas of submenu-windows
;;;             so that offset is correct when menubar is in a subwindow;
;;;             Type check :items with Check-Menubar-Items
;;; 03/04/93  Andrew Mickish - Made :string-set-func work with BAR-ITEMs
;;; 02/24/93  Andrew Mickish - Added :string-set-func
;;; 02/23/93  Andrew Mickish - Eliminated need to call notice-items-changed
;;;             on the menubar (but apparently must be called internally on
;;;             new submenus... Argh!)
;;; 02/08/93  Andrew Mickish - :notice-items-changed ---> :fix-update-slots
;;; 01/07/93  Rajan Parthasarathy - Converted the exported functions
;;;             to methods.  This way, you can call the same "function"
;;;             on both this menubar and the motif-menubar.  Also fixed
;;;             two bugs.
;;; 12/14/92  Andrew Mickish - Added type and parameter declarations
;;; 09/25/92  Andrew Mickish - Removed *** HACK ***'s because fixed update
;;; 09/21/92  Andrew Mickish - Now all submenu window bookkeeping is done in
;;;             MENUBAR's :submenu-window-list instead of interactor's :window.
;;; 08/24/92  Andrew Mickish - Added update calls to get around update bug
;;;             (see *** HACK *** comments)
;;; 07/22/92  Brad Vander Zanden - Changed two nconc's to append's
;;; 06/26/92  Andrew Mickish - Rewrote :notice-items-changed, :add-item, and
;;;             :remove-item methods so objects can appear in the :items list
;;; 06/22/92  Ed Pervin - It is necessary to call notice-items-changed on
;;;		menubars during the execution of opal:reconnect-garnet.
;;; 05/22/92  Brad Myers - new way to use menu-interactor that uses multiple
;;;                        windows.
;;; 04/15/92  Andrew Mickish - Gave BAR-ITEM text a white line style and put
;;;             it on top of the "cover"
;;; 04/14/92  Andrew Mickish - Added schema-p check in final-function so you
;;;             can destroy the menubar in an item function
;;; 02/19/92  Andrew Mickish - Fixed reuse of old bar-items by adding a push
;;;             in the remove-item method and a pop in the add-item method
;;; 06/10/91  Ed Pervin - Call opal:raise-window on subwindows so
;;;		they won't be covered by main window in twm.
;;; 05/15/91  Andrew Mickish - Added defvar's
;;; 05/01/91  Andrew Mickish - Put in Garnet-Gadgets package


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

(export '(MENUBAR BAR-ITEM SUBMENU SUBMENU-ITEM MAKE-SUBMENU-WIN

	  ; Creation Functions
	  Make-Menubar Make-Bar-Item Make-Submenu-Item))

; Demo things
#+garnet-debug
(export '(MENUBAR-GO MENUBAR-STOP DEMO-MENUBAR MENUBAR-WIN MENUBAR-TOP-AGG))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; First function is to display the submenu from gilt, etc.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Returns the new window if and only if a different bar-item is to be
;; displayed.  Gadget parameter is the top level menubar.
(defun Garnet-Menubar-popup-item (gadget)
  (let (bar-item)
    ;; first, find the MOTIF-BAR-ITEM that is under the mouse
    (setq bar-item (opal:point-to-component gadget
				      (inter:event-x inter:*current-event*)
				      (inter:event-y inter:*current-event*)))
    (when bar-item
      (DoSpecialPopUpMenubar gadget bar-item))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


(create-instance 'MENUBAR-TEXT-LABEL-PROTOTYPE opal:text
  (:left (o-formula
	  (case (gvl :parent :h-align)
	    (:left (+ (gvl :parent :left) (gvl :parent :text-offset)))
	    (:center (opal:gv-center-x-is-center-of (gvl :parent)))
	    (:right (- (+ (gvl :parent :left) (gvl :parent :width))
		       (gvl :width) (gvl :parent :text-offset))))))
  (:top (o-formula (+ (gvl :parent :top) (gvl :parent :text-offset))))
  (:string (o-formula (gvl :parent :string)))
  (:font (o-formula (gvl :parent :font))))


(defun Menubar-Get-Label (agg)
  (let ((alist (g-value agg :parent))
	(item-guess (g-value agg :item-guess)))
    (if (or alist item-guess)
	(let ((item (if alist
			(nth (g-value agg :rank)
			     (g-value alist :items))
			;; Hack to create stand-alone submenu-items
			item-guess
			))
	      ;; Have to have a globally defined text-label-prototype (i.e.,
	      ;; not a slot in the SUBMENU) because submenu components can be
	      ;; created stand-alone (i.e., unable to reference the SUBMENU).
	      (text-label-prototype MENUBAR-TEXT-LABEL-PROTOTYPE))
	  ;; Don't forget that item functions are allowed!
	  (if (consp item) (setq item (first item)))
	  ;; Don't forget that menus have item conversion functions!
	  (if (and alist (g-value alist :item-to-string-function))
	      (setf item (kr-send alist :item-to-string-function item)))
	  (cond
	    ((schema-p item)
	     (let ((new-label (if (g-value item :parent)
				  ;; The item has been used already --
				  ;; Use it as a prototype
				  (create-instance NIL item)
				  ;; Use the item itself
				  item))
		   (leftform (get-value text-label-prototype :left))
		   (topform (get-value text-label-prototype :top)))
	       ;; Automatically set the :left and :top of the label
	       (unless (is-a-p (get-local-value item :left) leftform)
		 (s-value new-label :left (formula leftform)))
	       (unless (is-a-p (get-local-value item :top) topform)
		 (s-value new-label :top (formula topform)))
	       new-label))
	    (t (create-instance NIL text-label-prototype))))
	;; Give the item-prototype a bogus part
	(create-instance NIL opal:null-object))))


;; This is an item in the vertical menus that appear below the menu bar.
;;
(create-instance 'SUBMENU-ITEM opal:aggregadget
   ; The mnemonic ":desc" is a description of a submenu.  The top-level :items
   ; slot is a list of desc's.
   (:desc (o-formula (nth (gvl :rank) (gvl :parent :items))))
   (:item-obj (o-formula (first (gvl :desc))))
   (:action (o-formula (second (gvl :desc))))
   (:string (o-formula (let ((item-obj (gvl :item-obj)))
			 (if (stringp item-obj)
			     item-obj
			     (string-capitalize
			      (string-trim ":" item-obj))))))
   (:text-offset (o-formula (gvl :parent :text-offset)))
   (:enabled (o-formula (if (= 4 (length (gvl :desc)))
			    (fourth (gvl :desc)) T)))
   (:font (o-formula (gvl :parent :font)))
   (:text-offset2 (o-formula (gvl :parent :text-offset2)))
   (:h-align (o-formula (gvl :parent :h-align)))
   (:max-text-width-thus-far
    (o-formula (if (gvl :prev)
		      (MAX (gvl :prev :max-text-width-thus-far)
			   (gvl :text :width))
		      (gvl :text :width))))
   (:height (o-formula (+ (gvl :text :height) (gvl :text-offset2))))
   (:width (o-formula (+ (gvl :parent :tail :max-text-width-thus-far)
			      (gvl :text-offset2))))
   ;; parent is a submenu
   (:bar-item (o-formula (gvl :parent :bar-item)))
   (:parts
    `((:text ,#'Menubar-Get-Label)
      (:cover ,opal:rectangle
	     (:left ,(o-formula (gvl :parent :text :left)))
	     (:top ,(o-formula (gvl :parent :text :top)))
	     (:height ,(o-formula (- (gvl :parent :height)
				     (gvl :parent :text-offset2))))
	     (:width ,(o-formula (- (gvl :parent :width)
				    (gvl :parent :text-offset2))))
	     (:visible ,(o-formula (or (not (gvl :parent :enabled))
				       (gvl :parent :interim-selected))))
	     (:draw-function ,(o-formula (if (gvl :parent :enabled)
					    :xor :and)))
	     (:line-style NIL)
	     (:filling-style ,(o-formula (if (gvl :parent :enabled)
					    opal:black-fill
					    opal:gray-fill)))))))


;; Menu that pops up when you click on the menubar
;; A window for this object is created in Attach-Submenu
;;
(create-instance 'SUBMENU opal:aggrelist
   (:left 0)
   (:top 0)
   (:height (o-formula (let ((total-height 0))
			 (dolist (c (gvl :components))
			   (incf total-height (g-value c :height)))
			 total-height)))
   (:h-align :left)
   (:text-offset 5)
   (:text-offset2 6)
   (:v-spacing 0)
   (:font NIL)  ; Set during fix-update-slots or add-item
   (:my-baritem NIL)
   (:enabled-items
    (o-formula (let ((enabled-items NIL))
		 (dovalues (component :self :components
				      :in-formula T
				      :result enabled-items)
		    (when (gv component :enabled)
		      (push component enabled-items))))))
   (:items (o-formula (gvl :bar-item :items)))
   (:item-prototype submenu-item)
   )

;; Returns a window that an instance of SUBMENU can be put into
;;
(defun MAKE-SUBMENU-WIN (a-bar-item a-submenu)
  (create-instance NIL inter:interactor-window
     (:bar-item a-bar-item)
     (:aggregate a-submenu)
     (:omit-title-bar-p T)
     (:save-under T)         
     (:double-buffered-p T)  
     (:visible NIL)
     (:height (o-formula (max 1 (gvl :aggregate :height))))
     (:width (o-formula
	      (max 1
		   (+ (gvl :aggregate :text-offset) (gvl :aggregate :width)))))
     (:left (o-formula
	     (let* ((bar-item (gvl :bar-item))
		    (win (gv bar-item :window))
		    (left-offset 0))
	       (cond
		 (win
		  (do ((parent (gv win :parent) (gv parent :parent)))
		      ((null parent) left-offset)
		    (setf left-offset
			  (+ left-offset (gv parent :left)
			     (or (gv parent :left-border-width) 0))))
		  (+ (gv bar-item :left) (gv win :left)
		     (gv win :left-border-width) left-offset))
		 (t 0)))))
     (:top (o-formula
	    (let* ((bar-item (gvl :bar-item))
		   (win (gv bar-item :window))
		   (top-offset 0))
	       (cond
		 (win
		  (do ((parent (gv win :parent) (gv parent :parent)))
		      ((null parent) top-offset)
		    (setf top-offset
			  (+ top-offset (gv parent :top)
			     (or (gv parent :top-border-width) 0))))
		  (+ (gv bar-item :top) (gv bar-item :height) (gv win :top)
		     (gv win :top-border-width) top-offset))
		 (t 0)))))
     ))


;; This object is a text field in the menu bar covered by a black rectangle.
;; An aggrelist of these items makes up the menu bar.
;;
(create-instance 'BAR-ITEM opal:aggregadget
   (:width (o-formula (+ (* 2 (gvl :spacing)) (gvl :text :width))))
   (:height (o-formula (gvl :text :height)))
   ; The mnemonic ":desc" is a description of a submenu.  The top-level :items
   ; slot is a list of desc's.
  
   (:desc (o-formula (nth (gvl :rank) (gvl :parent :items))))
   (:menu-obj (o-formula (first (gvl :desc))))
   (:action (o-formula (second (gvl :desc))))
   (:items (o-formula (third (gvl :desc))))
   (:string (o-formula (let ((menu-obj (gvl :menu-obj)))
			 (if (stringp menu-obj)
			     menu-obj
			     (string-capitalize
			      (string-trim ":" menu-obj))))))
   (:font (o-formula (gvl :parent :title-font)))
   (:spacing 5)
   (:enabled (o-formula (if (= 4 (length (gvl :desc)))
			    (fourth (gvl :desc))
			    T)))
   ;; slot :submenu filled by :fix-update-slots with a SUBMENU
   ;; slot :submenu-window set with the window of the submenu

   (:parts
    `((:cover ,opal:rectangle
	     (:left ,(o-formula (gvl :parent :left)))
	     (:top ,(o-formula (gvl :parent :top)))
             (:width ,(o-formula (gvl :parent :width)))
             (:height ,(o-formula (gvl :parent :height)))
	     (:line-style NIL)
	     (:filling-style ,(o-formula
			       (if (gvl :parent :enabled)
				   (if (gvl :parent :interim-selected)
				       opal:white-fill
				       opal:black-fill)
				   opal:gray-fill))))
      (:text ,opal:text
       (:left ,(o-formula (+ (gvl :parent :left) (gvl :parent :spacing))))
       (:top ,(o-formula (gvl :parent :top)))
       (:font ,(o-formula (gvl :parent :font)))
       (:string ,(o-formula (string (gvl :parent :string))))
       (:line-style ,(o-formula (if (gvl :parent :interim-selected)
				    opal:default-line-style
				    opal:white-line)))))))


(create-instance 'MENUBAR opal:aggrelist
   :declare ((:parameters :left :top :items :title-font :item-font
			  :selection-function)
	     (:type ((satisfies Check-Menubar-Items) :items)
		    ((or (is-a-p opal:font) (is-a-p opal:font-from-file))
			 :title-font :item-font)
		    ((or null function symbol) :selection-function)))
   ; Customizable slots
   (:left 0)(:top 0)
   (:items NIL)
   (:title-font opal:default-font)
   (:item-font opal:default-font)

   ; Internal slots
   (:h-spacing 0)
   (:direction :horizontal)
   (:item-prototype BAR-ITEM)
   (:selection-function NIL)
   (:enabled-items
    (o-formula (let ((enabled-items NIL))
		 (dovalues (component :self :components
				      :in-formula T
				      :result enabled-items)
		    (when (gv component :enabled)
		      (push component enabled-items))))))
   (:list-of-all-enabled-objects
    (o-formula (let ((l (copy-list (gvl :enabled-items))))
		 (dolist (baritems (gvl :enabled-items))
		   (setf l (append l (copy-list
				     (gv-local baritems :submenu :enabled-items)))))
		 l)))
   (:right (o-formula (+ (gvl :left) (gvl :width))))
   (:destroy-me 'MENUBAR-DESTROY)
   (:special-popup-func 'Garnet-Menubar-popup-item)
   (:special-popdown-func 'Put-Down-MenuBar-Popups)

   (:old-visible T)

   ;; slot :submenu-window-list set with list of all windows being used.
   (:interactors
    `((:menubar-select ,inter:menu-interactor
       (:active ,(o-formula (gv-local :self :operates-on :window)))
       (:window ,(o-formula
		  (let ((main-win (gv-local :self :operates-on :window))
			(submenu-wins (gv-local :self :operates-on
						:submenu-window-list)))
		    (if main-win
			(if submenu-wins
			    (cons main-win (copy-list submenu-wins))
			    main-win)))
		  ))
       (:start-where ,(o-formula (list :list-element-of
				       (gvl :operates-on)
				       :list-of-all-enabled-objects)))
       (:outside NIL)
       (:start-action
	,#'(lambda (inter obj)
	     (call-prototype-method inter obj)
	     (s-value (g-value inter :operates-on) :*bar-item-popped-up NIL)))
       (:outside-action
	,#'(lambda (inter outside prev)
	     (call-prototype-method inter outside prev)
	     ;; make sure the top level bar items stay selected since
	     ;; sub-menus are showing
	     (when (is-a-p prev BAR-ITEM)
	       (s-value prev :interim-selected T))))
       (:running-action
	,#'(lambda (inter prev new)
	     (let* ((prev-baritem (g-value inter :prev-baritem))
		    (new-is-bar (is-a-p new BAR-ITEM))
		    (new-baritem (when new
				   (if new-is-bar
				     new
				     ;; else is a sub-item, get its bar-item
				     (g-local-value new :bar-item)))))
	       (call-prototype-method inter prev new)
	       ;; keep the interim selected of the bar item so it
	       ;; shows up as highlighted when items in sub-menu selected.
	       (if new-baritem
		   ;; this makes the subwindow NOT go away when move
		   ;; outside subwindow
		   (unless (eq new-baritem prev-baritem)
		     (s-value inter :prev-baritem new-baritem)
		     (if prev-baritem
			 (let ((win (g-local-value prev-baritem :submenu-window)))
			   (s-value prev-baritem :interim-selected NIL)
			   (s-value win :visible NIL)
			   (opal:update win)))
		     (if new-baritem
			 (let ((win (g-local-value new-baritem :submenu-window)))
			   (s-value win :visible T)
			   (opal:raise-window win)))))  ;; Call update
	       ;;; this needs to be after the call-prototype-method,
	       ;;; since that is where the :interim-selected will be
	       ;;; turned off.
	       (unless new-is-bar
		 (when new-baritem
		   (s-value new-baritem :interim-selected T))))
	     ))
       (:abort-action
	,#'(lambda (inter last)
	     (declare (ignore last))
	     (let ((prev-baritem (g-value inter :prev-baritem)))
	       (when prev-baritem
		 (s-value prev-baritem :interim-selected NIL)
		 (let ((win (g-local-value prev-baritem :submenu-window)))
		   (s-value inter :prev-baritem NIL)
		   (s-value win :visible NIL)
		   (opal:update win))))))
       (:final-function
	,#'(lambda (inter obj)
	     (let* ((is-bar (is-a-p obj BAR-ITEM))
		    (baritem (if is-bar obj
				 ;; else is a sub-item, get its bar-item
				 (g-value obj :bar-item)))
		    (bar-action (g-value baritem :action))
		    (bar-obj (g-value baritem :menu-obj))
		    (item-action (unless is-bar (g-value obj :action)))
		    (item-obj (unless is-bar (g-value obj :item-obj)))
		    (gadget (g-value inter :operates-on))
		    (prev-baritem (g-value inter :prev-baritem))
		    )
	       (when baritem
		 (s-value baritem :interim-selected NIL)
		 (let ((win (g-local-value baritem :submenu-window)))
		   (when win
		     (s-value win :visible NIL)
		     (opal:update win))))
	       (when prev-baritem
		 (s-value inter :prev-baritem NIL))
	       (if bar-action
		   (funcall bar-action gadget bar-obj item-obj))
	       ;; The schema-p check allows you to destroy the menubar gadget
	       ;; in the menu-action or item-action and avoid passing the
	       ;; destroyed object to KR-SEND
	       (if (and item-action
			(schema-p gadget))
		   (funcall item-action gadget bar-obj item-obj))
	       (if (schema-p gadget)
		   (kr-send gadget :selection-function
			    gadget bar-obj item-obj)))))
       ))))

(s-value MENUBAR :do-not-dump-slots
	 (append '(:submenu-window-list)
		 (g-value MENUBAR :do-not-dump-slots)))
(s-value BAR-ITEM :do-not-dump-slots
	 (append '(:submenu :submenu-window)
		 (g-value BAR-ITEM :do-not-dump-slots)))
(s-value SUBMENU :do-not-dump-slots
	 (append '(:bar-item)
		 (g-value BAR-ITEM :do-not-dump-slots)))
(let ((menubar-select (g-value MENUBAR :menubar-select)))
  (s-value menubar-select :do-not-dump-slots
	   (append '(:prev-baritem)
		   (g-value menubar-select :do-not-dump-slots))))


;;; Auxiliary function for MENUBAR's :fix-update-slots method
;;;    This function is used to establish the links between a bar-item and its
;;; submenu.  A new submenu is created and put in a new window.  This new
;;; window is added to the :submenu-window-list slot of the interactor in the
;;  top-level menubar.
(defun ATTACH-SUBMENU (a-menubar a-bar-item)
  ; a-bar-item might have been taken from storage, so check to see whether
  ; it has a :submenu and :submenu-window before creating these
  (let* ((new-submenu (or (g-local-value a-bar-item :submenu)
			  (create-instance NIL SUBMENU
			     (:bar-item a-bar-item)
			     (:font (g-value a-menubar :item-font)))))
	 (win (or (g-local-value a-bar-item :submenu-window)
		  (Make-Submenu-Win a-bar-item new-submenu))))
      ; if a-bar-item was taken from storage, then its :submenu-window is
      ; already on the :submenu-window-list
      (s-value a-menubar :submenu-window-list
	       (append (list win)
		       (g-local-value a-menubar :submenu-window-list)))
      ; bookkeeping in case a-bar-item was not taken from storage
      (s-value a-bar-item :submenu new-submenu)
      (s-value a-bar-item :submenu-window win))
  a-bar-item)


;;;    This should be called when the number of items in the menu is changed
;;; manually (without calling add-item or remove-item).  This is basically an
;;; enhanced version of the default fix-update-slots which reuses old
;;; components.
;;;    When the menubar instance is created, aggrelists creates components
;;; for it, but this function has to be called to create submenus for the
;;; components.  If the number of :items changes, then this function should be
;;; called to both create (or destroy) new components for the aggrelist and
;;; create (or destroy) corresponding submenus.
;;;
(define-method :fix-update-slots MENUBAR (a-menubar)
  ;; will be called when :items or :visible change
  (let ((old-visible (g-value a-menubar :old-visible))
	(new-visible (g-value a-menubar :visible)))
    (if (eq old-visible new-visible)
	;; then items must have changed -- Generate new bar-items
	(call-prototype-method a-menubar)
	(progn ;; else visible changed
	  (s-value a-menubar :old-visible new-visible)
	  (unless new-visible
	    (Put-Down-MenuBar-Popups a-menubar))))))

(define-method :fix-update-slots BAR-ITEM (a-bar-item)
  (let ((a-menubar (g-value a-bar-item :parent)))
    (unless (member (g-local-value a-bar-item :submenu-window)
		    (g-local-value a-menubar :submenu-window-list))
      ;; Generate new submenu window for the bar-item
      (ATTACH-SUBMENU a-menubar a-bar-item))))



;;;
;;;  DEMO FUNCTIONS
;;;


#+garnet-debug (defparameter *FONT-TO-SWAP* (create-instance NIL opal:font))
#+garnet-debug (defvar family-text NIL)
#+garnet-debug (defvar face-text NIL)
#+garnet-debug (defvar size-text NIL)
#+garnet-debug (defvar combo-text NIL)


;;; When we want to change an object's font, set the slots of *FONT-TO-SWAP*,
;;; then set the object to have that font.  (Opal does not notice when you
;;; just change the slots of a font.)
;;;
#+garnet-debug
(defun Change-Font (text-obj &key family face size)
  (let ((old-font (g-value text-obj :font))
	(new-font *FONT-TO-SWAP*))
    (setf *FONT-TO-SWAP* old-font)
    (if family
	(s-value new-font :family family)
	(s-value new-font :family :fixed))
    (if face
	(s-value new-font :face face)
	(s-value new-font :face :roman))
    (if size
	(s-value new-font :size size)
	(s-value new-font :size :medium))
    (s-value text-obj :font new-font)))


;;; Some functions to call when items are selected
;;;
#+garnet-debug
(defun Family-Fn (gadget slot value)
  (declare (ignore gadget))
  (change-font family-text slot value)
  (s-value family-text :string (string-downcase value)))
#+garnet-debug
(defun Face-Fn (gadget slot value)
  (declare (ignore gadget))
  (change-font face-text slot value)
  (s-value face-text :string (string-downcase value)))
#+garnet-debug
(defun Size-Fn (gadget slot value)
  (declare (ignore gadget))
  (change-font size-text slot value)
  (s-value size-text :string (string-downcase value)))

#+garnet-debug
(defun Fixed-Fn (gadget slot value)
  (declare (ignore gadget slot value))
  (format t "Setting :family slot to :fixed.~%"))
#+garnet-debug
(defun Serif-Fn (gadget slot value)
  (declare (ignore gadget slot value))
  (format t "Setting :family slot to :serif.~%"))
#+garnet-debug
(defun Sans-Serif-Fn (gadget slot value)
  (declare (ignore gadget slot value))
  (format t "Setting :family slot to :sans-serif.~%"))


#+garnet-debug
(defun Menubar-Go (&key dont-enter-main-event-loop)

  (create-instance 'MENUBAR-WIN inter:interactor-window
     (:top 5)(:left 700)(:height 360)(:width 300)
     (:aggregate (create-instance 'MENUBAR-TOP-AGG opal:aggregate)))

  (create-instance 'DEMO-MENUBAR MENUBAR
     (:items
      '((:family family-fn
	 ((:fixed fixed-fn)(:serif serif-fn)(:sans-serif sans-serif-fn)))
	(:face face-fn
	 ((:roman)(:bold)(:italic)(:bold-italic)))
	(:size size-fn
	 ((:small)(:medium)(:large)(:very-large)))))
     (:selection-function
      #'(lambda (gadget slot value)
	  (declare (ignore gadget slot value))
	  (let ((family (g-value family-text :font :family))
		(face (g-value face-text :font :face))
		(size (g-value size-text :font :size)))
	    (change-font combo-text
			 :family family :face face :size size)))))

  (create-instance 'menubar-special-popup-inter inter:button-interactor
    (:start-event #\p)
    (:start-where (list :in DEMO-MENUBAR))
    (:continuous NIL)
    (:window MENUBAR-WIN)
    (:final-function #'(lambda (inter obj)
			 (declare (ignore inter obj))
			 (kr-send DEMO-MENUBAR :special-popup-func
				  DEMO-MENUBAR))))
  
  (opal:add-component MENUBAR-TOP-AGG DEMO-MENUBAR)

  (create-instance 'family-text opal:text
     (:left 10)
     (:top 200)
     (:string "fixed")
     (:font (create-instance NIL opal:font)))
  (create-instance 'face-text opal:text
     (:left 75)
     (:top 200)
     (:string "roman")
     (:font (create-instance NIL opal:font)))
  (create-instance 'size-text opal:text
     (:left 160)
     (:top 200)
     (:string "medium")
     (:font (create-instance NIL opal:font)))
  (create-instance 'combo-text opal:text
     (:left 75)
     (:top 230)
     (:string "combo")
     (:font (create-instance NIL opal:font)))

  (opal:add-components MENUBAR-TOP-AGG
		       family-text face-text size-text combo-text)
  
  (opal:update MENUBAR-WIN)
 (unless dont-enter-main-event-loop #-cmu (inter:main-event-loop))
 )



;;;
;;;  MENUBAR-STOP
;;;

#+garnet-debug
(defun Menubar-Stop ()
  (opal:destroy MENUBAR-WIN))


;;;
;;;  UTILITY FUNCTIONS USED BY EXPORTED FUNCTIONS
;;;

(defun Confirm-Menubar (a-menubar)
  (unless (is-a-p a-menubar MENUBAR)
    (error "~S is not an instance of ~S.~%" a-menubar MENUBAR))
  T)

(defun Confirm-Bar-Item (a-bar-item)
  (unless (is-a-p a-bar-item BAR-ITEM)
    (error "~S is not an instance of ~S.~%" a-bar-item BAR-ITEM))
  T)


(defun Confirm-Submenu-Item (a-submenu-item)
  (unless (is-a-p a-submenu-item SUBMENU-ITEM)
    (error "~S is not an instance of ~S.~%" a-submenu-item SUBMENU-ITEM))
  T)

(defun Confirm-Bar-or-Submenu-Item (menubar-component)
  (unless (or (is-a-p menubar-component BAR-ITEM)
	      (is-a-p menubar-component SUBMENU-ITEM))
    (error "~S is not an instance of ~S or ~S.~%" menubar-component
	   BAR-ITEM SUBMENU-ITEM))
  T)


;;;
;;;  EXPORTED FUNCTIONS
;;;

; Menubar Functions


;
; The parameter item may be either 
; 1) An instance of BAR-ITEM, or
; 2) A sublist of an :items list
;
; Locator should be a sublist of an :items list or an installed bar-item.
; Or, you can use the key feature of add-item and make locator the title of
; an installed bar-item:
;    (add-item demo-menubar new-bar :after "Bar2" :key #'car)
;
; Implementation note:  The reason that we do not just set the :items list
; and let :fix-update-slots deal with it is that the item parameter can be an
; actual component to be added, so you should not generate a new component via
; :fix-update-slots.
;
(define-method :add-item MENUBAR (a-menubar item &rest args)
  (let* ((a-bar-item (if (is-a-p item BAR-ITEM)
			 item
			 (or (let ((old-bi (pop (g-value a-menubar :storage))))
			       (when old-bi
				 (g-value old-bi :desc)  ; initialize formula
				 (s-value old-bi :desc item)
				 old-bi))
			     (make-bar-item :desc item))))
	 (parent (g-value a-bar-item :parent)) ; parent should be NIL
	 where locator key)

    (when parent
      (error "~S is already installed in ~S.~%" a-bar-item parent))
    
    (multiple-value-setq (where locator key) (opal::get-wheres args))
    
    ; Add the bar-item as a component of the menubar
    (let* ((locator-comp (if (is-a-p locator BAR-ITEM)
			     locator
			     (get-bar-component a-menubar locator)))
	   (items (copy-list (g-value a-menubar :items)))
	   (desc (g-value a-bar-item :desc)))
      
      (opal:add-local-component a-menubar a-bar-item where locator-comp)
      (s-value a-bar-item :rank
	       (position a-bar-item (g-value a-menubar :components)))
      (s-value a-menubar :old-items
	       (s-value a-menubar
			:items
			(opal::insert-item desc items where locator key)))
      )

    ; Do additional bookkeeping that attaches the bar-item to the menubar
    (let* ((win (g-local-value a-bar-item :submenu-window))
	   (top-inter (g-value a-menubar :menubar-select))
	   (cur-wins (g-value top-inter :window)))
      ; Make sure win is destroyed along with a-menubar
      (unless (member win (g-local-value a-menubar :submenu-window-list))      
	(s-value a-menubar :submenu-window-list
		 (append (list win) (g-local-value a-menubar :submenu-window-list))))
      ; Add win to the top-level interactor's :window slot
      (if (listp cur-wins)
	  (append (list win) (g-value top-inter :window))
	  (s-value top-inter :window (list win cur-wins)))
      (mark-as-changed top-inter :window))
    a-bar-item))


;
;
; The a-bar-item parameter can either be
; 1) An instance of BAR-ITEM, or
; 2) A sublist of the :items list
;
(define-method :remove-item MENUBAR (a-menubar item)
  (let ((a-bar-item (if (is-a-p item BAR-ITEM)
			item
			(get-bar-component a-menubar item))))
    (unless (and a-bar-item
		 (eq a-menubar (g-value a-bar-item :parent)))
      (error "~S does not have ~S as its menubar.~%"
	     a-bar-item a-menubar))
      
    ; Remove the bar-item from the menubar
    (s-value a-menubar :submenu-window-list
	     (remove (g-local-value a-bar-item :submenu-window)
		     (g-local-value a-menubar :submenu-window-list)))
    
    (opal:remove-local-component a-menubar a-bar-item)
    (unless (eq a-bar-item item)
      (push a-bar-item (g-value a-menubar :storage)))
      
    ; Change the top-level :items list
    (let ((old-desc (if (is-a-p a-bar-item BAR-ITEM)
			(g-value a-bar-item :desc)
			a-bar-item)))
      (s-value a-menubar :old-items
	 (s-value a-menubar :items (remove old-desc (g-value a-menubar :items)
					   :test #'equal))))))

(defun Make-Menubar ()
  (create-instance NIL menubar))

(define-method :Menubar-Components MENUBAR (a-menubar)
   (g-value a-menubar :components))

(define-method :Set-Menubar MENUBAR (a-menubar new-menus)
  ; Can't use dovalues and destructive operation on :components together
  (let ((components (copy-list (Menubar-Components a-menubar))))
    (dolist (old-comp components)
      (opal:remove-item a-menubar old-comp))
    (dolist (new-menu new-menus)
      (opal:add-item a-menubar new-menu))))



; Bar-Item functions

;
; The item parameter can either be
; 1) An instance of SUBMENU-ITEM, or
; 2) A description of a submenu-item: (list string action) where "string" is
;    a string or atom and "action" is a function
;
(define-method :add-item BAR-ITEM (a-bar-item item &rest args)
  (let ((a-menubar (g-value a-bar-item :parent))
	(submenu (g-value a-bar-item :submenu))
	(old-desc (or (g-local-value a-bar-item :desc)
		      (copy-list (g-value a-bar-item :desc))))
	where locator key)
    (multiple-value-setq (where locator key) (opal::get-wheres args))

    ;; item can be either a submenu-item or a list
    (if (schema-p item)
	(let* ((new-item `(,(g-value item :item-obj)
			   ,(g-value item :action)))
	       (new-sub-desc (opal::insert-item new-item (third old-desc)
						where locator key))
	       (new-desc (list (first old-desc) (second old-desc) new-sub-desc))
	       (rank (s-value item :rank
			      (position new-item new-sub-desc))))
	    (if a-menubar
		(s-value a-menubar :items
			 (substitute new-desc old-desc
			     (or (g-local-value a-menubar :items)
				 (copy-list (g-value a-menubar :items)))
			     :test #'equal))
		(s-value a-bar-item :desc new-desc))
	    (s-value submenu :old-items new-sub-desc)
    	    (opal:add-local-component submenu item :at rank))

	(let* ((new-sub-desc (opal::insert-item item (third old-desc)
						where locator key))
	       (new-desc (list (first old-desc) (second old-desc)
			       new-sub-desc))
	       (rank (position item new-sub-desc
			       :test #'(lambda (x y)
					 (equal x (funcall key y))))))
	    (if a-menubar
		(s-value a-menubar
		   :items
		   (substitute new-desc old-desc
			       (or (g-local-value a-menubar :items)
				   (copy-list (g-value a-menubar :items)))
			       :test #'equal))
		(s-value a-bar-item :desc new-desc))
	    (s-value submenu :old-items new-sub-desc)
	    (opal::Add-The-Component submenu rank)))))



;
;
; The item parameter can be either
; 1) An instance of SUBMENU-ITEM, or
; 2) A string or atom
;
(define-method :remove-item BAR-ITEM (a-bar-item &optional item
						 &key (key #'opal:no-func))
  (let* ((a-submenu-item (if (is-a-p item SUBMENU-ITEM)
			     item
			     (get-submenu-component a-bar-item item)))
	 (submenu (g-value a-bar-item :submenu))
	 (submenu-components (g-value submenu :components))
	 (rank (if item
		   (position a-submenu-item submenu-components
			     :test #'(lambda (x y)
				       (equal x (funcall key y))))
		   (1- (length submenu-components)))))

    (unless rank
      (error "~S does not have ~S as its bar-item.~%"
	     a-submenu-item a-bar-item))
    ; If the user did not supply an item, then just remove the last component
    (unless a-submenu-item
      (setf a-submenu-item (nth rank submenu-components)))
    ; Remove the submenu-item from the bar-item
    (opal:remove-local-component submenu a-submenu-item)
    
    ; Update the :items or :desc list
    (let* ((a-menubar (g-value a-bar-item :parent))
	   (old-desc (g-value a-bar-item :desc))
	   (old-sub-desc (third old-desc))
	   (item-obj (g-value a-submenu-item :item-obj))
	   (action (g-value a-submenu-item :action))
	   (new-sub-desc (remove (if action
				     (list item-obj action)
				     (list item-obj))
				 old-sub-desc :test #'equal))
	   (new-desc (substitute new-sub-desc old-sub-desc old-desc
				 :test #'equal)))
      (if a-menubar
	  (s-value a-menubar
		   :items
		   (substitute new-desc old-desc (g-value a-menubar :items)
			       :test #'equal))
	  (s-value a-bar-item :desc new-desc))
      (s-value submenu :old-items new-sub-desc))))

(s-value BAR-ITEM :change-item #'menubar-change-item)
(s-value BAR-ITEM :remove-nth-item (g-value opal:aggrelist :remove-nth-item))
(s-value BAR-ITEM :set-submenu-fn #'set-submenu-fn)
(s-value MENUBAR :get-bar-component-fn #'get-bar-component-fn)

(defun Make-Bar-Item (&key desc font title)
  ;; Type-check the argument before creating an object for it.
  (when desc (Check-Bar-Item desc))
  (let* ((new-bar-item (create-instance NIL BAR-ITEM))
	 (new-submenu (create-instance NIL SUBMENU
			 (:bar-item new-bar-item)
			 (:font (or font opal:default-font))))
	 (win (Make-Submenu-Win new-bar-item new-submenu)))
    (s-value new-bar-item :submenu new-submenu)
    (s-value new-bar-item :submenu-window win)
    ; Put in initial value of :desc slot
    (g-value new-bar-item :desc) ; to initialize the default formula
    (if desc
	(s-value new-bar-item :desc desc)
	(s-value new-bar-item :desc (list title NIL NIL)))
    new-bar-item))


(defun Menubar-Get-Title-Fn (menubar-component)
  (cond ((is-a-p menubar-component BAR-ITEM)
	 (g-value menubar-component :menu-obj))
	((is-a-p menubar-component SUBMENU-ITEM)
	 (g-value menubar-component :item-obj))))

(s-value BAR-ITEM :get-title-fn #'menubar-get-title-fn)
(s-value SUBMENU-ITEM :get-title-fn #'menubar-get-title-fn)

; If the menubar-component is installed in a menubar, then the :items list is
; changed.  Otherwise, the object's :desc slot is set.
;
(defun Menubar-Set-Title-Fn (menubar-component string)
  (cond

    ;; The parameter is a BAR-ITEM
    ((is-a-p menubar-component BAR-ITEM)
     (let* ((a-bar-item menubar-component)
	    (a-menubar (g-value a-bar-item :parent)))
       (cond
	 (a-menubar
	  ; a-bar-item is installed, so set the :items list
	  (rplaca (find (g-value a-bar-item :desc)
			(g-value a-menubar :items) :test #'equal)
		  string)
	  (mark-as-changed a-menubar :items))
	 
	 (t
	  ; not installed, so just set local :desc list
	  (rplaca (g-value a-bar-item :desc) string)
	  (mark-as-changed a-bar-item :desc)))))

    ;; The parameter is a SUBMENU-ITEM
    ((is-a-p menubar-component SUBMENU-ITEM)
     (let* ((a-submenu-item menubar-component)
	    (a-submenu-agg (g-value menubar-component :parent)))
       (cond
	 (a-submenu-agg
	  ; a-submenu-item is installed in a bar-item
	  (let* ((a-bar-item (g-value a-submenu-agg :bar-item))
		 (a-menubar (when a-bar-item
			      (g-value a-bar-item :parent))))
	    (cond

	      ; a-submenu-item is installed in a menubar
	      (a-menubar
	       (let* ((old-desc (g-value a-bar-item :desc))
		      (old-items-desc
		       (find old-desc (g-value a-menubar :items))))
		 (dolist (desc (third old-items-desc))
		   (when (and (equal (first desc)
				     (g-value a-submenu-item :item-obj))
			      (equal (second desc)
				     (g-value a-submenu-item :action)))
		     (rplaca desc string)
		     (mark-as-changed a-menubar :items)))))

	      ; a-submenu-item is not installed in a menubar
	      (t
	       (let* ((old-desc (g-value a-bar-item :desc)))
		 (dolist (desc (third old-desc))
		   (when (and (equal (first desc)
				     (g-value a-submenu-item :item-obj))
			      (equal (second desc)
				     (g-value a-submenu-item :action)))
		     (rplaca desc string)
		     (mark-as-changed a-bar-item :items))))))))

	 ; a-submenu-item is not installed in a bar-item
	 (t
	  (g-value a-submenu-item :desc)
	  (s-value a-submenu-item :desc (list string))))))
	  
    ; Else, print error message
    (t (error "~S is not an instance of ~S or ~S.~%" menubar-component
	   BAR-ITEM SUBMENU-ITEM)))
  
  string)

(s-value BAR-ITEM :set-title-fn #'menubar-set-title-fn)
(s-value SUBMENU-ITEM :set-title-fn #'menubar-set-title-fn)

(define-method :Submenu-Components BAR-ITEM (a-bar-item)
  (g-value a-bar-item :submenu :components))

;
; The args parameter will (optionally) include the where, locator, and key
; parameters that are sent to add-item.  The where refers to the placement
; of the new submenu item among the current submenu items within b-item.
;
(define-method :Add-Submenu-Item MENUBAR (a-menubar b-item s-item &rest args)
  (let* ((a-bar-item (if (is-a-p b-item BAR-ITEM)
			 b-item
			 (get-bar-component a-menubar b-item)))
	 where locator key)
    (multiple-value-setq (where locator key) (opal::get-wheres args))
    (opal:add-item a-bar-item s-item where locator key)))

;
; After looking up the b-item to get a bar-item object, call opal:remove-item.
;
(define-method :Remove-Submenu-Item MENUBAR (a-menubar b-item s-item)
  (let* ((a-bar-item (if (is-a-p b-item BAR-ITEM)
			 b-item
			 (get-bar-component a-menubar b-item))))
    (opal:remove-item a-bar-item s-item)))

; new-desc can have two forms:
;   1) A list of SUBMENU-ITEM instances, or
;   2) A list of submenu-item descriptions, such as
;      ((item1 action1) (item2) (item3 action3)))
;

(define-method :Get-Submenu-Component BAR-ITEM (a-bar-item item)
  (find-if #'(lambda (a-submenu-item)
	       (if (listp item)
		   (equal item (g-value a-submenu-item :desc))
		   (equal item (g-value a-submenu-item :item-obj))))
	   (Submenu-Components a-bar-item)))

(define-method :Find-Submenu-Component MENUBAR (a-menubar submenu-title submenu-item)
  (let ((a-bar-item (if (is-a-p submenu-title BAR-ITEM)
			submenu-title
			(get-bar-component a-menubar submenu-title))))
    (get-submenu-component a-bar-item submenu-item)))


; Submenu-item functions

;
; This function returns an instance of SUBMENU-ITEM.  If the :desc key is
; supplied, then the accompanying parameter should be the string/function or
; atom/function pair that describes a submenu-item.
;
(defun Make-Submenu-Item (&key desc (enabled T))
  (unless (listp desc)
    (error "Expected a list description of a submenu-item, but got ~S.~%"
	   desc))
  (let ((new-submenu-item (create-instance NIL SUBMENU-ITEM
			    (:item-guess (car desc)))))
    (when desc
      (g-value new-submenu-item :desc)
      (s-value new-submenu-item :desc desc))
    (g-value new-submenu-item :enabled)
    (s-value new-submenu-item :enabled enabled)
    new-submenu-item))


;;;
;;;  Methods for editing the strings of the menubar and submenus
;;;

(define-method :new-item-label gg::MENUBAR (obj)
  (let ((val (1+ (or (g-value obj :last-menubar-label-used)
		     (length (g-value obj :items))))))
    (s-value obj :last-menubar-label-used val)
    (list (format NIL "Label~a" val) NIL '(("sub-item1")))))

(define-method :new-item-label gg::BAR-ITEM (obj)
  (let ((val (1+ (or (g-value obj :last-bar-item-label-used)
		     (length (g-value obj :items))))))
    (s-value obj :last-bar-item-label-used val)
    (list (format NIL "Label~a" val))))

(define-method :string-set-func gg::MENUBAR
  (gadget-obj str-obj final-event final-string)
  ;; First test if we are editing the string of a bar-item
  (let ((a-bar-item (g-value str-obj :parent)))
    (if (is-a-p a-bar-item gg::BAR-ITEM)
	(let* ((old-desc (g-value a-bar-item :desc))
	       (new-desc (push final-string (cdr (copy-tree old-desc)))))
	  (Put-Down-MenuBar-Popups gadget-obj)
	  (opal::Aggrelist-Edit-String-Func gadget-obj gadget-obj str-obj
					    final-event new-desc :rank))
	;; It wasn't the string of a bar-item.  It was a submenu item.
	(let ((a-submenu (g-value str-obj :parent :parent)))
	  (setf a-bar-item (g-value a-submenu :bar-item))
	  (opal::Aggrelist-Edit-String-Func a-bar-item a-submenu str-obj
					    final-event (list final-string)
					    :rank))
	)))




#|
***** PROBLEMS: Asynchronous window errors
***** disabled items not going grey
|#
