//Title:       PopupMenuApplet
//Version:     1.1.2
//Date:        04/03/2002
//Author:      David Binard (http://www.california.com/~binard)
//Description: A configurable menu applet based on the
//             Java (TM) 1.1 PopupMenu class.
//Changelog:   1.1.2 cleaned up code (indentation), added support for disabled and checkbox menu items, font/style/size support for menu headings, new mouse event and field separator params, new popup location params and new popup(x,y) public method callable from JavaScript, new menu data URL param, and support for the javascript: syntax in menu items.
//             1.1.1 added font, style and size support for menu items.
//             1.1 first release.

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.net.*;
import java.util.*;
import java.io.*;
import netscape.javascript.*;

public class PopupMenuApplet extends Applet implements MouseListener, ActionListener {

  String title, img, bgcolor, text, font, size, style, menufont, menuheadingfont;
  String menustyle, menuheadingstyle, menusize, menuheadingsize, data, dataurl;
  String fieldseparator, popupX, popupY, mouseEvents;
  String actions[], targets[];
  Menu menus[];
  MenuItem menuItems[];
  int fontsize, menufontsize, menuheadingfontsize;
  int popupx, popupy;
  CardLayout cardlayout;
  PopupMenu mainMenu;
  Image bgImage = null;
  boolean parseError = false, imgError = false, isMenuDataUrl = false;
  boolean mousePressedFlag = false, mouseReleasedFlag = false, mouseClickedFlag = false;
  boolean mouseEnteredFlag = false, mouseExitedFlag = false;
  URL imgUrl, menuDataUrl;
  Label label;
  Font labelFont, menuFont, menuHeadingFont;

  //Construct the applet
  public PopupMenuApplet() {
  }

