Module:       duim-gadgets-internals
Synopsis:     DUIM gadgets
Author:       Scott McKay, Andy Armstrong
Copyright:    Original Code is Copyright (c) 1995-2004 Functional Objects, Inc.
              All rights reserved.
License:      Functional Objects Library Public License Version 1.0
Dual-license: GNU Lesser General Public License
Warranty:     Distributed WITHOUT WARRANTY OF ANY KIND

/// Menus

define open abstract class <menu-bar>
    (<no-value-gadget-mixin>,
     <gadget-bar-mixin>,
     <basic-gadget>)
end class <menu-bar>;

//--- Maybe this should just be <sheet>, but it's often convenient 
//--- to specify a frame
define constant <menu-owner> = false-or(type-union(<sheet>, <frame>));

// A menu will have three kinds of children:
//  - one of the subclasses of <menu-button>
//  - one of the subclasses of <menu-box>
//  - other <menu>'s
define open abstract class <menu>
    (<no-value-gadget-mixin>,
     <gadget-command-mixin>,
     <gadget-bar-mixin>,
     <mnemonic-mixin>,
     <updatable-gadget-mixin>,
     <labelled-gadget-mixin>,
     <basic-gadget>)
  sealed slot menu-owner :: <menu-owner> = #f,		// for pop-up menus
    init-keyword: owner:;
end class <menu>;

