// Dashboard.java
//
//    Created: September 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard;

import org.biomoby.shared.Utils;
import org.biomoby.registry.meta.RegistriesList;
import org.biomoby.registry.meta.Registries;
import org.biomoby.registry.meta.Registry;

import org.tulsoft.shared.UUtils;
import org.tulsoft.shared.PrefsUtils;
import org.tulsoft.tools.gui.SwingUtils;
import org.tulsoft.tools.gui.AwtUtils;
import org.tulsoft.tools.gui.ProgressView;

import org.apache.commons.discovery.tools.Service;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JComponent;
import javax.swing.JTabbedPane;
import javax.swing.JScrollPane;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JCheckBox;
import javax.swing.Icon;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JMenuBar;
import javax.swing.JProgressBar;
import javax.swing.JOptionPane;
import javax.swing.AbstractAction;
import javax.swing.Box;
import javax.swing.BorderFactory;
import javax.swing.UIManager;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.HyperlinkEvent;
import javax.swing.ToolTipManager;
import javax.swing.KeyStroke;

import java.awt.GridBagLayout;
import java.awt.Font;
import java.awt.Color;
import java.awt.Insets;
import java.awt.Dimension;
import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;

import java.util.Vector;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Date;
import java.net.URL;
import java.text.DateFormat;
import java.io.IOException;

/**
 * Dasboard is a graphical user interface for Biomoby service
 * providers. It consists of several panels, and new panels can be
 * added by an extension mechanism, using Java SPI. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: Dashboard.java,v 1.31 2008/03/02 12:45:26 senger Exp $
 */