  //Initialize the applet
  public void init() {
    cardlayout = new CardLayout();
    title = getParameter("title");
    if (title == null) title = "Menu";
    // the colors for bgcolor and text are used only for the text label
    // Java 1.1 menus themselves don't support colors, although some Unix implementations
    // allow a menu's background color to be inherited from the object it's attached to.
    bgcolor = getParameter("bgcolor");
    if (bgcolor == null) bgcolor = "white";
    else bgcolor = bgcolor.toLowerCase();
    text = getParameter("text");
    if (text == null) text = "blue";
    else text = text.toLowerCase();
    // font, size and style are used for the applet's text label (if any)
    font = getParameter("font");
    if (font == null) font = "SansSerif";
    size = getParameter("size");
    if (size == null) fontsize = 11;
    else
      try {
        fontsize = Integer.parseInt(size);
      } catch(NumberFormatException f) {
        fontsize = 11;
      }
    style = getParameter("style");
    if (style == null) style = "plain";
    labelFont = new Font(font, translateStyle(style), fontsize);
    // menufont, menusize and menustyle default to font, size and style if undefined,
    // but they can be also defined explicitely in case we want the popup menu to look different
    // from the applet's text label (if any)
    // same goes for menu headings
    menufont = getParameter("menufont");
    if (menufont == null) menufont = font;
    menuheadingfont = getParameter("menuheadingfont");
    if (menuheadingfont == null) menuheadingfont = menufont;
    menusize = getParameter("menusize");
    if (menusize == null) menufontsize = fontsize;
    else
      try {
        menufontsize = Integer.parseInt(menusize);
      } catch(NumberFormatException f) {
        menufontsize = 11;
      }
    menuheadingsize = getParameter("menuheadingsize");
    if (menuheadingsize == null) menuheadingfontsize = menufontsize;
    else
      try {
        menuheadingfontsize = Integer.parseInt(menuheadingsize);
      } catch(NumberFormatException f) {
        menuheadingfontsize = 11;
      }
    menustyle = getParameter("menustyle");
    if (menustyle == null) menustyle = style;
    menuheadingstyle = getParameter("menuheadingstyle");
    if (menuheadingstyle == null) menuheadingstyle = menustyle;
    menuFont = new Font(menufont, translateStyle(menustyle), menufontsize);
    menuHeadingFont = new Font(menuheadingfont, translateStyle(menuheadingstyle), menuheadingfontsize);
    fieldseparator = getParameter("fieldseparator");
    if (fieldseparator == null) fieldseparator = "*";
    data = getParameter("data");
    dataurl = getParameter("dataurl");
    if (dataurl != null) {
      isMenuDataUrl = true;
      try { // absolute URL
        menuDataUrl = new URL(dataurl);
      } catch(MalformedURLException a) {
        try { // relative URL
          menuDataUrl = new URL(this.getDocumentBase(), dataurl);
        } catch(MalformedURLException r) {
          isMenuDataUrl = false;
        }
      }
    }
    if (isMenuDataUrl) {
      try {
        URLConnection urlConnection = menuDataUrl.openConnection();
        BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        String line;
        while ((line = reader.readLine()) != null) {
          data += line;
        }
        reader.close();
      } catch(IOException i) {
        this.showStatus("PopupMenuApplet: unable to download menu data from URL " + data + ": " + i);
      }
    }
    parseData(data, fieldseparator);
    this.addMouseListener(this);
    this.setBackground(translateColor(bgcolor));
    this.setForeground(translateColor(text));
    this.setFont(labelFont);
    this.setLayout(cardlayout);
    img = getParameter("img");
    if (img != null) {
      try { // absolute URL
        imgUrl = new URL(img);
        bgImage = this.getImage(imgUrl);
      } catch(MalformedURLException a) {
        try { // relative URL
          imgUrl = new URL(this.getDocumentBase(), img);
          bgImage = this.getImage(imgUrl);
        } catch(MalformedURLException r) {
          imgError = true;
        }
      }
      bgImage = this.getImage(this.getDocumentBase(), img);
    } else {
      label = new Label(title);
      label.setAlignment(1);
      label.addMouseListener(this);
      this.add(title, label);
    }
    popupX = getParameter("popupX");
    popupY = getParameter("popupY");
    mouseEvents = getParameter("mouseevents");
    if (mouseEvents == null) mouseEvents = "mouseentered,mousepressed";
    else mouseEvents = mouseEvents.toLowerCase();
    if (mouseEvents.indexOf("mousepressed") != -1) mousePressedFlag = true;
    if (mouseEvents.indexOf("mousereleased") != -1) mouseReleasedFlag = true;
    if (mouseEvents.indexOf("mouseclicked") != -1) mouseClickedFlag = true;
    if (mouseEvents.indexOf("mouseentered") != -1) mouseEnteredFlag = true;
    if (mouseEvents.indexOf("mouseexited") != -1) mouseExitedFlag = true;
 }


  //Start the applet
  public void start() {
  }

  //Stop the applet
  public void stop() {
  }

  //Destroy the applet
  public void destroy() {
  }

  //Get Applet information
  public String getAppletInfo() {
    return(
      "Title:\t\tPopupMenuApplet\n" +
      "Version:\t\t1.1.2\n" +
      "Date:\t\t04/03/2002\n" +
      "Author:\t\tDavid Binard (http://www.california.com/~binard)\n" +
      "Description:\tA configurable menu applet based on the\n" +
      "\t\tJava (TM) 1.1 PopupMenu class."
    );
  }

  //Get parameter info
  public String[][] getParameterInfo() {
    String pinfo[][] = {
      {"title", "String", "Menu title"},
      {"img", "URL", "Background image"},
      {"bgcolor", "Color or Hex RGB value", "Text label background color"},
      {"text", "Color or Hex RGB value", "Text label foreground color"},
      {"font", "Font name", "Menu font name"},
      {"size", "Integer", "Menu font size"},
      {"style", "Sum of BOLD,PLAIN,ITALIC", "Menu font style"},
      {"menufont", "Font name", "Menu font name"},
      {"menusize", "Integer", "Menu font size"},
      {"menustyle", "Sum of BOLD,PLAIN,ITALIC", "Menu font style"},
      {"menuheadingfont", "Font name", "Menu heading font name"},
      {"menuheadingsize", "Integer", "Menu heading font size"},
      {"menuheadingstyle", "Sum of BOLD,PLAIN,ITALIC", "Menu heading font style"},
      {"fieldseparator", "Character", "Field separator for menu data"},
      {"popupX", "LEFT,CENTER,RIGHT or Integer value", "popup X coordinate"},
      {"popupY", "BOTTOM,MIDDLE,TOP or Integer value", "popup Y coordinate"},
      {"data", "Menu item names, actions and targets", "Menu data"},
      {"dataurl", "URL pointing to menu data", "Menu data"},
      {"mouseevents", "mousePressed,mouseReleased,mouseClicked,mouseEntered,mouseExited", "Mouse Events"},
    };
    return pinfo;
  }