define method initialize
    (menu :: <menu>,
     #key tear-off? = #f, help-menu? = #f,
	  x-alignment = #"left", y-alignment = #"top")
  next-method();
  let bits = logior(if (tear-off?) %tear_off_menu else 0 end,
		    if (help-menu?) %help_menu else 0 end);
  // <labelled-gadget-mixin> also does this, but we do it again
  // here because the defaults are different for menus...
  let xa = select (x-alignment)
	     #"left"  => %x_alignment_left;
	     #"right" => %x_alignment_right;
	     #"center", #"centre" => %x_alignment_center;
	   end;
  let ya = select (y-alignment)
	     #"top"      => %y_alignment_top;
	     #"bottom"   => %y_alignment_bottom;
	     #"baseline" => %y_alignment_baseline;
	     #"center", #"centre" => %y_alignment_center;
	   end;
  gadget-flags(menu)
    := logior(logand(gadget-flags(menu),
		     lognot(logior(%tear_off_menu, %help_menu,
				   %x_alignment_mask, %y_alignment_mask))),
	      bits, xa, ya)
end method initialize;

define sealed inline method tear-off-menu?
    (menu :: <menu>) => (tear-off? :: <boolean>)
  logand(gadget-flags(menu), %tear_off_menu) = %tear_off_menu
end method tear-off-menu?;

define sealed inline method help-menu?
    (menu :: <menu>) => (help? :: <boolean>)
  logand(gadget-flags(menu), %help_menu) = %help_menu
end method help-menu?;

define sealed method top-level-sheet 
    (menu :: <menu>) => (sheet :: false-or(<sheet>))
  let owner = menu-owner(menu);
  if (owner)
    top-level-sheet(owner)
  else
    next-method()
  end
end method top-level-sheet;

define sealed method display
    (sheet :: <menu>) => (display :: false-or(<display>))
  if (menu-owner(sheet))
    display(menu-owner(sheet))
  else
    next-method()
  end
end method display;

define sealed method gadget-selection-mode
    (menu :: <menu>) => (selection-mode :: <selection-mode>)
  #"none"
end method gadget-selection-mode;

define open generic note-menu-attached 
    (frame :: <abstract-frame>, menu :: <menu>) => ();

define open generic note-menu-detached
    (frame :: <abstract-frame>, menu :: <menu>) => ();

// If this is a pop-up menu, ensure that it's mirrored before we map it.
// Then unmap it when we're done.
define method sheet-mapped?-setter 
    (mapped? == #t, menu :: <menu>, 
     #key do-repaint? = #t, clear? = do-repaint?)
 => (mapped? :: <boolean>)
  ignore(do-repaint?, clear?);
  let owner     = menu-owner(menu);
  let top-sheet = owner & top-level-sheet(owner);
  if (top-sheet)
    block ()
      unless (sheet-attached?(menu))
        //--- We should add this to the owner's top-level sheet,
        //--- but <top-level-sheet> only allows a single child
        add-child(display(top-sheet), menu);	// ensure the menu is attached
	note-menu-attached(sheet-frame(menu), menu)
      end;
      next-method()
    cleanup
      sheet-mapped?(menu) := #f
    end;
    mapped?
  else
    next-method()
  end
end method sheet-mapped?-setter;

define method note-sheet-detached
    (menu :: <menu>) => ()
  next-method();
  let owner     = menu-owner(menu);
  let top-sheet = owner & top-level-sheet(owner);
  when (top-sheet)
    note-menu-detached(sheet-frame(top-sheet), menu)
  end
end method note-sheet-detached;

define method note-menu-attached
    (frame :: <frame>, menu :: <menu>) => ()
  #f
end method note-menu-attached;

define method note-menu-detached
    (frame :: <frame>, menu :: <menu>) => ()
  #f
end method note-menu-detached;

define method display-menu
    (menu :: <menu>,
     #key x :: false-or(<integer>), y :: false-or(<integer>)) => ()
  let owner = menu-owner(menu);
  assert(owner,
	 "Cannot display a popup menu without an owner: %=", menu);
  if (x & y)
    set-sheet-position(menu, x, y)
  else
    let _port   = port(owner);
    let pointer = port-pointer(_port);
    //--- It would be much simpler if the owner was always a sheet
    let sheet
      = select (owner by instance?)
	  <frame> => top-level-sheet(owner);
	  <sheet> => owner;
	end;
    when (sheet)
      let (x, y) = pointer-position(pointer, sheet: sheet);
      set-sheet-position(menu, x, y)
    end
  end;
  sheet-mapped?(menu) := #t
end method display-menu;


/// Menu boxes

define open abstract class <menu-box>
    (<gadget-box>,
     <basic-gadget>)
end class <menu-box>;

define function menu-box-selection-mode-class
    (selection-mode :: <selection-mode>) => (class :: <class>)
  select (selection-mode)
    #"none"     => <push-menu-box>;
    #"single"   => <radio-menu-box>;
    #"multiple" => <check-menu-box>;
  end
end function menu-box-selection-mode-class;

define sealed inline method make
    (class == <menu-box>, #rest initargs,
     #key selection-mode :: <selection-mode> = #"none", #all-keys)
 => (menu-box :: <menu-box>)
  apply(make, menu-box-selection-mode-class(selection-mode), initargs)
end method make;


define open abstract class <push-menu-box>
    (<action-gadget-mixin>,
     <menu-box>,
     <basic-value-gadget>)
end class <push-menu-box>;

define sealed method gadget-selection-mode
    (menu :: <push-menu-box>) => (selection-mode :: <selection-mode>)
  #"none"
end method gadget-selection-mode;


define open abstract class <radio-menu-box> 
    (<gadget-selection-mixin>, <menu-box>)
end class <radio-menu-box>;

define sealed method gadget-selection-mode
    (menu :: <radio-menu-box>) => (selection-mode :: <selection-mode>)
  #"single"
end method gadget-selection-mode;


define open abstract class <check-menu-box> 
    (<gadget-selection-mixin>, <menu-box>)
end class <check-menu-box>;

define sealed method gadget-selection-mode
    (menu :: <check-menu-box>) => (selection-mode :: <selection-mode>)
  #"multiple"
end method gadget-selection-mode;


/// Menu buttons

define open abstract class <menu-button>
    (<updatable-gadget-mixin>,
     <button>)
end class <menu-button>;


define open abstract class <push-menu-button> 
    (<menu-button>, <default-gadget-mixin>, <basic-value-gadget>)
end class <push-menu-button>;

define method initialize
    (button :: <push-menu-button>, #key) => ()
  next-method();
  when (gadget-command(button) & ~gadget-activate-callback(button))
    gadget-activate-callback(button) := callback-for-command(gadget-command(button))
  end
end method initialize;

define sealed method gadget-selection-mode
    (button :: <push-menu-button>) => (selection-mode :: <selection-mode>)
  #"none"
end method gadget-selection-mode;


define open abstract class <radio-menu-button>
    (<menu-button>, <basic-value-gadget>)
end class <radio-menu-button>;

define sealed method gadget-selection-mode
    (button :: <radio-menu-button>) => (selection-mode :: <selection-mode>)
  #"single"
end method gadget-selection-mode;

// Because we don't have <action-gadget-mixin> in radio menu buttons
define sealed method activate-gadget
    (gadget :: <radio-menu-button>) => ()
  execute-value-changed-callback(gadget, gadget-client(gadget), gadget-id(gadget))
end method activate-gadget;


define open abstract class <check-menu-button>
    (<menu-button>, <basic-value-gadget>)
end class <check-menu-button>;

define sealed method gadget-selection-mode
    (button :: <check-menu-button>) => (selection-mode :: <selection-mode>)
  #"multiple"
end method gadget-selection-mode;

// Because we don't have <action-gadget-mixin> in check menu buttons
define sealed method activate-gadget
    (gadget :: <check-menu-button>) => ()
  execute-value-changed-callback(gadget, gadget-client(gadget), gadget-id(gadget))
end method activate-gadget;


define function menu-button-selection-mode-class
    (selection-mode :: <selection-mode>) => (class :: <class>)
  select (selection-mode)
    #"none"     => <push-menu-button>;
    #"single"   => <radio-menu-button>;
    #"multiple" => <check-menu-button>;
  end;
end function menu-button-selection-mode-class;

define sealed inline method make
    (class == <menu-button>, #rest initargs,
     #key selection-mode :: <selection-mode> = #"none", #all-keys)
 => (button :: <menu-button>)
  apply(make, menu-button-selection-mode-class(selection-mode), initargs)
end method make;


/// Popup menus

// Default implementation of menu chooser when handed a sequence of items.
// This is "sideways" because it is a forward reference from DUIM-Sheets.
define sideways method do-choose-from-menu
    (framem :: <frame-manager>, owner :: <sheet>, items :: <sequence>,
     #rest keys,
     #key title = #f, value,
          label-key = collection-gadget-default-label-key,
          value-key = collection-gadget-default-value-key,
          width, height, foreground, background, text-style,
	  multiple-sets? = #f,
     #all-keys)
 => (value, success? :: <boolean>)
  dynamic-extent(keys);
  ignore(value);
  let menu
    = make-menu-from-items(framem, items,
			   label-key: label-key,
			   value-key: value-key,
			   width:  width,
			   height: height,
			   foreground: foreground,
			   background: background,
			   text-style: text-style,
			   title: title,
			   owner: owner,
			   multiple-sets?: multiple-sets?);
  block ()
    apply(do-choose-from-menu, framem, owner, menu, keys)
  cleanup
    // We're done with it, so get rid of all back-end resources
    destroy-sheet(menu)
  end  
end method do-choose-from-menu;

define method make-menu-from-items
    (framem :: <frame-manager>, items :: <sequence>,
     #key owner, title = "Menu",
          label-key = collection-gadget-default-label-key,
          value-key = collection-gadget-default-value-key,
          width, height, foreground, background, text-style, multiple-sets? = #f)
 => (menu :: <menu>)
  with-frame-manager (framem)
    let menu-boxes :: <stretchy-object-vector> = make(<stretchy-vector>);
    local method make-menu-box (items)
	    add!(menu-boxes, make(<menu-box>,
				  items: items,
				  label-key: label-key,
				  value-key: value-key,
				  foreground: foreground,
				  background: background,
				  text-style: text-style))
	  end method;
    if (multiple-sets?)
      do(make-menu-box, items)
    else
      make-menu-box(items)
    end;
    make(<menu>,
	 owner: owner,
	 label: title,
	 width:  width,
	 height: height,
	 children:   menu-boxes, 
	 foreground: foreground,
	 background: background,
	 text-style: text-style)
  end
end method make-menu-from-items;


/// Menu creation macros

/*--- The problem with this is that we lose the variable 'menu'!
define macro menu
  { menu (?label:expression, #rest ?options:expression)
      ?entries:*
    end }
    => { make(<menu>, label: ?label, children: vector(?entries), ?options) }
end macro menu;

define macro menu-box
  { menu-box (?label:expression, #rest ?options:expression)
      ?entries:*
    end }
    => { make(<menu-box>, label: ?label, children: vector(?entries), ?options) }
 entries:
  { } => { }
  { item ?label:expression => ?callback:expression; ... }
    => { make(<menu-button>, label: ?label, activate-callback: ?callback), ... }
end macro menu-box;
*/