public class Dashboard
    implements DashboardPanel, DashboardProperties, ChangeListener {

    private static org.apache.commons.logging.Log log =
       org.apache.commons.logging.LogFactory.getLog (Dashboard.class);

    protected static final String SELECTION_PROLOGUE =
    "<html>Choose panels that you wish to be shown in the dashboard.<br>" +
    "Some panels may be mandatory so you cannot remove them.<p>";

    protected static final String CONNECTED_TO = "          Connected to: ";

    protected static final Insets BREATH = new Insets (10,10,10,10);
    protected static final Insets BREATH_TOP = new Insets (10,0,0,0);
    protected static final Insets BREATH_BOT = new Insets (0,0,10,0);
    protected static final Insets BREATH_SMALL = new Insets (5,5,5,5);

    // action commands for menu items
    protected final static String AC_EXIT    = "ac-exit";
    protected final static String AC_ABOUT   = "ac-about";
    protected final static String AC_HELP    = "ac-help";
    protected final static String AC_PSELECT = "ac-pselect";
    protected final static String AC_MYPREF  = "ac-mypref";
    protected final static String AC_EXPORT  = "ac-export";
    protected final static String AC_IMPORT  = "ac-import";

    public static boolean useLoadMonitor = true;
    protected static Properties dashboardProperties;
    protected static ExitSecurityManager exitman;

    private JComponent dashboard;
    private DashboardHeader header;
    protected JTabbedPane tabbedPane;
    protected Color bgcolor;
    protected DashboardPanel[] panels;
    protected DashboardPanel[] shownPanels;
    protected JCheckBox[] panelBoxes;
    protected PropertyChannel propertyChannel;
    protected JMenuBar menuBar;
    protected JMenu helpMenu, titleMenu;
    protected int countOfPermanentHelpItems;

    protected static Icon exitIcon, aboutIcon;
    protected static Icon helpIcon, helpIconDis;
    protected static Icon pselectIcon, pselectIconDis;
    protected static Icon prefIcon, prefIconDis;
    protected static Icon exportIcon, exportIconDis;
    protected static Icon importIcon, importIconDis;
    protected static Icon lafIcon;

//     static {
// 	try {
// 	    // get the classloader for this class
// 	    ClassLoader loader = Dashboard.class.getClassLoader();
// 	    if (loader == null) {
// 		loader = Thread.currentThread().getContextClassLoader();
// 	    }

// 	    // load dashboard properties from a file located by the
// 	    // class resource loader
// 	    Enumeration en = loader.getResources (DashboardConfig.DASHBOARD_CONFIG_FILENAME);
// 	    dashboardProperties = new Properties();
// 	    while (en.hasMoreElements()) {
// 		URL resourceURL = (URL)en.nextElement();
// 		dashboardProperties.load (resourceURL.openStream());
// 	    }
// 	} catch (Exception e) {
// 	    log.warn ("Cannot find/open/read dasboard property file " +
// 		      DashboardConfig.DASHBOARD_CONFIG_FILENAME + ": " + e.toString());
// 	}
//     }

    // set exit manager to prevent System.exit()
//     static {
// 	exitman = ExitSecurityManager.createAndInstall();
//     }

    // setting longer delay for all tool-tips
    static {
	ToolTipManager.sharedInstance().setDismissDelay (30000);
    }


    /**************************************************************************
     * Default constructor. It loads all panels, connect them and
     * creates the GUI - but does not show it yet.
     **************************************************************************/
    public Dashboard() {
    	log.debug ("----------------------- new dashboard start ------------------------");
	loadIcons();
	loadPanels();
	propertyChannel = new PropertyChannel();
	UIManager.put ("FileChooser.readOnly", Boolean.TRUE);
	dashboard = getComponent (propertyChannel);

	KeyStroke ks = KeyStroke.getKeyStroke (KeyEvent.VK_F1, 0);
	dashboard.getInputMap (JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
	    .put (ks, "HELP");
	dashboard.getActionMap().put ("HELP", new AbstractAction() {
		public void actionPerformed (ActionEvent evt) {
		    // find which panel is currently shown
		    if (shownPanels == null) return;
		    try {
			DashboardPanel currentPanel =
			    shownPanels [tabbedPane.getSelectedIndex()];
			openWindow (getPanelHelpPage (currentPanel),
				"About " + currentPanel.getName() + "...");
		    } catch (Exception e) {
			// here may be Array out of bounds, perhaps...
		    }
		}
	    });
    }

    /*********************************************************************
     * Allow access to individual Dashboard properties that had been
     * loaded when Dashboard started, and that may contain properties
     * used also by individual panels. <p>
     *
     * @param propertyName name of the property that is wanted
     * @return wanted property value, or null if the property was not
     * found; if you want to get back a default value, use rather
     * {@link DashboardConfig#getString DashboardConfig.getString}
     ********************************************************************/
    public static String getProperty (String propertyName) {
	return DashboardConfig.getString (propertyName, null);
    }

    /*********************************************************************
     * Load shared icons.
     ********************************************************************/
    protected void loadIcons() {
	if (aboutIcon == null) aboutIcon = AbstractPanel.loadIcon ("images/smallInfo.gif");
	if (exitIcon == null) exitIcon = AbstractPanel.loadIcon ("images/smallWarning.gif");
	if (lafIcon == null) lafIcon = AbstractPanel.loadIcon ("images/smallLaF.gif");

	if (helpIcon == null) helpIcon = AbstractPanel.loadIcon ("images/smallHelp.gif");
	if (helpIconDis == null) helpIconDis = AbstractPanel.loadIcon ("images/smallHelp_dis.gif");

	if (prefIcon == null) prefIcon = AbstractPanel.loadIcon ("images/smallPref.gif");
	if (prefIconDis == null) prefIconDis = AbstractPanel.loadIcon ("images/smallPref_dis.gif");

	if (pselectIcon == null) pselectIcon = AbstractPanel.loadIcon ("images/smallSelect.gif");
	if (pselectIconDis == null) pselectIconDis = AbstractPanel.loadIcon ("images/smallSelect_dis.gif");

	if (exportIcon == null) exportIcon = AbstractPanel.loadIcon ("images/smallExport.gif");
	if (exportIconDis == null) exportIconDis = AbstractPanel.loadIcon ("images/smallExport_dis.gif");

	if (importIcon == null) importIcon = AbstractPanel.loadIcon ("images/smallImport.gif");
	if (importIconDis == null) importIconDis = AbstractPanel.loadIcon ("images/smallImport_dis.gif");
    }

    /**************************************************************************
     * Load all available Dashboard panels...
     **************************************************************************/
    public void loadPanels() {

	// load all panels; also create for each panel a checkbox used
	// in panels selection
        Enumeration spe = Service.providers (DashboardPanel.class);
	Vector<DashboardPanel> v = new Vector<DashboardPanel>();
	Vector<JCheckBox> c = new Vector<JCheckBox>();
        while (spe.hasMoreElements()) {
            DashboardPanel panel = (DashboardPanel)spe.nextElement();
            v.addElement (panel);
	    c.addElement (createPanelBox (panel));
        }
	panels = new DashboardPanel [ v.size() ];
	v.copyInto (panels);
	panelBoxes = new JCheckBox [ c.size() ];
	c.copyInto (panelBoxes);

	// find which panels were loaded last time
	chooseShownPanels();
    }

    protected void chooseShownPanels() {
	Vector<DashboardPanel> v = new Vector<DashboardPanel>();
	for (int i = 0; i < panels.length; i++) {
	    String prefKey = panels[i].getClass().getName();
	    if ( panels[i].isMandatory() ||
		 PrefsUtils.getNode (Dashboard.class).getBoolean (prefKey,
								  ! panels[i].loadOnlyOnDemand()) )
		v.addElement (panels[i]);
	}
	shownPanels = new DashboardPanel [ v.size() ];
	v.copyInto (shownPanels);
    }

    protected JCheckBox createPanelBox (DashboardPanel panel) {
	final String prefKey = panel.getClass().getName();
	JCheckBox box =
	    new JCheckBox (panel.getName());
	boolean selected =
	    ( panel.isMandatory() ||
	      PrefsUtils.getNode (Dashboard.class).getBoolean (prefKey,
							       ! panel.loadOnlyOnDemand()) );
	PrefsUtils.getNode (Dashboard.class).putBoolean (prefKey, selected);
	box.setSelected (selected);
        box.setFocusPainted (false);
	if (panel.isMandatory())
	    box.setEnabled (false);
	box.addItemListener (new ItemListener() {
		public void itemStateChanged (ItemEvent e) {
		    PrefsUtils.getNode (Dashboard.class)
			.putBoolean (prefKey,
				     (e.getStateChange() == ItemEvent.SELECTED));
		}
	    });
	return box;
    }

    /**************************************************************************
     * Show the main frame.
     **************************************************************************/
    public void show() {
  	final JFrame frame = SwingUtils.createSoftMainFrame (getComponent (propertyChannel),
							     getName());
	frame.addWindowListener (new WindowAdapter() {
		public void windowClosing (WindowEvent e) {
		    if (exitman != null)
			exitman.setExitForbidden (false);
		    System.exit (0);
		}
	    });
	addMenuBar (frame);
	if (panels == null || panels.length == 0) {
	    if (menuBar.getComponentCount() > 1)
		menuBar.remove (1);  // removing 'Setting'
	}
	propertyChannel.addPropertyChangeListener (new PropertyChangeListener() {
		public void propertyChange (PropertyChangeEvent e) {
		    if (DP_REGISTRY_NAMESPACE.equals (e.getPropertyName()))
			titleMenu.setText (CONNECTED_TO +
					   findRegistryName ((String)e.getNewValue()) +
					   " registry");
		}});

	Dimension screenSize = frame.getToolkit().getScreenSize();
	final int width = screenSize.width * 4 / 5;
	final int height = screenSize.height * 5 / 6;

	// set exit manager to prevent System.exit()
	exitman = ExitSecurityManager.createAndInstall();

	// schedule a job for the event-dispatching thread
 	SwingUtilities.invokeLater (new Runnable() {
   		public void run() {
		    SwingUtils.showMainFrame (frame, width, height);
    		}
    	    });
    }

    /**************************************************************************
     *
     **************************************************************************/
    public Dimension getPreferredSize() {
	return new Dimension (800, 640);
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String toString() {
	StringBuffer buf = new StringBuffer();
	for (int i = 0; i < panels.length; i++) {
	    buf.append ((i+1));
	    buf.append (": ");
	    buf.append (panels[i].getName());
	    buf.append ("\n");
	}
	return new String (buf);
    }

    //
    // Implement DashboardPanel interface
    //

    /**************************************************************************
     *
     **************************************************************************/
    public String getName() {
	return "Biomoby Dashboard";
    }

    /**************************************************************************
     *
     **************************************************************************/
    public JLabel getTitle() {
	Icon titleIcon =
	    SwingUtils.createIcon (getProperty (DP_TITLE_ICON), this);
	String titleStr = getProperty (DP_TITLE);
	if (titleStr == null)
	    titleStr = getName();
	JLabel title = new JLabel (titleStr, titleIcon, JLabel.CENTER);
	title.setFont (new Font ("Serif", Font.BOLD, 30));
	return title;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getHelp() {
	String panelClassName = Utils.simpleClassName (this.getClass().getName());
	String help = null;
	try {
	    help = Utils.readResource ("help" +
				       System.getProperty ("file.separator") +
				       panelClassName + ".html",
				       this.getClass());
	    if (help != null)
		return help;
	} catch (IOException e) {
	}
	help = getDescription();
	if (help != null)
	    return help;

	return "";
    }

    /**************************************************************************
     *
     **************************************************************************/
    public URL getHelpURL() {
	return null;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getDescription() {
	return getDescription (DP_DESCRIPTION, DP_DESCRIPTION_FILE);
    }

    /**************************************************************************
     * Return description taken either from the dashboard property
     * 'descriptionPropName', or - if that one is empty - from the
     * property 'descriptionFilePropName'. If none of these two
     * properties exists, or if something wrong happened during
     * reading a file, an empty string is returned.
     **************************************************************************/
    protected String getDescription (String descriptionPropName,
				     String descriptionFilePropName) {
	String description = getProperty (descriptionPropName);
	if (UUtils.isEmpty (description)) {
	    String filename = getProperty (descriptionFilePropName);
	    if (UUtils.isEmpty (filename))
		return "";
	    try {
		return Utils.readResource (filename, this.getClass());
	    } catch (IOException e) {
		return "";
	    }
	}
	return description;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public Icon getIcon() {
	return SwingUtils.createIcon (getProperty (DP_TITLE_ICON), this);
    }

    /**************************************************************************
     *
     **************************************************************************/
    public URL getIconURL() {
	return null;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public boolean isMandatory() {
	return true;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public boolean loadOnlyOnDemand() {
	return false;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public JComponent getComponent (PropertyChannel propertyChannel) {

	if (dashboard != null)
	    return dashboard;

	if (useLoadMonitor) {
	    ProgressView.monitor = new ProgressView (shownPanels.length);
	    ProgressView.monitor.show ("Welcome to Biomoby Dashboard");
	}

	
	// create itself
	JPanel p = new JPanel (new GridBagLayout(), true);
	bgcolor = GraphColours.getColour (getProperty (DP_BGCOLOR),
					  p.getBackground());
	p.setBackground (bgcolor);

        header = getHeader();
	tabbedPane = new JTabbedPane();
	tabbedPane.addChangeListener (this);
	addPanels();
        StatusBar statusBar = getStatusBar();
	propertyChannel.addPropertyChangeListener (statusBar);

	if (useLoadMonitor)
	    ProgressView.monitor.destroy();

	if (panels == null || panels.length == 0) {
	    JLabel labNoPanels = new JLabel
		("<html><center><font color='red' size='+2'>" +
		 "Sorry, but no Dashboard panel was specified.<br><p>" +
		 "The most probable cause is that no Dasboard configuration file was found.<br>" +
		 "Please run '<font color='black'>ant dashboard</font>' first.<br>" +
		 "</center></font>");

	    SwingUtils.addComponent (p, labNoPanels, 0, 0, 1, 1, AbstractPanel.CENTER, AbstractPanel.CENTER, 1.0, 1.0);
	} else {

	    // put it all together
	    SwingUtils.addComponent (p, header,     0, 0, 1, 1, AbstractPanel.HORI, AbstractPanel.NWEST, 1.0, 0.0);
	    SwingUtils.addComponent (p, tabbedPane, 0, 1, 1, 1, AbstractPanel.BOTH, AbstractPanel.NWEST, 1.0, 1.0);
	    SwingUtils.addComponent (p, statusBar,  0, 2, 1, 1, AbstractPanel.HORI, AbstractPanel.WEST,  1.0, 0.0, BREATH_SMALL);
	}

	return p;
    }

    /**************************************************************************
     * Load all panels labeled as to be shown.
     **************************************************************************/
    protected void addPanels() {
// 	int progressCount = 0;
// 	if (useLoadMonitor) {
// 	    for (int i = 0; i < shownPanels.length; i++) {
// 		if (shownPanels[i] instanceof AbstractPanel) {
// 		    (Abstract
//     int progressItemsCount();
	for (int i = 0; i < shownPanels.length; i++) {
	    String name = shownPanels[i].getName();
	    if (useLoadMonitor)
		ProgressView.monitor.setTextAndAdd ("Loading " + name + "...");
	    tabbedPane.addTab ("<html>" + name + "<br>&nbsp;",
			       shownPanels[i].getIcon(),
			       shownPanels[i].getComponent (propertyChannel));
	    if (i < 10)
		tabbedPane.setMnemonicAt (i, (0x30 + i + 1));
	}
    }

    /**************************************************************************
     *
     **************************************************************************/
    protected DashboardHeader getHeader() {
	DashboardHeader h = new DashboardHeader();
	if (shownPanels.length > 0)
	    h.setPanelTitle (shownPanels[0].getTitle());
	return h;
    }

    class DashboardHeader extends JPanel {
	public DashboardHeader() {
	    super (new GridBagLayout());
	    setBackground (bgcolor);
	    JLabel mainTitle = getTitle();
	    JLabel panelTitle = new JLabel();

	    // put it all together (keep 'panelTitle' always as the 3nd component)
	    Component glue = Box.createHorizontalGlue();
	    SwingUtils.addComponent (this, mainTitle,  0, 0, 1, 1, AbstractPanel.NONE, AbstractPanel.NWEST, 0.0, 0.0, BREATH);
	    SwingUtils.addComponent (this, glue,       1, 0, 1, 1, AbstractPanel.HORI, AbstractPanel.NWEST, 1.0, 0.0);
	    SwingUtils.addComponent (this, panelTitle, 2, 0, 1, 1, AbstractPanel.NONE, AbstractPanel.NEAST, 0.0, 0.0, BREATH);
	}
	public void setPanelTitle (JLabel newTitle) {
	    if (newTitle == null) newTitle = new JLabel();
 	    remove (2);
	    SwingUtils.addComponent (this, newTitle,   2, 0, 1, 1, AbstractPanel.NONE, AbstractPanel.NEAST, 0.0, 0.0, BREATH);
	    AwtUtils.redisplay (this);
	}
    }  

    /**************************************************************************
     * Return a component representing status bar (usually placed at
     * the bottom of a dashboard).
     **************************************************************************/
    protected StatusBar getStatusBar() {
	return new StatusBar
	    ("Dashboard is ready. Fasten your seat belts - it's going to be a bumpy ride.");
    }

    class StatusBar extends JLabel implements PropertyChangeListener {
	DateFormat df = DateFormat.getTimeInstance();
	Icon myIcon;
	public StatusBar (String text) {
	    super (text);
	}
	public void propertyChange (PropertyChangeEvent e) {
	    String prop = e.getPropertyName();
	    if (prop == null) return;     // no interest in non-specific changes
	    Object value = e.getNewValue();
	    if (value == null) return;   // no interest in non-defined  new values
	    if (prop.equalsIgnoreCase (DP_STATUS_MSG))
		setText ("[" + df.format (new Date()) + "] " + value.toString());
	}
	public Icon getIcon() {
	    if (myIcon == null)
		myIcon = SwingUtils.createIcon ("images/smallDone.gif", Dashboard.class);
	    return myIcon;
	}
    }

    /**************************************************************************
     * Create and add a menu bar to the dashboard.
     **************************************************************************/
    protected void addMenuBar (JFrame frame) {
 	menuBar = new JMenuBar();
	frame.setJMenuBar (menuBar);

	// main menu items
	String title = null;
	if (propertyChannel != null)
	    title = (String)propertyChannel.get (DP_REGISTRY_NAMESPACE);
	if (title == null) title = "";
	else title = CONNECTED_TO + findRegistryName (title) + " registry";

	JMenu dashmenu = createMenu ("Dashboard", KeyEvent.VK_D, null, null);
	JMenu setting  = createMenu ("Setting",   KeyEvent.VK_S, null, null);
	titleMenu      = createMenu (title,       -1,            null, null);
	helpMenu       = createMenu ("Help",      KeyEvent.VK_H, null, null);
	menuBar.add (dashmenu);
	menuBar.add (setting);
	menuBar.add (titleMenu);
	menuBar.add (Box.createHorizontalGlue());
	menuBar.add (helpMenu);

	// submenus' menu items
	dashmenu.add
	    (createMenuItem (new AbstractAction ("Exit") {
		    public void actionPerformed (ActionEvent e) {
			if (exitman != null)
			    exitman.setExitForbidden (false);
			System.exit (0);
		    }
		}, AC_EXIT, KeyEvent.VK_X, exitIcon, null));

	setting.add
	    (createMenuItem (new AbstractAction ("Panels selection") {
		    public void actionPerformed (ActionEvent e) {
			onPanelsSelection();
		    }
		}, AC_PSELECT, KeyEvent.VK_P, pselectIcon, pselectIconDis));

	setting.add (LookAndFeelUtils.getLookAndFeelMenu ("Look & Feel", lafIcon, frame));

	setting.addSeparator();
	setting.add
	    (createMenuItem (new AbstractAction ("Manage My Dashboard") {
		    public void actionPerformed (ActionEvent e) {
		    }
		}, AC_MYPREF, KeyEvent.VK_M, prefIcon, prefIconDis));
	setEnabledMenuItem (AC_MYPREF, false);
	setting.add
	    (createMenuItem (new AbstractAction ("Export My Dashboard") {
		    public void actionPerformed (ActionEvent e) {
		    }
		}, AC_EXPORT, KeyEvent.VK_E, exportIcon, exportIconDis));
	setEnabledMenuItem (AC_EXPORT, false);
	setting.add
	    (createMenuItem (new AbstractAction ("Import My Dashboard") {
		    public void actionPerformed (ActionEvent e) {
		    }
		}, AC_IMPORT, KeyEvent.VK_I, importIcon, importIconDis));

	setEnabledMenuItem (AC_IMPORT, false);
	helpMenu.add
	    (createMenuItem (new AbstractAction ("About...") {
		    public void actionPerformed (ActionEvent e) {
			openWindow (getWelcomePage (null), "About Dashboard...");
		    }
		}, AC_ABOUT, KeyEvent.VK_A, aboutIcon, null));
	helpMenu.add
	    (createMenuItem (new AbstractAction ("Dashboard") {
		    public void actionPerformed (ActionEvent e) {
			openWindow (getPanelHelpPage (Dashboard.this),
				    "About " + getName() + "...");
		    }
		}, AC_HELP, KeyEvent.VK_D, helpIcon, null));
	helpMenu.addSeparator();
	countOfPermanentHelpItems = helpMenu.getMenuComponentCount();

	putPanelsToHelp();
    }

    /*********************************************************************
     * Removes all panel helps from the help menu and put there back
     * those are currently selcted to be visible.
     ********************************************************************/
    protected void putPanelsToHelp() {

	// remove all panel helps
	int count = helpMenu.getMenuComponentCount();
	while (count > countOfPermanentHelpItems) {
	    helpMenu.remove (count - 1);
	    count = helpMenu.getMenuComponentCount();
	}

	// add all loaded panel helps
	int maxIconWidth = 0;
	for (int i = 0; i < shownPanels.length; i++) {
	    final DashboardPanel panel = shownPanels[i];
	    final String name = panel.getName();
	    final Icon icon = panel.getIcon();
	    if (icon != null)
		maxIconWidth = Math.max (maxIconWidth, icon.getIconWidth());
	    helpMenu.add
		(createMenuItem (new AbstractAction (name) {
			public void actionPerformed (ActionEvent e) {
			    openWindow (getPanelHelpPage (panel),
					"About " + name + "...");
			}
		    }, name, -1, icon, null));
	}
	// TBD: setting gap between icon and text does not work... why?
	//      (it does not seem to be used al all - just look into source code)
	if (maxIconWidth > 0) {
	    count = helpMenu.getMenuComponentCount();
	    for (int i = countOfPermanentHelpItems; i < count; i++) {
		Component item = helpMenu.getMenuComponent (i);
		if (item instanceof JMenuItem) {
		    Icon icon = ((JMenuItem)item).getIcon();
		    if (icon != null) {
			((JMenuItem)item).setIconTextGap (maxIconWidth - icon.getIconWidth() + 4);
		    }
		}
	    }
	}
    }

    /*********************************************************************
     * Create Dasboard's welcome page. Include also a progress bar -
     * if given.
     ********************************************************************/
    protected JPanel getWelcomePage (JProgressBar progressBar) {
	JPanel p = new JPanel (new GridBagLayout());
	p.setBackground (bgcolor);

	JScrollPane lDesc = createPane (getWelcomePageContents());

	String contact = getProperty (DP_CONTACT);
	if (UUtils.isEmpty (contact)) contact = "";
	JLabel lContact = new JLabel ("Contact: " + contact);

	JLabel title = getTitle();
	JLabel icon =
	    new JLabel ("",
			SwingUtils.createIcon (getProperty (DP_ICON), this),
			JLabel.CENTER);	  

	// put it together
	SwingUtils.addComponent (p, icon,     0, 0, 1, 1, AbstractPanel.NONE, AbstractPanel.NWEST, 0.0, 0.0, BREATH);
	SwingUtils.addComponent (p, title,    1, 0, 1, 1, AbstractPanel.NONE, AbstractPanel.NEAST, 0.0, 0.0, BREATH);
	SwingUtils.addComponent (p, lDesc,    0, 1, 2, 1, AbstractPanel.BOTH, AbstractPanel.NWEST, 1.0, 1.0, BREATH);
	SwingUtils.addComponent (p, lContact, 0, 2, 2, 1, AbstractPanel.NONE, AbstractPanel.NWEST, 0.0, 0.0);
	return p;
    }

    /**************************************************************************
     *
     **************************************************************************/
    protected String getWelcomePageContents() {
	StringBuffer buf = new StringBuffer (500);
	buf.append (getDescription (DP_DESCRIPTION, DP_DESCRIPTION_FILE));
	buf.append ("<p>");
	buf.append (getDescription (DP_P_DESCRIPTION, DP_P_DESCRIPTION_FILE));
	buf.append ("<h2>Dashboard panels</h2>");

	for (int i = 0; i < shownPanels.length; i++) {
	    URL iconURL = shownPanels[i].getIconURL();
	    if (iconURL != null) {
		buf.append ("<img src='");
		buf.append (iconURL.toString());
		buf.append ("' border='0' hspace='5' align='left'></img>");
	    }
	    buf.append ("<b>");
	    // TBD: uncomment back URLs when there is a 'Back' button there
// 	    URL helpURL = shownPanels[i].getHelpURL();
// 	    if (helpURL == null) {
 		buf.append (shownPanels[i].getName());
// 	    } else {
// 		buf.append ("<a href='");
// 		buf.append (helpURL.toString());
// 		buf.append ("'>");
// 		buf.append (shownPanels[i].getName());
// 		buf.append ("</a>");
// 	    }
	    buf.append ("</b><blockquote>");
	    buf.append (shownPanels[i].getDescription());
	    buf.append ("</blockquote><p>");
	}
	return new String (buf);
    }

    /*********************************************************************
     * Create panel help page (using, of course, data from panel's own
     * method 'getHelp()'.
     ********************************************************************/
    protected JPanel getPanelHelpPage (DashboardPanel panel) {
	JPanel p = new JPanel (new GridBagLayout());
	p.setBackground (bgcolor);

	String help = panel.getHelp();
	if (help == null)
	    help = panel.getDescription();
	JScrollPane pHelp = createPane (help);
	SwingUtils.addComponent (p, pHelp,    0, 0, 1, 1, AbstractPanel.BOTH, AbstractPanel.NWEST, 1.0, 1.0, BREATH);
	return p;
    }

    static final String PANEL_PROTOCOL = "panel://";

    /*********************************************************************
     * Create a scrollable panel with given 'contents' that is able to
     * react to the hyperlinks. The 'contents' is treated as an HTML
     * text.
     ********************************************************************/
    JScrollPane createPane (String contents) {
	final JEditorPane pane = new JEditorPane();
	pane.setBackground (bgcolor);
	pane.setEditable (false);
	pane.setContentType ("text/html");
	pane.setText (contents);
	pane.addHyperlinkListener (new HyperlinkListener() {
		public void hyperlinkUpdate (HyperlinkEvent event) {
		    if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
			try {
			    String link = event.getDescription();
			    if (link != null && link.startsWith (PANEL_PROTOCOL)) {
				boolean panelFound = false;
				String panelClassName = link.substring (PANEL_PROTOCOL.length());
				for (int i = 0; i < panels.length; i++) {
				    if (panels[i].getClass().getName().equals (panelClassName)) {
					pane.setText (panels[i].getHelp());
					panelFound = true;
					break;
				    }
				}
				if (!panelFound)
				    AbstractPanel.error ("Can't follow link to panel " +
							 panelClassName +
							 ".\n Perhaps the panel is not loaded.\n");
			    } else {
				pane.setPage (event.getURL());
			    }
			} catch (IOException e) {
			    String msg = (event.getURL() == null ?
					  event.getDescription() :
					  event.getURL().toExternalForm());
			    AbstractPanel.error ("Can't follow link to " + msg + ": " + e);
			}
		    }
		}});
	JScrollPane scroll = new JScrollPane (pane);
	scroll.setBorder (BorderFactory.createEmptyBorder (0, 0, 0, 0));
	return scroll;
    }

    /*********************************************************************
     * Create a panel with given 'contents'. The 'contents' is treated
     * as an HTML text (but no hyperlinks are recognized).
     ********************************************************************/
    JEditorPane createSimplePane (String contents) {
	final JEditorPane pane = new JEditorPane();
	pane.setEditable (false);
	pane.setContentType ("text/html");
	pane.setText (contents);
// 	pane.setBorder (BorderFactory.createEmptyBorder (0, 0, 0, 0));
	return pane;
    }

    /**************************************************************************
     * Craete and show a non-modal window with the given component and name.
     **************************************************************************/
    protected void openWindow (final JComponent component,
			       final String name) {

        // schedule a job for the event-dispatching thread
        SwingUtilities.invokeLater (new Runnable() {
		public void run() {
		    JFrame frame = SwingUtils.createSoftMainFrame (component, name);
		    Dimension screenSize = frame.getToolkit().getScreenSize();
		    int width = screenSize.width / 3;
		    int height = screenSize.height * 2 / 3;
		    SwingUtils.showMainFrame (frame, width, height);
 		}
 	    });
    }


    /*********************************************************************
     * A panel that lets select which panels should be displayed.
     ********************************************************************/
    protected void onPanelsSelection() {
	int result =
	    JOptionPane.showConfirmDialog (null,
					   getPanelsSelection(),
					   "Selecting panels...",
					   JOptionPane.OK_CANCEL_OPTION,
					   JOptionPane.QUESTION_MESSAGE,
					   AbstractPanel.confirmIcon);
        if (result == JOptionPane.OK_OPTION) {
	    chooseShownPanels();
	    tabbedPane.removeAll();
	    useLoadMonitor = false;
	    addPanels();
	    putPanelsToHelp();
	}
    }

    /*********************************************************************
     * A panel that lets select which panels should be displayed.
     ********************************************************************/
    protected JPanel getPanelsSelection() {
	JPanel p = new JPanel (new GridBagLayout());
	JLabel prologue = new JLabel (SELECTION_PROLOGUE);
	SwingUtils.addComponent (p, prologue, 0, 0, 2, 1,
				 AbstractPanel.NONE, AbstractPanel.NWEST, 0.0, 0.0, BREATH_BOT);

	for (int i = 0; i < panelBoxes.length; i++) {
	    SwingUtils.addComponent (p, panelBoxes[i], 0, i+1, 2, 1,
				     AbstractPanel.NONE, AbstractPanel.NWEST, 0.0, 0.0);
	}

	return p;
    }

    /*********************************************************************
     * Some delegated methods...
     ********************************************************************/
    private void setEnabledMenuItem (String actionCommand,
				     boolean enabled) {
	AbstractPanel.setEnabledMenuItem (menuBar, actionCommand, enabled);
    }
//    private static JMenuItem createMenuItem (AbstractAction action,
//					     String actionCommand) {
//	return AbstractPanel.createMenuItem (action, actionCommand);
//    }
    private static JMenuItem createMenuItem (AbstractAction action,
					     String actionCommand,
					     int mnemonic,
					     Icon icon,
					     Icon disabledIcon) {
	return AbstractPanel.createMenuItem (action, actionCommand,
					     mnemonic, icon, disabledIcon);
    }
    private static JMenu createMenu (String name,
				     int mnemonic,
				     Icon icon,
				     Icon disabledIcon) {
	return AbstractPanel.createMenu (name, mnemonic,
					 icon, disabledIcon);
    }

    /**************************************************************************
     * Dashboard is listening to the changes of panels in order to
     * repaint the header with a label of a currently selected panel.
     **************************************************************************/
    public void stateChanged (ChangeEvent e) {
	int panelIndex = tabbedPane.getSelectedIndex();
	if (panelIndex < 0 || panelIndex >= shownPanels.length)
	    header.setPanelTitle (null);
	else
	    header.setPanelTitle (shownPanels[panelIndex].getTitle());
    }

    /**************************************************************************
     * Look into a registry of registries and return a synonym of a
     * registry with the given namespace. If not found, return the
     * namespace.
     **************************************************************************/
    private String findRegistryName (String namespace) {
	final Registries regList = RegistriesList.getInstance();
	Registry[] regs = regList.getAll();
	for (int i = 0; i < regs.length; i++) {
	    if (namespace.equals (regs[i].getNamespace()))
		return regs[i].getSynonym();
	}
	return namespace;
    }

    /**************************************************************************
     *
     * An entry point...
     *
     **************************************************************************/
    public static void main (String[] args) {
	if (args.length > 0 && args[0].equals ("-nop"))
	    Dashboard.useLoadMonitor = false;
	new Dashboard().show();
    }

}