  public void paint(Graphics g) {
    if (bgImage != null)
       g.drawImage(bgImage,0,0,this);
    if (imgError)
       this.showStatus("PopupMenuApplet: invalid URL for img parameter " + img);
    if (parseError)
       this.showStatus("PopupMenuApplet Error: unbalanced braces in data parameter tag."); 
  }

  public void mousePressed(MouseEvent e) {
    if (mousePressedFlag) popup(e);
  }

  public void mouseReleased(MouseEvent e) {
    if (mouseReleasedFlag) popup(e);
  }

  public void mouseClicked(MouseEvent e) {
    if (mouseClickedFlag) popup(e);
  }

  public void mouseEntered(MouseEvent e) {
    if (mouseEnteredFlag) popup(e);
  }

  public void mouseExited(MouseEvent e) {
    if (mouseExitedFlag) popup(e);
  }

  public void actionPerformed(ActionEvent e) {
    int elt = Integer.parseInt(e.getActionCommand());
    String cmd = actions[elt];
    String target = targets[elt];
    if ((cmd.length() > 7 && cmd.substring(0,7).equalsIgnoreCase("script="))
     || (cmd.length() > 11 && cmd.substring(0,11).equalsIgnoreCase("javascript:"))) {
      // catch exception thrown by the appletviewer (but not a browser)
      // just so we can test the menu appearance with the appletviewer.
      try {
        if (cmd.substring(0,7).equalsIgnoreCase("script="))
          cmd = cmd.substring(7); // trim the "script=" identifier
        else if (cmd.substring(0,11).equalsIgnoreCase("javascript:"))
          cmd = cmd.substring(11); // trim the "javascript:" identifier
        if (target.equalsIgnoreCase("_self") || target.equalsIgnoreCase("_parent")
         || target.equalsIgnoreCase("_top"))
          target = target.substring(1).toLowerCase(); // allow for leading underscore HTML syntax
        cmd = target + "." + cmd;
        JSObject win = JSObject.getWindow(this);
        win.eval(cmd);
      } catch(Exception n) {
        this.showStatus("PopupMenuApplet: Error running script " + cmd);
      }
    } else {
      try { // absolute URL
        URL dest = new URL(cmd);
        this.getAppletContext().showDocument(dest, target);
      } catch(MalformedURLException a) {
        try { // relative URL
          URL dest = new URL(this.getDocumentBase(), cmd);
          this.getAppletContext().showDocument(dest, target);
        } catch(MalformedURLException r) {
          this.showStatus("PopupMenuApplet: invalid URL " + cmd);
        }
      }
    }
  }

  public void popup() {
    if (parseError) return;
    // can't use mouse for popup location on popup(void) so use defaults
    if (popupX == null || popupX.equalsIgnoreCase("mouse"))
      popupx = this.getSize().width/2;
    if (popupY == null || popupY.equalsIgnoreCase("mouse"))
      popupy = this.getSize().height/2;
    mainMenu.show(this, popupx, popupy);
  }

  public void popup(MouseEvent e) {
    if (parseError) return;
    if (popupX == null || popupX.equalsIgnoreCase("middle") || popupX.equalsIgnoreCase("center"))
      popupx = this.getSize().width/2; // default location is middle of applet area
    else if (popupX.equalsIgnoreCase("mouse"))
      popupx = e.getX();
    else if (popupX.equalsIgnoreCase("left"))
       popupx = 0;
    else if (popupX.equalsIgnoreCase("right"))
       popupx = this.getSize().width;
    else
      popupx = Integer.parseInt(popupX);
    if (popupY == null || popupY.equalsIgnoreCase("middle") || popupY.equalsIgnoreCase("center"))
      popupy = this.getSize().height/2; // default location is middle of applet area
    else if (popupY.equalsIgnoreCase("mouse"))
      popupy = e.getY();
    else if (popupY.equalsIgnoreCase("bottom"))
      popupy = this.getSize().height;
    else if (popupY.equalsIgnoreCase("top"))
      popupy = 0;
    else
      popupy = Integer.parseInt(popupY);
    mainMenu.show(this, popupx, popupy);
  }

  public void popup(String popupX, String popupY) {
    if (parseError) return;
    if (popupX == null || popupX.equalsIgnoreCase("middle") || popupX.equalsIgnoreCase("center"))
      popupx = this.getSize().width/2; // default location is middle of applet area
    else if (popupX.equalsIgnoreCase("left"))
       popupx = 0;
    else if (popupX.equalsIgnoreCase("right"))
       popupx = this.getSize().width;
    else
      popupx = Integer.parseInt(popupX);
    if (popupY == null || popupY.equalsIgnoreCase("middle") || popupY.equalsIgnoreCase("center"))
      popupy = this.getSize().height/2; // default location is middle of applet area
    else if (popupY.equalsIgnoreCase("bottom"))
      popupy = this.getSize().height;
    else if (popupY.equalsIgnoreCase("top"))
      popupy = 0;
    else
      popupy = Integer.parseInt(popupY);
    mainMenu.show(this, popupx, popupy);
  }

  private Color translateColor(String c) {
    if(c.equalsIgnoreCase("white"))
      return(Color.white);
    else if(c.equalsIgnoreCase("lightgray"))
      return(Color.lightGray);
    else if(c.equalsIgnoreCase("gray"))
      return(Color.gray);
    else if(c.equalsIgnoreCase("darkgray"))
      return(Color.darkGray);
    else if(c.equalsIgnoreCase("black"))
      return(Color.black);
    else if(c.equalsIgnoreCase("red"))
      return(Color.red);
    else if(c.equalsIgnoreCase("pink"))
      return(Color.pink);
    else if(c.equalsIgnoreCase("orange"))
      return(Color.orange);
    else if(c.equalsIgnoreCase("yellow"))
      return(Color.yellow);
    else if(c.equalsIgnoreCase("green"))
      return(Color.green);
    else if(c.equalsIgnoreCase("magenta"))
      return(Color.magenta);
    else if(c.equalsIgnoreCase("cyan"))
      return(Color.cyan);
    else if(c.equalsIgnoreCase("blue"))
      return(Color.blue);
    // allow for Hex RGB values (and an optional leading #, as in HTML syntax)
    else if (c.length() == 6 || (c.length() == 7 && c.charAt(0) == '#')) {
      if (c.length() == 7) c = c.substring(1);
      return(new Color(hexToInt(c.substring(0,2)),
             hexToInt(c.substring(2,4)),hexToInt(c.substring(4,6))));
    }
    else
      return(Color.white);
  }

  private int hexToInt(String c) {
    try {
      return(Integer.parseInt(c, 16));
    } catch(NumberFormatException h) {
      return 0;
    }
  }

  private int translateStyle(String s) {
    int style = 0;
    String token = null;
    StringTokenizer st = new StringTokenizer(s,",+ \t\n\r");
    do {
      try {
        token = st.nextToken();
      } catch(NoSuchElementException n) {}
      if (token.equalsIgnoreCase("PLAIN"))
        style += Font.PLAIN;
      else if (token.equalsIgnoreCase("BOLD"))
        style += Font.BOLD;
      else if (token.equalsIgnoreCase("ITALIC"))
        style += Font.ITALIC;
    } while (st.hasMoreTokens());
    return style;
  }
 
  private void parseData(String s, String fieldseparator) {
    // menuItem counters start at -1 so that at first increment, they get set to
    // the first array subscript value of 0
    // menu counters start at 0 so that at first increment, they get set to
    // the array subscript value of 1, the first value (0) being reserved for the main menu
    int levelCtr = -1, menuCtr = 0, menuItemCtr = -1;
    int levelCount = 0, menuCount = 0, menuItemCount = -1;
    int parentMenuPtr[];
    String itemToken = null, datatoken = null;
    String title = "", action = "", target = "_self";
    boolean newMenu = false;
    boolean enabledFlag = true;
    boolean checkboxFlag = false;
    if (s == null || s.indexOf("{") == -1) {
      parseError = true;
      return;
    }
    StringTokenizer braces = new StringTokenizer(s,"{}",true);
    StringTokenizer braceCtr = new StringTokenizer(s,"{}",true);
    StringTokenizer asterisks;
    // Get the number of menus and menuItems for which to allocate array space
    do {
      try {
        itemToken = braceCtr.nextToken();
      } catch(NoSuchElementException i) {}
      if (itemToken.charAt(0) == '{') {
        if (newMenu) menuCount++;
        newMenu = true;
        levelCtr++;
        if (levelCount < levelCtr) levelCount = levelCtr;
      } else if (itemToken.charAt(0) == '}') {
        if (newMenu) menuItemCount++;
        newMenu = false;
        levelCtr--;
      }
    } while (braceCtr.hasMoreTokens());
    if (levelCtr != -1) {
      parseError = true;
      return;
    }
    // allocate one more element than the counter values , since the first subscript value is 0
    actions = new String[menuItemCount+1];
    targets = new String[menuItemCount+1];
    menuItems = new MenuItem[menuItemCount+1];
    menus = new Menu[menuCount+1];
    parentMenuPtr = new int[levelCount+1];
    mainMenu = new PopupMenu(title);
    menus[0] = (Menu)(mainMenu);
    this.add(mainMenu);
    itemToken = null;
    newMenu = false;
    // Parse the data Param and build the menu and menu items
    do {
      try {
        itemToken = braces.nextToken();
      } catch(NoSuchElementException i) {}
      if (itemToken.charAt(0) == '{') {
        if (newMenu) {
          menuCtr++;
          menus[menuCtr] = new Menu(title);
          menus[menuCtr].setFont(menuHeadingFont);
          menus[menuCtr].setEnabled(enabledFlag);
          menus[parentMenuPtr[levelCtr]].add(menus[menuCtr]);
          parentMenuPtr[levelCtr+1] = menuCtr;
        }
        newMenu = true;
        levelCtr++;
      } else if (itemToken.charAt(0) == '}') {
        if (newMenu) {
          menuItemCtr++;
          actions[menuItemCtr] = action;
          targets[menuItemCtr] = target;
          if (checkboxFlag == true)
             menuItems[menuItemCtr] = new CheckboxMenuItem(title, true);
          else
             menuItems[menuItemCtr] = new MenuItem(title);
          menuItems[menuItemCtr].setFont(menuFont);
          menuItems[menuItemCtr].setEnabled(enabledFlag);
          menuItems[menuItemCtr].addActionListener(this);
          menuItems[menuItemCtr].setActionCommand(new Integer(menuItemCtr).toString());
          menus[parentMenuPtr[levelCtr]].add(menuItems[menuItemCtr]);
        }
        newMenu = false;
        levelCtr--;
      } else if (!itemToken.trim().equals("")) {
        //asterisks = new StringTokenizer(itemToken, "*");
        asterisks = new StringTokenizer(itemToken, fieldseparator);
        try {
          title = asterisks.nextToken();
          // a menu separator is a -, but allow for hr as well, as in HTML syntax
          if (title.startsWith("!--")) { // check for disabled menu
            title = title.substring(3);
            enabledFlag = false;
          } else {
            enabledFlag = true;
          }
          if (title.startsWith("!..")) { // check for checkbox menu
            title = title.substring(3);
            checkboxFlag = true;
          } else {
            checkboxFlag = false;
          }
          if (title.equals("-") || title.equalsIgnoreCase("HR")) title = "-";
        } catch(NoSuchElementException i) {
          title = "-";
        }
        try {
          action = asterisks.nextToken();
        } catch(NoSuchElementException i) {
          action = "";
        }
        try {
          target = asterisks.nextToken();
        } catch(NoSuchElementException i) {
          target = "_self";
        }
      }
    } while (braces.hasMoreTokens());
  }

}
