/*
 * The tracker package defines a set of video/image analysis tools
 * built on the Open Source Physics framework by Wolfgang Christian.
 *
 * Copyright (c) 2019  Douglas Brown
 *
 * Tracker is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tracker is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tracker; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at <http://www.gnu.org/copyleft/gpl.html>
 *
 * For additional Tracker information and documentation, please see
 * <http://physlets.org/tracker/>.
 */
package org.opensourcephysics.cabrillo.tracker;

import java.beans.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.*;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.Document;

import org.opensourcephysics.controls.*;
import org.opensourcephysics.display.*;
import org.opensourcephysics.media.core.*;
import org.opensourcephysics.tools.*;
import org.opensourcephysics.tools.Launcher.HTMLPane;

/**
 * This is the main frame for Tracker.
 *
 * @author Douglas Brown
 */
public class TFrame extends OSPFrame implements PropertyChangeListener {

  // static fields
  protected static String helpPath = "/org/opensourcephysics/cabrillo/tracker/resources/help/"; //$NON-NLS-1$
  protected static String helpPathWeb = "https://physlets.org/tracker/help/"; //$NON-NLS-1$
  static Color yellow = new Color(255, 255, 105);

  // instance fields
  private JToolBar playerBar;
  private JPopupMenu popup = new JPopupMenu();
  private JMenuItem closeItem;
  private JMenuBar defaultMenuBar;
  private JMenu recentMenu;
  // maps tab panel->Object[5] {main view, views, split panes, toolbar, menubar}
  private Map<JPanel, Object[]> tabs = new HashMap<JPanel, Object[]>();
  protected JTabbedPane tabbedPane;
  protected JTextPane notesTextPane;
  protected Action saveNotesAction;
  protected JButton cancelNotesDialogButton, closeNotesDialogButton;
  protected JCheckBox displayWhenLoadedCheckbox;
  protected JDialog notesDialog;
  protected JDialog helpDialog;
  protected LibraryBrowser libraryBrowser;
  protected Launcher helpLauncher;
  protected JDialog dataToolDialog;
  protected TrackerPanel prevPanel;
  protected double defaultRightDivider = 0.7;
  protected double defaultBottomDivider = 0.5;
  protected FileDropHandler fileDropHandler;
  protected Action openRecentAction;
  protected boolean splashing=true;
  protected ArrayList<String> loadedFiles = new ArrayList<String>();
  protected boolean anglesInRadians = Tracker.isRadians;
  protected File tabsetFile; // used when saving tabsets
  protected int framesLoaded, prevFramesLoaded; // used when loading xuggle videos
//  protected JProgressBar monitor;
  protected PrefsDialog prefsDialog;
  protected ClipboardListener clipboardListener;
  protected boolean alwaysListenToClipboard;

  /**
   * Constructs an empty TFrame.
   */
  public TFrame() {
    super("Tracker"); //$NON-NLS-1$
    setName("Tracker"); //$NON-NLS-1$
    if (Tracker.TRACKER_ICON != null) 
    	setIconImage(Tracker.TRACKER_ICON.getImage());
    // set default close operation
    setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    createGUI();    
    // size and position this frame
    pack();
    
    // set transfer handler on tabbedPane
    fileDropHandler = new FileDropHandler(this);
    tabbedPane.setTransferHandler(fileDropHandler);
    
    // set size and limit maximized size so taskbar not covered
    GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment();
    Rectangle screenRect = e.getMaximumWindowBounds();
    setMaximizedBounds(screenRect);
    double extra = FontSizer.getFactor(Tracker.preferredFontLevel)-1;
    int w = Math.min(screenRect.width, (int)(1024+extra*800));
    int h = Math.min(screenRect.height, 3*w/4);
    Dimension dim = new Dimension(w, h);
    setSize(dim);
    // center frame on the screen
    int x = (screenRect.width-dim.width)/2;
    int y = (screenRect.height-dim.height)/2;
    setLocation(x, y);
    TrackerRes.addPropertyChangeListener("locale", this); //$NON-NLS-1$
  }

  /**
   * Constructs a TFrame with the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   */
  public TFrame(TrackerPanel trackerPanel) {
    this();
    addTab(trackerPanel);
  }

  /**
   * Adds a tab that displays the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   */
  public void addTab(final TrackerPanel trackerPanel) {
    if (getTab(trackerPanel) >= 0) return; // tab already exists
    // listen for changes that affect tab title
    trackerPanel.addPropertyChangeListener("datafile", this); //$NON-NLS-1$
    trackerPanel.addPropertyChangeListener("video", this); //$NON-NLS-1$
    // set up trackerPanel to listen for angle format property change
    addPropertyChangeListener("radian_angles", trackerPanel); //$NON-NLS-1$
    // create the tab
    JPanel panel = new JPanel(new BorderLayout());
    // create the tab panel components
    Tracker.setProgress(30);
    MainTView mainView = getMainView(trackerPanel);
    Container[] views = createViews(trackerPanel);
    JSplitPane[] panes = getSplitPanes(trackerPanel);
    Tracker.setProgress(50);
    TToolBar toolbar = getToolBar(trackerPanel);
    Tracker.setProgress(60);
    TMenuBar menubar = getMenuBar(trackerPanel);
    TTrackBar trackbar = getTrackBar(trackerPanel);
    // put the components into the tabs map
    Object[] array = new Object[] {mainView, views, panes, toolbar, menubar, trackbar};
    tabs.put(panel, array);
    // add the tab
    setIgnoreRepaint(true);
    String name = trackerPanel.getTitle();
    synchronized(tabbedPane) {
    	tabbedPane.addTab(name, panel);
    	int tab = getTab(trackerPanel);
      tabbedPane.setToolTipTextAt(tab, trackerPanel.getToolTipPath());
    }
    // from now on trackerPanel's top level container is this TFrame,
    // so trackerPanel.getFrame() method will return non-null
    
    boolean hasViews = (trackerPanel.viewsProperty != null);
    // select the views
    if (trackerPanel.selectedViewsProperty != null) {
    	if (!hasViews) { // no custom views, just load and show selected views
        XMLControl[] controls = trackerPanel.selectedViewsProperty.getChildControls();
        for (int i = 0; i < Math.min(controls.length, views.length); i++) {
        	if (controls[i]==null) continue;
          if (views[i] instanceof TViewChooser) {
            TViewChooser chooser = (TViewChooser)views[i];
            Class<?> viewType = controls[i].getObjectClass();
            TView view = chooser.getView(viewType);
            if (view != null) { 
              if (chooser.getSelectedView() != view) {
                chooser.setSelectedView(view);
              }
              controls[i].loadObject(view);
            }
          }
  	    }    	
    	}
    	else { // has custom views
    		Iterator<Object> it = trackerPanel.selectedViewsProperty.getPropertyContent().iterator();
    		int i = -1;
    		while (it.hasNext() && i < views.length) {
    			i++;
          if (views[i] instanceof TViewChooser) {
            TViewChooser chooser = (TViewChooser)views[i];
          	XMLProperty next = (XMLProperty)it.next();
          	if (next==null) continue;
          	String viewName = (String)next.getPropertyContent().get(0);
          	chooser.setSelectedView(chooser.getView(viewName));
          }
    		}
    	}
      trackerPanel.selectedViewsProperty = null;
    }
    // load the views
    if (hasViews) {
    	java.util.List<Object> arrayItems = trackerPanel.viewsProperty.getPropertyContent();
    	Iterator<Object> it = arrayItems.iterator();
    	while (it.hasNext()) {
    		XMLProperty next = (XMLProperty)it.next();
    		if (next==null) continue;
    		String index = next.getPropertyName().substring(1);
    		index = index.substring(0, index.length()-1);
    		int i = Integer.parseInt(index);
        if (i<views.length && views[i] instanceof TViewChooser) {
          XMLControl[] elements = next.getChildControls();
          TViewChooser chooser = (TViewChooser)views[i];
          for (int j = 0; j < elements.length; j++) {
            Class<?> viewType = elements[j].getObjectClass();
            TView view = chooser.getView(viewType);
            if (view != null) { 
              elements[j].loadObject(view);
            }
          }
        }
    	}
      trackerPanel.viewsProperty = null;
    }
    setViews(trackerPanel, views);
    initialize(trackerPanel);
  	FontSizer.setFonts(panel, FontSizer.getLevel());
    // inform all tracks of current angle display format
    for (TTrack track: trackerPanel.getTracks()) {
    	track.setAnglesInRadians(anglesInRadians);
    }
    setIgnoreRepaint(false);
    trackerPanel.changed = false;

    Timer timer = new Timer(500, new ActionListener() {
      public void actionPerformed(ActionEvent e) {
		    // close blank tab at position 0, if any
		    if (getTabCount()>1) {
			  	TrackerPanel existingPanel = getTrackerPanel(0);
			    if (tabbedPane.getTitleAt(0).equals(
			        TrackerRes.getString("TrackerPanel.NewTab.Name")) //$NON-NLS-1$
			        && !existingPanel.changed) {
			      removeTab(existingPanel);
			    }
		    }
        trackerPanel.refreshTrackData();
        refresh();
      }
    });
		timer.setRepeats(false);
		timer.start();

//    Runnable runner = new Runnable() {
//    	public void run() {
//		    // close blank tab at position 0, if any
//		    if (getTabCount()>1) {
//			  	TrackerPanel existingPanel = getTrackerPanel(0);
//			    if (tabbedPane.getTitleAt(0).equals(
//			        TrackerRes.getString("TrackerPanel.NewTab.Name")) //$NON-NLS-1$
//			        && !existingPanel.changed) {
//			      removeTab(existingPanel);
//			    }
//		    }
//        trackerPanel.refreshTrackData();
//        refresh();
//    	}
//    };
//    SwingUtilities.invokeLater(runner);
  }
  
  /**
   * Removes all tabs.
   */
  public void removeAllTabs() {
    // remove all tabs
    for (int i = getTabCount()-1; i >= 0; i--) {
    	// save/close tabs in try/catch block so always closes
      try {
				if (!getTrackerPanel(i).save()) {
				  // action is cancelled
				  return;
				}
				removeTab(getTrackerPanel(i));
			} catch (Exception ex) {
			}
    }
  }

  /**
   * Removes the tab that displays the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   */
  public void removeTab(TrackerPanel trackerPanel) {
    int tab = getTab(trackerPanel);
    if (tab == -1) return; // tab doesn't exist
    if (!trackerPanel.save()) return; // user cancelled
    
    trackerPanel.selectedPoint = null;
    trackerPanel.selectedStep = null;
    trackerPanel.selectedTrack = null;
    
    // hide the info dialog if trackerPanel is in selected tab
    if (tab==getSelectedTab()) {
    	notesDialog.setVisible(false);
    }
    
    // inform listeners
    firePropertyChange("tab", trackerPanel, null); //$NON-NLS-1$
    
    // clean up mouse handler
    trackerPanel.mouseHandler.selectedTrack = null;
    trackerPanel.mouseHandler.p = null;
    trackerPanel.mouseHandler.iad = null;
    
    // clear filter classes
    trackerPanel.clearFilters();
    // remove transfer handler
    trackerPanel.setTransferHandler(null);
    
    // remove property change listeners
    trackerPanel.removePropertyChangeListener("datafile", this); //$NON-NLS-1$
    trackerPanel.removePropertyChangeListener("video", this); //$NON-NLS-1$
    removePropertyChangeListener("radian_angles", trackerPanel); //$NON-NLS-1$
    
    // dispose of the track control, clip inspector and player bar
    TrackControl.getControl(trackerPanel).dispose();
    ClipInspector ci = trackerPanel.getPlayer().getVideoClip().getClipInspector();
    if (ci!=null) {
    	ci.dispose();
    }

    // set the video to null
    trackerPanel.setVideo(null);
    
    // dispose of TViewChoosers and TViews
    Container[] views = getViews(trackerPanel);
    for (int i = 0; i < views.length; i++) {
      if (views[i] instanceof TViewChooser) {
      	TViewChooser chooser = (TViewChooser)views[i];
      	chooser.dispose();
      }
    }

    // clean up main view
    MainTView mainView = getMainView(trackerPanel);
    mainView.dispose();
    trackerPanel.setScrollPane(null);
    
    // clear the drawables AFTER disposing of main view
    ArrayList<TTrack> tracks = trackerPanel.getTracks();
    trackerPanel.clear();
    for (TTrack track: tracks) {
    	track.dispose();
    }
    
    // get the tab panel and remove components from it
    JPanel tabPanel = (JPanel)tabbedPane.getComponentAt(tab);
    tabPanel.removeAll();
    
    // remove the tab
    synchronized(tabbedPane) {
    	tabbedPane.remove(tabPanel);
    }
    
    // dispose of trackbar, toolbar, menubar AFTER removing tab
    TToolBar toolbar = getToolBar(trackerPanel);
    toolbar.dispose();
    TMenuBar menubar = getMenuBar(trackerPanel);
    menubar.dispose();
    TTrackBar trackbar = getTrackBar(trackerPanel);
    trackbar.dispose();
    JSplitPane[] panes = getSplitPanes(trackerPanel);
    for (int i=0; i<panes.length; i++) {
    	JSplitPane pane = panes[i];
    	pane.removeAll();
    }
    for (int i=0; i<panes.length; i++) {
    	panes[i] = null;
    }
    
    // remove the components from the tabs map
    Object[] array = tabs.get(tabPanel);
//  array is {mainView, views, panes, toolbar, menubar, trackbar};
    if (array != null) {
      for (int i=0; i< array.length; i++) {
      	array[i] = null;
      }
    }    
    tabs.remove(tabPanel);
    
    TActions.getActions(trackerPanel).clear();
    TActions.actionMaps.remove(trackerPanel);
  	if (prefsDialog!=null) {
  		prefsDialog.trackerPanel = null;
  	}
  	Undo.undomap.remove(trackerPanel);
    
    trackerPanel.dispose();
        
    // change menubar and show floating player of newly selected tab, if any
    array = tabs.get(tabbedPane.getSelectedComponent());
    if (array != null) {
      setJMenuBar( (JMenuBar) array[4]);
      ((TTrackBar)array[5]).refresh();
      playerBar = ( (MainTView) array[0]).getPlayerBar();
      Container frame = playerBar.getTopLevelAncestor();
      if (frame != null && frame != TFrame.this)
        frame.setVisible(true);
    }
    else {
    	// show defaultMenuBar
    	refreshOpenRecentMenu(recentMenu);
      setJMenuBar(defaultMenuBar);
    }
  }

  /**
   * Returns the tab index for the specified tracker panel,
   * or -1 if no tab is found.
   *
   * @param trackerPanel the tracker panel
   * @return the tab index
   */
  public int getTab(TrackerPanel trackerPanel) {
    for (int i = 0; i < getTabCount(); i++) {
      Object[] array = tabs.get(tabbedPane.getComponentAt(i));
      if (array == null) return -1;
      MainTView mainView = (MainTView)array[0];
      if (mainView.getTrackerPanel() == trackerPanel) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Returns the tab index for the specified data file,
   * or -1 if no tab is found.
   *
   * @param dataFile the data file used to load the tab
   * @return the tab index
   */
  public int getTab(File dataFile) {
  	if (dataFile==null) return -1;
  	try {
			String path = dataFile.getCanonicalPath();
			for (int i = getTabCount()-1; i >=0; i--) {
			  Object[] array = tabs.get(tabbedPane.getComponentAt(i));
			  if (array == null) return -1;
			  MainTView mainView = (MainTView)array[0];
			  File file = mainView.getTrackerPanel().getDataFile();
			  if (file!=null && path.equals(file.getCanonicalPath())) {
			    return i;
			  }
			}
		} catch (IOException e) {
		}
    return -1;
  }

  /**
   * Gets the tab count.
   *
   * @return the tab count
   */
  public int getTabCount() {
    return tabbedPane.getTabCount();
  }

  /**
   * Gets the selected tab index.
   *
   * @return the tab index
   */
  public int getSelectedTab() {
    return tabbedPane.getSelectedIndex();
  }

  /**
   * Sets the selected tab index.
   *
   * @param tab the tab index
   */
  public void setSelectedTab(int tab) {
    if (tab < 0 || tab >= getTabCount()) return;
    tabbedPane.setSelectedIndex(tab);
    TrackerPanel trackerPanel = getTrackerPanel(tab);
    if (trackerPanel!=null) 
    	trackerPanel.refreshNotesDialog();
  }

  /**
   * Sets the selected tab specified by tracker panel.
   *
   * @param trackerPanel the tracker panel
   */
  public void setSelectedTab(TrackerPanel trackerPanel) {
    setSelectedTab(getTab(trackerPanel));
  }

  /**
   * Gets the tracker panel at the specified tab index.
   *
   * @param tab the tab index
   * @return the tracker panel
   */
  public TrackerPanel getTrackerPanel(int tab) {
    if (tab < 0 || tab >= tabbedPane.getTabCount()) return null;
    Object[] array = tabs.get(tabbedPane.getComponentAt(tab));
    MainTView mainView = (MainTView)array[0];
    return mainView.getTrackerPanel();
  }

  /**
   * Gets the title of the specified tab.
   *
   * @param tab the tab index
   * @return the title
   */
  public String getTabTitle(int tab) {
    return tabbedPane.getTitleAt(tab);
  }

  /**
   * Refreshes the tab for the specified tracker panel.
   *
   * @param panel the tracker panel
   */
  public void refreshTab(TrackerPanel panel) {
    int tab = getTab(panel);
    tabbedPane.setTitleAt(tab, panel.getTitle());
    tabbedPane.setToolTipTextAt(tab, panel.getToolTipPath());
  }

  /**
   * Sets the title of the specified tab.
   *
   * @param tab the tab index
   * @param title the title
   */
  public void setTabTitle(int tab, String title) {
    tabbedPane.setTitleAt(tab, title);
  }

  /**
   * Sets the view for a specified tracker panel and pane.
   *
   * @param trackerPanel the tracker panel
   * @param view the new view
   * @param pane the pane number
   */
  public void setView(TrackerPanel trackerPanel, Container view, int pane) {
    if (view == null || pane > 3) return;
    Container[] views = getViews(trackerPanel);
    views[pane] = view;
    setViews(trackerPanel, views);
  }

  /**
   * Sets the views for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @param newViews an array of up to 4 views
   */
  public void setViews(TrackerPanel trackerPanel, Container[] newViews) {
    if (newViews == null) newViews = new Container[0];
    int tab = getTab(trackerPanel);
    JPanel panel = (JPanel)tabbedPane.getComponentAt(tab);
    panel.removeAll();
    Object[] array = tabs.get(panel);
    Container[] views = (Container[])array[1];
    for (int i = 0; i < Math.min(newViews.length, views.length); i++) {
      if (newViews[i] != null) views[i] = newViews[i];
    }
    array[1] = views;
    MainTView mainView = (MainTView)array[0];
    JSplitPane[] panes = (JSplitPane[])array[2];
    panel.add(panes[0], BorderLayout.CENTER);
    panes[0].setLeftComponent(panes[2]);
    panes[0].setRightComponent(panes[1]);
    panes[1].setTopComponent(views[0]);
    panes[1].setBottomComponent(views[1]);
    panes[2].setTopComponent(mainView);
    panes[2].setBottomComponent(panes[3]);
    panes[3].setRightComponent(views[2]);
    panes[3].setLeftComponent(views[3]);
    // add toolbars at north position
    Box north = Box.createVerticalBox();
    north.add((JToolBar)array[3]);
    north.add((JToolBar)array[5]);
    panel.add(north, BorderLayout.NORTH);
  }

  /**
   * Gets the views for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return an array of views
   */
  public Container[] getViews(TrackerPanel trackerPanel) {
    Object[] array = getArray(trackerPanel);
    if (array == null) return new Container[4];
    Container[] views = (Container[])array[1];
    return views.clone();
  }

  /**
   * Gets the views for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return an array of views
   */
  public TView[][] getTViews(TrackerPanel trackerPanel) {
    return getTViews(trackerPanel, false);
  }

  /**
   * Gets the views for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @param customOnly true to return only customized views
   * @return an array of views
   */
  public TView[][] getTViews(TrackerPanel trackerPanel, boolean customOnly) {
    Container[] views = getViews(trackerPanel);
    TView[][] array = new TView[views.length][];
    for (int i = 0; i < array.length; i++) {
      if (views[i] instanceof TViewChooser) {
        TViewChooser chooser = (TViewChooser)views[i];
        Collection<TView> c = chooser.getViews();
        array[i] = new TView[c.size()];
        boolean empty = true;
        Iterator<TView> it = c.iterator();
        for (int j = 0; j < c.size(); j++) {
        	TView next = it.next();
        	if (!customOnly || next.isCustomState()) {
        		array[i][j] = next;
        		empty = false;
        	}
        }
        if (empty) array[i] = null;
      }
    }
    return array;
  }

  /**
   * Gets the selected TViews for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return an array of TViews (some elements may be null)
   */
  public String[] getSelectedTViews(TrackerPanel trackerPanel) {
    Container[] views = getViews(trackerPanel);
    String[] selectedViews = new String[views.length];
    for (int i = 0; i < selectedViews.length; i++) {
      if (views[i] instanceof TViewChooser) {
        TViewChooser chooser = (TViewChooser)views[i];
        selectedViews[i] = chooser.getSelectedView().getViewName();
      }
    }
    return selectedViews;
  }

  /**
   * Determines whether a view is open for the specified tracker panel.
   *
   * @param index the view index
   * @param trackerPanel the tracker panel
   * @return true if it is open
   */
  public boolean isViewOpen(int index, TrackerPanel trackerPanel) {
    JSplitPane[] panes = getSplitPanes(trackerPanel);
    double[] locs = new double[panes.length];
    for (int i = 0; i < panes.length; i++) {
    	int max = panes[i].getMaximumDividerLocation();
    	locs[i] = 1.0 * panes[i].getDividerLocation() / max;
    }
    switch(index) {
    	case 0: return locs[0] < 0.95 && locs[1] > 0.05;
    	case 1: return locs[0] < 0.95 && locs[1] < 0.95;
    	case 2: return locs[2] < 0.95 && locs[3] < 0.95;
    	case 3: return locs[2] < 0.95 && locs[3] > 0.05;
    }
    return false;
  }

  /**
   * Sets the location of a splitpane divider for a tracker panel
   *
   * @param trackerPanel the tracker panel
   * @param paneIndex the index of the split pane
   * @param loc the desired relative divider location
   */
  public void setDividerLocation(TrackerPanel trackerPanel,
                                 int paneIndex, double loc) {
    JSplitPane[] panes = getSplitPanes(trackerPanel);
    if (paneIndex < panes.length) {
      panes[paneIndex].setDividerLocation(loc);
      validate();
    }
  }

  /**
   * Sets the location of a splitpane divider for a tracker panel
   *
   * @param trackerPanel the tracker panel
   * @param paneIndex the index of the split pane
   * @param loc the desired absolute divider location
   */
  public void setDividerLocation(TrackerPanel trackerPanel,
                                 int paneIndex, int loc) {
    JSplitPane[] panes = getSplitPanes(trackerPanel);
    if (paneIndex < panes.length) {
      panes[paneIndex].setDividerLocation(loc);
      validate();
    }
  }

  /**
   * Gets a splitpane for a tracker panel
   *
   * @param trackerPanel the tracker panel
   * @param paneIndex the index of the split pane
   * @return the splitpane
   */
  JSplitPane getSplitPane(TrackerPanel trackerPanel, int paneIndex) {
    JSplitPane[] panes = getSplitPanes(trackerPanel);
    if (paneIndex < panes.length) {
      return panes[paneIndex];
    }
    return null;
  }

  /**
   * Gets the main view for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return a MainTView
   */
  public MainTView getMainView(TrackerPanel trackerPanel) {
    Object[] array = getArray(trackerPanel);
    if (array != null) {
      return (MainTView)array[0];
    }
    return new MainTView(trackerPanel);
  }

  /**
   * Responds to property change events.
   *
   * @param e the property change event
   */
  public void propertyChange(PropertyChangeEvent e) {
    String name = e.getPropertyName();
    if (name.equals("datafile") ||  name.equals("video")) { // from TrackerPanel  //$NON-NLS-1$ //$NON-NLS-2$
      TrackerPanel trackerPanel = (TrackerPanel)e.getSource();
    	refreshTab(trackerPanel);
    }    
    else if (name.equals("progress")) { // from currently loading (xuggle) video  //$NON-NLS-1$
    	Object val = e.getNewValue();
    	String vidName = XML.forwardSlash((String)e.getOldValue());
    	try {
				framesLoaded = Integer.parseInt(val.toString());
			} catch (Exception ex) {
			}
    	for (MonitorDialog next: TrackerIO.monitors) {
    		String monitorName = XML.forwardSlash(next.getName());
    		if (monitorName.endsWith(vidName)) {
  				int progress = 20+(framesLoaded/20 % 60);
    			if (next.getFrameCount()!=Integer.MIN_VALUE) {
    				progress = 20+(int)(framesLoaded*60.0/next.getFrameCount());
    			}
	      	next.setProgress(progress);
	      	next.setTitle(TrackerRes.getString("TFrame.ProgressDialog.Title.FramesLoaded")+": "+framesLoaded); //$NON-NLS-1$ //$NON-NLS-2$
	      	break;
    		}
    	}
    }
    else if (name.equals("stalled")) { // from stalled xuggle video //$NON-NLS-1$
    	String fileName = XML.getName((String)e.getNewValue());
    	String s = TrackerRes.getString("TFrame.Dialog.StalledVideo.Message0") //$NON-NLS-1$
    			+"\n"+TrackerRes.getString("TFrame.Dialog.StalledVideo.Message1") //$NON-NLS-1$ //$NON-NLS-2$
    			+"\n"+TrackerRes.getString("TFrame.Dialog.StalledVideo.Message2") //$NON-NLS-1$ //$NON-NLS-2$
    			+"\n\n"+TrackerRes.getString("TFrame.Dialog.StalledVideo.Message3"); //$NON-NLS-1$ //$NON-NLS-2$
    	String stop = TrackerRes.getString("TFrame.Dialog.StalledVideo.Button.Stop"); //$NON-NLS-1$
    	String wait = TrackerRes.getString("TFrame.Dialog.StalledVideo.Button.Wait"); //$NON-NLS-1$
    	int response = JOptionPane.showOptionDialog(TFrame.this, 
    			s, TrackerRes.getString("TFrame.Dialog.StalledVideo.Title")+": "+fileName,  //$NON-NLS-1$ //$NON-NLS-2$
    			JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, 
    			new String[] {stop, wait}, stop);
    	if (response==0) { // user chose to stop loading
    		VideoIO.setCanceled(true);
    		MonitorDialog monitor = null;
      	for (MonitorDialog next: TrackerIO.monitors) {
      		String monitorName = XML.forwardSlash(next.getName());
      		String videoName = XML.forwardSlash(fileName);
      		if (monitorName.endsWith(videoName)) {
   	      	monitor = next;
  	      	break;
      		}
      	}
      	if (monitor!=null)
      		monitor.close();
    	}
    }
    else if (name.equals("locale")) { // from TrackerRes   //$NON-NLS-1$
    	// clear the existing menubars and actions
    	TMenuBar.clear();
    	TActions.clear();
    	// create new actions
    	Tracker.createActions();
    	// create new default menubar
      createDefaultMenuBar();
      // replace and refresh the stored menubars and toolbars
      Iterator<Object[]> it = tabs.values().iterator();
      while (it.hasNext()) {
      	Object[] array = it.next();
      	MainTView mainView = (MainTView)array[0];
      	TrackerPanel trackerPanel = mainView.getTrackerPanel();
      	boolean changed = trackerPanel.changed; // save changed state and restore below
        array[4] = TMenuBar.getMenuBar(trackerPanel);
        CoordAxes axes = trackerPanel.getAxes();
        if (axes!=null) {
        	axes.setName(TrackerRes.getString("CoordAxes.New.Name")); //$NON-NLS-1$
        }
        trackerPanel.changed = changed;
        TToolBar toolbar = (TToolBar)array[3];
        toolbar.refresh(false);
        TTrackBar trackbar = (TTrackBar)array[5];
        trackbar.refresh();
      }
      // replace current menubar
      TrackerPanel trackerPanel = getTrackerPanel(getSelectedTab());
      if (trackerPanel != null) {
        // replace menu bar
      	TMenuBar menuBar = getMenuBar(trackerPanel);
      	if (menuBar!=null) {
	        setJMenuBar(menuBar);
	      	menuBar.refresh();
      	}
      	// show hint
        if (Tracker.startupHintShown) {
        	trackerPanel.setMessage(TrackerRes.getString("Tracker.Startup.Hint")); //$NON-NLS-1$
        }
        else {
        	// shows hint as side effect
        	trackerPanel.setCursorForMarking(false, null);
        }
      }
      else {
      	// show defaultMenuBar
      	refreshOpenRecentMenu(recentMenu);
        setJMenuBar(defaultMenuBar);
      }
      // refresh tabs
      for (int i = 0; i < tabbedPane.getTabCount(); i++) {
      	trackerPanel = getTrackerPanel(i);
      	tabbedPane.setTitleAt(i, trackerPanel.getTitle());
      	VideoPlayer player = trackerPanel.getPlayer();
      	player.refresh();
      	player.setLocale((Locale)e.getNewValue());
        Video vid = trackerPanel.getVideo();
        if (vid != null) {
          vid.getFilterStack().refresh();
        }
      	// refresh track controls and toolbars
      	TrackControl.getControl(trackerPanel).refresh();
      	getToolBar(trackerPanel).refresh(false);
      	getTrackBar(trackerPanel).refresh();
        // refresh views
        Container[] views = getViews(trackerPanel);
        for (int j = 0; j < views.length; j++) {
          if (views[j] instanceof TViewChooser) {
            ((TViewChooser)views[j]).refresh();
          }
        }
        // refresh autotracker
        if (trackerPanel.autoTracker!=null) {
        	trackerPanel.autoTracker.getWizard().textPaneSize = null;
        	trackerPanel.autoTracker.getWizard().refreshGUI();
        	trackerPanel.autoTracker.getWizard().pack();
        }
        // refresh prefs dialog
        if (prefsDialog!=null && prefsDialog.isVisible()) {
        	prefsDialog.refreshGUI();
        }
        // refresh pencil drawer
        PencilDrawer.getDrawer(trackerPanel).refresh();
        // refresh info dialog
        cancelNotesDialogButton.setText(TrackerRes.getString("Dialog.Button.Cancel")); //$NON-NLS-1$
        closeNotesDialogButton.setText(TrackerRes.getString("Dialog.Button.Close")); //$NON-NLS-1$ 
        displayWhenLoadedCheckbox.setText(TrackerRes.getString("TFrame.NotesDialog.Checkbox.ShowByDefault")); //$NON-NLS-1$
      }
      // refresh memory button
      TTrackBar.refreshMemoryButton();
      validate();
      if (helpLauncher!=null) {
        // refresh navigation bar components
        Component[] search = HelpFinder.getNavComponentsFor(helpLauncher);
        Component[] comps = new Component[search.length+2];
        System.arraycopy(search, 0, comps, 0, search.length);
        Tracker.pdfHelpButton.setText(TrackerRes.getString("Tracker.Button.PDFHelp")); //$NON-NLS-1$
        comps[comps.length-2] = Tracker.pdfHelpButton;
        comps[comps.length-1] = Box.createHorizontalStrut(4);
        helpLauncher.setNavbarRightEndComponents(comps);        
      }
    }
  }

  public void setVisible(boolean visible) {
    super.setVisible(visible);
    if (!Tracker.splash.isVisible()) return;
//    Tracker.setProgress(100);
    // dispose of splash automatically after short time
    Timer timer = new Timer(1500, new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      	Tracker.splash.dispose();
      }
    });
		timer.setRepeats(false);
		timer.start();
  }
  
  /**
   * Sets the display units for angles.
   * 
   * @param inRadians true to display radians, false to display degrees
   */
  public void setAnglesInRadians(boolean inRadians) {
  	if (anglesInRadians==inRadians)
  		return;
  	anglesInRadians = inRadians;
    firePropertyChange("radian_angles", null, inRadians); //$NON-NLS-1$
  }

  /**
   * Gets the preferences dialog.
   * @return the preferences dialog
   */
  public PrefsDialog getPrefsDialog() {
  	TrackerPanel trackerPanel = getTrackerPanel(getSelectedTab());
  	if (prefsDialog!=null) {
  		if (prefsDialog.trackerPanel!=trackerPanel) {
		  	prefsDialog.trackerPanel = trackerPanel;
	  		prefsDialog.refreshGUI();
  		}
  	}
  	else {
      // create PrefsDialog
  		prefsDialog = new PrefsDialog(trackerPanel, TFrame.this);
      // center on screen
      Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
      int x = (dim.width - prefsDialog.getBounds().width) / 2;
      int y = (dim.height - prefsDialog.getBounds().height) / 2;
      prefsDialog.setLocation(x, y);
  	}
  	return prefsDialog;
  }

  /**
   * Shows the preferences dialog.
   */
  public void showPrefsDialog() {
  	Runnable runner = new Runnable() {
  		public void run() {
  	  	PrefsDialog prefsDialog = getPrefsDialog();
  			prefsDialog.setVisible(true);
  			prefsDialog.requestFocus();
  		}
  	};
  	new Thread(runner).start();
  }
  
  /**
   * Shows the preferences dialog set to a specified tab.
   * 
   * @param tabName the name of the tab: config, runtime, video, general, display
   */
  public void showPrefsDialog(final String tabName) {
  	Runnable runner = new Runnable() {
  		public void run() {
				// show prefs dialog and select video tab
  	  	PrefsDialog prefsDialog = getPrefsDialog();
  	  	if (tabName!=null) {
  	  		if (tabName.contains("runtime")) //$NON-NLS-1$
  	  			prefsDialog.tabbedPane.setSelectedComponent(prefsDialog.runtimePanel);
  	  		else if (tabName.contains("video")) //$NON-NLS-1$
  	  			prefsDialog.tabbedPane.setSelectedComponent(prefsDialog.videoPanel);
  	  		else if (tabName.contains("general")) //$NON-NLS-1$
  	  			prefsDialog.tabbedPane.setSelectedComponent(prefsDialog.generalPanel);
  	  		else if (tabName.contains("display")) //$NON-NLS-1$
  	  			prefsDialog.tabbedPane.setSelectedComponent(prefsDialog.displayPanel);
  	  	}
  			prefsDialog.setVisible(true);
  			prefsDialog.requestFocus();
  		}
  	};
  	new Thread(runner).start();
  }
  
//_________________________________ private methods _________________________
  
  /**
   * Creates the default views of the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return a Container[numberOfViews] array of views
   */
  private Container[] createViews(TrackerPanel trackerPanel) {
    TViewChooser chooser1 = new TViewChooser(trackerPanel);
    chooser1.setSelectedView(chooser1.getView(TrackerRes.getString("TFrame.View.Plot"))); //$NON-NLS-1$
    TViewChooser chooser2 = new TViewChooser(trackerPanel);
    chooser2.setSelectedView(chooser2.getView(TrackerRes.getString("TFrame.View.Table"))); //$NON-NLS-1$
    TViewChooser chooser3 = new TViewChooser(trackerPanel);
    chooser3.setSelectedView(chooser3.getView(TrackerRes.getString("TFrame.View.World"))); //$NON-NLS-1$
    TViewChooser chooser4 = new TViewChooser(trackerPanel);
    chooser4.setSelectedView(chooser4.getView(TrackerRes.getString("TFrame.View.Text"))); //$NON-NLS-1$
    return new Container[] {chooser1, chooser2, chooser3, chooser4};
  }

  /**
   * Gets the split panes for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return an array of split panes
   */
  JSplitPane[] getSplitPanes(final TrackerPanel trackerPanel) {
    Object[] array = getArray(trackerPanel);
    if (array != null) {
      return (JSplitPane[])array[2];
    }
    JSplitPane[] panes = new JSplitPane[4];
    panes[0] = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT) { // right pane
      // override setDividerLocation to update Window menu item
      public void setDividerLocation(int loc) {
        int cur = getDividerLocation();
        int max = getMaximumDividerLocation();
        if (loc == max) {
          super.setDividerLocation(1.0);
        }
        else if (loc != cur)
          super.setDividerLocation(loc);
        cur = getDividerLocation();
        boolean open =  (1.0 * cur / max) < 0.98;
        TMenuBar menubar = TMenuBar.getMenuBar(trackerPanel);
        if (menubar!=null) {
        	menubar.rightPaneItem.setSelected(open);
        }
      }
    };
    panes[1] = new JSplitPane(JSplitPane.VERTICAL_SPLIT); // plot/table split
    panes[2] = new JSplitPane(JSplitPane.VERTICAL_SPLIT) { // bottom pane
      public void setDividerLocation(int loc) {
        int cur = getDividerLocation();
        int max = getMaximumDividerLocation();
        if (loc == max) {
          super.setDividerLocation(1.0);
        }
        else if (loc != cur)
          super.setDividerLocation(loc);
        cur = getDividerLocation();
        boolean open =  (1.0 * cur / max) < 0.98;
        TMenuBar menubar = TMenuBar.getMenuBar(trackerPanel);
        if (menubar!=null) {
        	menubar.bottomPaneItem.setSelected(open);
        }
      }
    };
    panes[3] = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT) { // world/html
      public void setDividerLocation(int loc) {
        int cur = getDividerLocation();
        int min = getMinimumDividerLocation();
        if (loc == min) {
          if (cur != 0)
            super.setDividerLocation(0);
        }
        else if (loc != cur)
          super.setDividerLocation(loc);
      }
    };
    panes[0].setResizeWeight(1.0);
    panes[1].setResizeWeight(0.5);
    panes[2].setResizeWeight(1.0);
    panes[3].setResizeWeight(0.5);
    panes[0].setOneTouchExpandable(true);
    panes[1].setOneTouchExpandable(true);
    panes[2].setOneTouchExpandable(true);
    panes[3].setOneTouchExpandable(true);
    return panes;
  }

  /**
   * Gets the toolbar for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return a TToolBar
   */
  public TToolBar getToolBar(TrackerPanel trackerPanel) {
    Object[] array = getArray(trackerPanel);
    if (array != null) {
      return (TToolBar)array[3];
    }
    return TToolBar.getToolbar(trackerPanel);
  }

  /**
   * Gets the menubar for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return a TMenuBar
   */
  public TMenuBar getMenuBar(TrackerPanel trackerPanel) {
    Object[] array = getArray(trackerPanel);
    if (array != null && array[4] != null) {
      return (TMenuBar)array[4];
    }
    return TMenuBar.getMenuBar(trackerPanel);
  }

  /**
   * Sets the menubar for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @param menubar a TMenuBar
   */
  public void setMenuBar(TrackerPanel trackerPanel, TMenuBar menubar) {
    Object[] array = getArray(trackerPanel);
    if (array != null && array.length > 4) {
      array[4] = menubar;
      setJMenuBar(menubar);
    }
  }

  /**
   * Gets the selected track bar for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return a TSelectedTrackBar
   */
  public TTrackBar getTrackBar(TrackerPanel trackerPanel) {
    Object[] array = getArray(trackerPanel);
    if (array != null) {
      return (TTrackBar)array[5];
    }
    return TTrackBar.getTrackbar(trackerPanel);
  }
  
  /**
   * Refreshes the open recent files menu.
   *
   * @param menu the menu to refresh
   */
  public void refreshOpenRecentMenu(final JMenu menu) {
  	synchronized(Tracker.recentFiles) {
	  	menu.setText(TrackerRes.getString("TMenuBar.Menu.OpenRecent")); //$NON-NLS-1$
	  	menu.setEnabled(!Tracker.recentFiles.isEmpty());
	  	if (openRecentAction==null) {
		  	openRecentAction = new AbstractAction() {
		  		public void actionPerformed(ActionEvent e) {
		  			String path = e.getActionCommand();
		        URL url = null;
		        File file = new File(path);
		        if (!file.exists()) {
		        	int n = path.indexOf("!"); //$NON-NLS-1$
		        	if (n>-1) {
		        		file = new File(path.substring(0, n));
		        	}
		        }
		        if (!file.exists()) {
		        	try {
								url = new URL(e.getActionCommand());
							} catch (MalformedURLException e1) {
							}
		        }
		        if (!file.exists() && url==null) {
		        	Tracker.recentFiles.remove(e.getActionCommand());
		        	int n = getSelectedTab();
		        	if (n>-1) {
		        		TrackerPanel trackerPanel = getTrackerPanel(n);
		        		TMenuBar.getMenuBar(trackerPanel).refresh();
		        	}
		        	else {
		          	refreshOpenRecentMenu(recentMenu);
		        	}
		        	JOptionPane.showMessageDialog(TFrame.this, 
		        			TrackerRes.getString("TFrame.Dialog.FileNotFound.Message") //$NON-NLS-1$
		        			+ "\n"+MediaRes.getString("VideoIO.Dialog.Label.Path")+": "+e.getActionCommand(), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		        			TrackerRes.getString("TFrame.Dialog.FileNotFound.Title"),  //$NON-NLS-1$
		        			JOptionPane.WARNING_MESSAGE);
		        	return;
		        }
	        	TrackerPanel selected = getTrackerPanel(getSelectedTab());
	        	if (selected != null) {
	        		selected.setMouseCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
	        	}
        		TrackerIO.open(path, TFrame.this);
//	        	if (url!=null)
//	        		TrackerIO.open(url, TFrame.this);
//	        	else 
//	        		TrackerIO.open(file, TFrame.this);
	        	setCursor(Cursor.getDefaultCursor());
		  		}
		  	};
	  	}
	  	menu.removeAll();
	  	menu.setEnabled(!Tracker.recentFiles.isEmpty());
	  	for (String next: Tracker.recentFiles) {
	    	JMenuItem item = new JMenuItem(XML.getName(next));
	    	item.setActionCommand(next);
	    	item.setToolTipText(next);
	    	item.addActionListener(openRecentAction);
	    	menu.add(item);
	    }
	    FontSizer.setFonts(menu, FontSizer.getLevel());
  	}
  }

  /**
   * Refreshes the GUI.
   */
  public void refresh() {
    int i = getSelectedTab();
    if (i < 0) return;
    TrackerPanel trackerPanel = getTrackerPanel(i);
    getMenuBar(trackerPanel).refresh();
    getToolBar(trackerPanel).refresh(true);
    getTrackBar(trackerPanel).refresh();
    for (Container next: getViews(trackerPanel)) {
    	if (next instanceof TViewChooser) {
    		TViewChooser chooser = (TViewChooser)next;
    		chooser.refreshMenus();
    	}
    }
    trackerPanel.refreshNotesDialog();
//    checkMemory();
  }

  /**
   * Refreshes the Window menu for a TrackerPanel.
   *
   * @param trackerPanel the TrackerPanel
   */
  public void refreshWindowMenu(TrackerPanel trackerPanel) {
    // refresh window menu
    JSplitPane pane = getSplitPane(trackerPanel, 0);
    int max = pane.getMaximumDividerLocation();
    int cur = pane.getDividerLocation();
    double loc = 1.0 * cur / max;
    TMenuBar menubar = TMenuBar.getMenuBar(trackerPanel);
    menubar.rightPaneItem.setSelected(loc < 0.99);
    pane = getSplitPane(trackerPanel, 2);
    max = pane.getMaximumDividerLocation();
    cur = pane.getDividerLocation();
    loc = 1.0 * cur / max;
    menubar.bottomPaneItem.setSelected(loc < .99);
    TrackControl tc = TrackControl.getControl(trackerPanel);
    menubar.trackControlItem.setSelected(tc.isVisible());
    menubar.trackControlItem.setEnabled(!tc.isEmpty());
    menubar.notesItem.setSelected(notesDialog.isVisible());
    menubar.dataBuilderItem.setSelected(trackerPanel.dataBuilder!=null
    		&& trackerPanel.dataBuilder.isVisible());
    menubar.dataToolItem.setSelected(DataTool.getTool().isVisible());
    
    // rebuild window menu
    menubar.windowMenu.removeAll();
    boolean maximized = false;
  	Container[] views = getViews(trackerPanel);
  	for (int i = 0; i<views.length; i++) {
    	if (views[i] instanceof TViewChooser) {
    		TViewChooser chooser = (TViewChooser)views[i];
    		if (chooser.maximized) {
    			maximized = true;
    			break;
    		}
    	}
  	}
  	if (maximized) {
      menubar.windowMenu.add(menubar.restoreItem);
  	}
  	else {
      menubar.windowMenu.add(menubar.rightPaneItem);
      menubar.windowMenu.add(menubar.bottomPaneItem);
  	}
    menubar.windowMenu.addSeparator();
    menubar.windowMenu.add(menubar.trackControlItem);
    menubar.windowMenu.add(menubar.notesItem);
    if (trackerPanel.isEnabled("data.builder") //$NON-NLS-1$
  			|| trackerPanel.isEnabled("data.tool")) { //$NON-NLS-1$        
	    menubar.windowMenu.addSeparator();
	    if (trackerPanel.isEnabled("data.builder")) //$NON-NLS-1$
	    	menubar.windowMenu.add(menubar.dataBuilderItem);
	    if (trackerPanel.isEnabled("data.tool")) //$NON-NLS-1$
	    		menubar.windowMenu.add(menubar.dataToolItem);
    }
    for (int i=0; i<getTabCount(); i++) {
    	if (i==0) menubar.windowMenu.addSeparator();
    	JMenuItem tabItem = new JRadioButtonMenuItem(getTabTitle(i));   	
    	tabItem.setActionCommand(String.valueOf(i));
  		tabItem.setSelected(i==getSelectedTab());
  		tabItem.addActionListener(new ActionListener() {
  	    public void actionPerformed(ActionEvent e) {
  	    	int j = Integer.parseInt(e.getActionCommand());
  	    	setSelectedTab(j);
  	    }
  		});
  		menubar.windowMenu.add(tabItem);
    }
    menubar.windowMenu.revalidate();
  }
  
  /**
   * Sets the font level.
   *
   * @param level the desired font level
   */
  public void setFontLevel(int level) {
  	try {
			super.setFontLevel(level);
		} catch (Exception e) {}  	
  	if (tabbedPane==null) return;
		FontSizer.setFonts(tabbedPane, level);
  	
  	Step.textLayoutFont = FontSizer.getResizedFont(Step.textLayoutFont, level);
  	
  	for (int i=0; i<getTabCount(); i++) {
  		TrackerPanel trackerPanel = getTrackerPanel(i);
  		trackerPanel.setFontLevel(level);
  	}
  	
  	ExportZipDialog.setFontLevels(level);
  	if (ExportVideoDialog.videoExporter!=null) {
  		ExportVideoDialog.videoExporter.setFontLevel(level);
  	}
  	if (ThumbnailDialog.thumbnailDialog!=null) {
  		FontSizer.setFonts(ThumbnailDialog.thumbnailDialog, level);
  		ThumbnailDialog.thumbnailDialog.refreshGUI();
  	}
  	if (prefsDialog!=null) {
  		prefsDialog.refreshGUI();
  	}
  	if (libraryBrowser!=null) {
  		libraryBrowser.setFontLevel(level);
  	}
  	FontSizer.setFonts(notesDialog, level);
		FontSizer.setFonts(OSPLog.getOSPLog(), level);
  	if (Tracker.readmeDialog!=null) {
  		FontSizer.setFonts(Tracker.readmeDialog, level);
  	}
  	if (Tracker.startLogDialog!=null) {
  		FontSizer.setFonts(Tracker.startLogDialog, level);
  	}		
    FontSizer.setFonts(defaultMenuBar, level);
    if (helpLauncher!=null) {
    	helpLauncher.setFontLevel(level);
    	for (int i=0; i<helpLauncher.getTabCount(); i++) {
		    LaunchPanel tab = helpLauncher.getTab(i);
		    if (level>0) {
		    	String newValue = "help"+level+".css"; //$NON-NLS-1$ //$NON-NLS-2$
		    	tab.getHTMLSubstitutionMap().put("help.css", newValue); //$NON-NLS-1$
		    }
		    else {
		    	tab.getHTMLSubstitutionMap().remove("help.css"); //$NON-NLS-1$
		    }
    	}
	    for (int i=0; i<helpLauncher.getHTMLTabCount(); i++) {
	    	HTMLPane pane = helpLauncher.getHTMLTab(i);
	    	pane.editorPane.getDocument().putProperty(Document.StreamDescriptionProperty, null);
	    }
	    helpLauncher.setDivider((int)(175*FontSizer.getFactor(level)));
	    helpLauncher.refreshSelectedTab();
    }
  }

  /**
   * Gets the library browser.
   *
   * @return the library browser
   */
  protected LibraryBrowser getLibraryBrowser() {
    if (libraryBrowser == null) {
    	LibraryComPADRE.desiredOSPType = "Tracker"; //$NON-NLS-1$
//    	JDialog dialog = new JDialog(this, false);
    	libraryBrowser = LibraryBrowser.getBrowser(null);
//    	libraryBrowser = LibraryBrowser.getBrowser(dialog);
    	libraryBrowser.addOSPLibrary(LibraryBrowser.TRACKER_LIBRARY);
    	libraryBrowser.addOSPLibrary(LibraryBrowser.SHARED_LIBRARY);
    	libraryBrowser.addComPADRECollection(LibraryComPADRE.TRACKER_SERVER_TREE+LibraryComPADRE.PRIMARY_ONLY);
	    libraryBrowser.refreshCollectionsMenu();
    	libraryBrowser.addPropertyChangeListener("target", new PropertyChangeListener() { //$NON-NLS-1$
	  		public void propertyChange(PropertyChangeEvent e) {
	  			libraryBrowser.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
	  			LibraryResource record = (LibraryResource)e.getNewValue();
  				String target = XML.getResolvedPath(record.getTarget(), record.getBasePath());
    			target = ResourceLoader.getURIPath(target);
    			
					// download comPADRE targets to osp cache
					if (target.indexOf("document/ServeFile.cfm?")>-1) { //$NON-NLS-1$
						String fileName = record.getProperty("download_filename"); //$NON-NLS-1$  					
						try {
							File file = ResourceLoader.downloadToOSPCache(target, fileName, false);
							target = file.toURI().toString();
						} catch (Exception ex) {
							String s = TrackerRes.getString("TFrame.Dialog.LibraryError.Message"); //$NON-NLS-1$
	        		JOptionPane.showMessageDialog(libraryBrowser, 
	        				s+" \""+record.getName()+"\"", //$NON-NLS-1$ //$NON-NLS-2$
	        				TrackerRes.getString("TFrame.Dialog.LibraryError.Title"), //$NON-NLS-1$
	        				JOptionPane.WARNING_MESSAGE);
							return;
						}
					}
					
    			String lcTarget = target.toLowerCase();
    			boolean accept = lcTarget.endsWith(".trk"); //$NON-NLS-1$
					accept = accept || lcTarget.endsWith(".zip"); //$NON-NLS-1$
					accept = accept || lcTarget.endsWith(".trz"); //$NON-NLS-1$
  				for (String ext: VideoIO.getVideoExtensions()) {
  					accept = accept || lcTarget.endsWith("."+ext); //$NON-NLS-1$
    			}
  				if (accept) {
//		  			libraryBrowser.setVisible(false);
		        Resource res = ResourceLoader.getResourceZipURLsOK(target);
  					if (res!=null) {
  						ArrayList<String> urlPaths = new ArrayList<String>();
  						urlPaths.add(target);
  						TrackerIO.open(urlPaths, TFrame.this, null);
  					}
  					else {
							String s = TrackerRes.getString("TFrame.Dialog.LibraryError.FileNotFound.Message"); //$NON-NLS-1$
	        		JOptionPane.showMessageDialog(libraryBrowser, 
	        				s+" \""+XML.getName(target)+"\"", //$NON-NLS-1$ //$NON-NLS-2$
	        				TrackerRes.getString("TFrame.Dialog.LibraryError.FileNotFound.Title"), //$NON-NLS-1$
	        				JOptionPane.WARNING_MESSAGE);
			  			libraryBrowser.setVisible(true);
  					}
  				}

	  			libraryBrowser.setCursor(Cursor.getDefaultCursor());
					TFrame.this.requestFocus();
	  		}
	  	});
    	LibraryBrowser.fireHelpEvent = true;
    	libraryBrowser.addPropertyChangeListener("help", new PropertyChangeListener() { //$NON-NLS-1$
	  		public void propertyChange(PropertyChangeEvent e) {
	  			showHelp("library", 0); //$NON-NLS-1$
	  		}
	  	});
  		libraryBrowser.setFontLevel(FontSizer.getLevel());
//      Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
//      int x = (dim.width - dialog.getBounds().width) / 2;
//      int y = (dim.height - dialog.getBounds().height) / 2;
//      dialog.setLocation(x, y);
    }
    return libraryBrowser;
  }

  /**
   * Gets the properties dialog for a specified TrackerPanel.
   *
   * @param trackerPanel 
   * @return the properties dialog
   */
  protected PropertiesDialog getPropertiesDialog(TrackerPanel trackerPanel) {
  	// return a new dialog every time
  	PropertiesDialog dialog = new PropertiesDialog(trackerPanel);
    Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
    int x = (dim.width - dialog.getBounds().width) / 2;
    int y = (dim.height - dialog.getBounds().height) / 2;
    dialog.setLocation(x, y);
    return dialog;
  }
 
  /**
   * Gets the help dialog.
   *
   * @return the help dialog
   */
  protected Component getHelpDialog() {
    if (helpDialog == null) {
      helpDialog = new JDialog(this, TrackerRes.getString("TFrame.Dialog.Help.Title"), false); //$NON-NLS-1$
      String help_path = helpPath + "help_set.xml"; //$NON-NLS-1$
//      String lang = TrackerRes.locale.getLanguage();
//      String webHelp = helpPathWeb+"help_"+lang+"/help_set.xml"; //$NON-NLS-1$ //$NON-NLS-2$
//      Resource res = ResourceLoader.getResource(webHelp);
//      if (res!=null) { 
//      	help_path = res.getString(); // open launcher with xml string
//      }
//      System.out.println(help_path);
      helpLauncher = new Launcher(help_path, false);
      helpLauncher.popupEnabled = false;
      int level = FontSizer.getLevel();
      if (helpLauncher.getTabCount()>0) {
		    LaunchPanel tab = helpLauncher.getTab(0);
		    if (level>0) {
		    	String newValue = "help"+level+".css"; //$NON-NLS-1$ //$NON-NLS-2$
		    	tab.getHTMLSubstitutionMap().put("help.css", newValue); //$NON-NLS-1$
		    }
		    else {
		    	tab.getHTMLSubstitutionMap().remove("help.css"); //$NON-NLS-1$
		    }
      }
	    helpLauncher.setDivider((int)(175*FontSizer.getFactor(level)));
	    
      // navigation bar and search components
      helpLauncher.setNavigationVisible(true);
      
      Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
      Dimension dim = helpLauncher.getSize();
      dim.width = Math.min((9*screen.width)/10, (int)((1+level*0.35)*dim.width));    
      dim.height = Math.min((9*screen.height)/10, (int)((1+level*0.35)*dim.height)); 
      helpLauncher.setSize(dim);

      helpDialog.setContentPane(helpLauncher.getContentPane());
  		FontSizer.setFonts(helpDialog, FontSizer.getLevel());

      helpDialog.pack();
      int x = (screen.width - helpDialog.getBounds().width) / 2;
      int y = (screen.height - helpDialog.getBounds().height) / 2;
      helpDialog.setLocation(x, y);
    }
    // refresh navigation bar components in case locale has changed
    Component[] search = HelpFinder.getNavComponentsFor(helpLauncher);
    Component[] comps = new Component[search.length+2];
    System.arraycopy(search, 0, comps, 0, search.length);
    Tracker.pdfHelpButton.setText(TrackerRes.getString("Tracker.Button.PDFHelp")); //$NON-NLS-1$
    comps[comps.length-2] = Tracker.pdfHelpButton;
    comps[comps.length-1] = Box.createHorizontalStrut(4);
    helpLauncher.setNavbarRightEndComponents(comps);
    return helpDialog;
  }
  
  /**
   * Shows a specified help topic.
   *
   * @param selectedNode the name of the help node to be displayed
   */
  protected void showHelp(String selectedNode) {
  	getHelpDialog(); // create dialog and launcher if needed
    helpLauncher.setSelectedNode(selectedNode);
    helpDialog.setVisible(true);
  }

  /**
   * Shows a specified help topic by keyword: gettingstarted,
   * install, linux, GUI, video, filters, tracks, coords, axes, tape,
   * offset, calibration, pointmass, cm, vector, vectorsum, profile,
   * rgbregion, particle, plot, datatable, xml, etc.
   *
   * @param keywords the keywords of the help node to be displayed
   * @param pageNumber the html page number
   */
  protected void showHelp(String keywords, int pageNumber) {
  	boolean firstTime = helpDialog == null;
  	getHelpDialog(); // create dialog and launcher if needed
  	if (keywords == null && firstTime) {
  		keywords = "help"; //$NON-NLS-1$
  	}
  	helpLauncher.setSelectedNodeByKey(keywords, pageNumber);
    if (firstTime) helpLauncher.clearHistory();
    helpDialog.setVisible(true);
  }

  /**
   * Shows the track control if any user tracks are present.
   *
   * @param panel the tracker panel
   */
  protected void showTrackControl(final TrackerPanel panel) {
    if (panel.getUserTracks().size() > 0) {
	    Runnable runner = new Runnable() {
	      public void run() {
	      	TrackControl tc = TrackControl.getControl(panel);
	        if (tc.positioned && !tc.isEmpty()) {
	        	tc.setVisible(true);
	        }
	      }
	    };
	    EventQueue.invokeLater(runner);
    }
  }

  /**
   * Shows the notes, if any.
   *
   * @param panel the tracker panel
   */
  protected void showNotes(final TrackerPanel panel) {
    final JButton button = getToolBar(panel).notesButton;
    Runnable runner = new Runnable() {
      public void run() {
      	TTrack track = panel.getSelectedTrack();
      	if (!panel.hideDescriptionWhenLoaded &&
      			((track != null && track.getDescription()!= null &&
                !track.getDescription().trim().equals("")) || //$NON-NLS-1$
            (track == null && panel.getDescription() != null &&
                !panel.getDescription().trim().equals("")))) { //$NON-NLS-1$
          if (!button.isSelected()) button.doClick();
        }
        else if (button.isSelected()) button.doClick();
      }
    };
    EventQueue.invokeLater(runner);
  }

//  /**
//   * Checks the current memory usage. If the total memory being used approaches 
//   * the max available, this reopens Tracker in a new larger java vm.
//   */
//  public void checkMemory() {
//  	System.gc();
//  	Runtime runtime = Runtime.getRuntime();
//  	double total = runtime.totalMemory();
//  	double max = runtime.maxMemory();
//  	JOptionPane.showMessageDialog(this, "memory "+total+" of "+max); //$NON-NLS-1$ //$NON-NLS-2$
//  	if (total/max > 0.6 && OSPRuntime.getLaunchJarPath() != null) {
//  		int result = JOptionPane.showConfirmDialog(this, "Resize memory to "+2*max+"?"); //$NON-NLS-1$ //$NON-NLS-2$
//      if (result != JOptionPane.YES_OPTION) return;
//  		// save trackerPanel fileNames
//  		ArrayList<File> files = new ArrayList<File>();
//  		for (int i = 0; i < getTabCount(); i++) {
//  			File file = getTrackerPanel(i).getDataFile();
//  			if (file != null) files.add(file);
//  		}
//  		// dispose of this frame 
//  		this.dispose();
//  		// launch Tracker in new vm
//      // construct the command to execute
//      final java.util.Vector<String> cmd = new java.util.Vector<String>();
//      cmd.add("java"); //$NON-NLS-1$
//      String classPath = OSPRuntime.getLaunchJarPath();
//      // convert colons to semicolons
//      classPath = classPath.replace(':', ';');
//      // replace semicolons with platform-dependent path separator
//      char pathSeparator = System.getProperty("path.separator").charAt(0);   //$NON-NLS-1$
//      classPath = classPath.replace(';', pathSeparator);
//      cmd.add("-classpath");                                                 //$NON-NLS-1$
//      cmd.add(classPath);
//      cmd.add(Tracker.class.getName());
//      String memoryArg = "-Xmx"+2*max; //$NON-NLS-1$
//      cmd.add(memoryArg);
//      memoryArg = "-Xms"+2*max; //$NON-NLS-1$
//      cmd.add(memoryArg);
//      Iterator<File> it = files.iterator();
//      while (it.hasNext()) {
//      	String arg = it.next().getPath();
//        cmd.add(arg);
//      }
//      // launch thread for new VM
//      Runnable launchRunner = new Runnable() {
//         public void run() {
//            OSPLog.finer(cmd.toString());
//            String[] cmdarray = cmd.toArray(new String[0]);
//            try {
//               Process proc = Runtime.getRuntime().exec(cmdarray);
//               BufferedInputStream errStream=new BufferedInputStream(proc.getErrorStream());
//               StringBuffer buff= new StringBuffer();
//               while(true){
//                 int datum=errStream.read();
//                 if(datum==-1) break;
//                 buff.append((char)datum);
//               }
//               errStream.close();
//               String msg=buff.toString().trim();
//               if(msg.length()>0){
//                 OSPLog.info("error buffer: " + buff.toString()); //$NON-NLS-1$
//               }
//            } catch(Exception ex) {
//               ex.printStackTrace();
//            }
//         }
//      };
//      Thread relauncher = new Thread(launchRunner);
//      relauncher.setPriority(Thread.NORM_PRIORITY);
//      relauncher.start();           
//  	}
//  }

  /**
   * Gets the object array for the specified tracker panel.
   *
   * @param trackerPanel the tracker panel
   * @return the object array
   */
  private Object[] getArray(TrackerPanel trackerPanel) {
    int tab = getTab(trackerPanel);
    if (tab >= 0) return tabs.get(tabbedPane.getComponentAt(tab));
    return null;
  }
  
  /**
   * Gets the (singleton) clipboard listener.
   *
   * @return the ClipboardListener
   */
  protected ClipboardListener getClipboardListener() {
  	if (clipboardListener==null) {
  		clipboardListener = new ClipboardListener(this);
  		clipboardListener.start();
  	}
  	return clipboardListener;
  }

  /**
   * Starts or ends the clipboard listener as needed.
   */
  protected void checkClipboardListener() {
  	// do we need clipboard listener?
  	Runnable runner = new Runnable() {
  		public void run() {
  	  	boolean needListener = alwaysListenToClipboard;
  	  	if (!needListener) {
  		  	// do any pasted data tracks exist?
  		    try {
  					for (int i = 0; i < getTabCount(); i++) {
  						TrackerPanel trackerPanel = getTrackerPanel(i);
  						ArrayList<DataTrack> dataTracks = trackerPanel.getDrawables(DataTrack.class);
  						// do any tracks have null source?
  						for (DataTrack next: dataTracks) {
  							if (next.getSource()==null) {
  								// null source, so data is pasted
  								needListener = true;
  								break;
  							}
  						}
  					}
  				} catch (Exception ex) {
  				}
  	  	}
  	    
  	    if (needListener) {
  	    	getClipboardListener();
  	    }
  	  	else {
  	  		if (clipboardListener==null) return;
  	    	// end existing listener
  	    	clipboardListener.end();
  	    	clipboardListener = null;
  	  	}  			
  		}
  	};
//  	new Thread(runner).start();
  	SwingUtilities.invokeLater(runner);

  }

  /**
   * Creates the GUI.
   */
  private void createGUI() {
  	// add focus listener to notify ParticleDataTracks and other listeners
  	addWindowFocusListener(new WindowAdapter() {
  		@Override
      public void windowGainedFocus(WindowEvent e) {
  			firePropertyChange("windowfocus", null, null); //$NON-NLS-1$
  		}
  	});
    // create notes actions and dialog
    saveNotesAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
      	if (notesTextPane.getBackground() == Color.WHITE) return;
      	String desc = notesTextPane.getText();
        if (getSelectedTab() > -1 && notesDialog.getName() != "canceled") { //$NON-NLS-1$
          TrackerPanel trackerPanel = getTrackerPanel(getSelectedTab());
          trackerPanel.changed = true;
          TTrack track = trackerPanel.getTrack(notesDialog.getName());
          if (track != null && !desc.equals(track.getDescription())) {
            track.setDescription(desc);
          }
          else if (!desc.equals(trackerPanel.getDescription())) {
            trackerPanel.setDescription(desc);
            trackerPanel.hideDescriptionWhenLoaded = !displayWhenLoadedCheckbox.isSelected();
          }
        }
      	notesTextPane.setBackground(Color.WHITE);
      	cancelNotesDialogButton.setEnabled(false);
      	closeNotesDialogButton.setEnabled(true);
        closeNotesDialogButton.setText(TrackerRes.getString("Dialog.Button.Close")); //$NON-NLS-1$ 
      }
    };
    notesDialog = new JDialog(this, false) {
    	public void setVisible(boolean vis) {
    		super.setVisible(vis);

        if (getSelectedTab() > -1) {
          TrackerPanel trackerPanel = getTrackerPanel(getSelectedTab());
          TToolBar toolbar = getToolBar(trackerPanel);
          toolbar.notesButton.setSelected(vis);
        }
    	}
    };
    JPanel infoContentPane = new JPanel(new BorderLayout());
    notesDialog.setContentPane(infoContentPane);
    notesTextPane = new JTextPane();
    notesTextPane.setBackground(Color.WHITE);
    notesTextPane.addHyperlinkListener(new HyperlinkListener() {
      public void hyperlinkUpdate(HyperlinkEvent e) {
        if(e.getEventType()==HyperlinkEvent.EventType.ACTIVATED) {
        	String url = e.getURL().toString();
        	org.opensourcephysics.desktop.OSPDesktop.displayURL(url);
        }
      }
    });
    notesTextPane.setPreferredSize(new Dimension(420, 200));
    notesTextPane.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        TrackerPanel trackerPanel = getTrackerPanel(getSelectedTab());
      	if (!trackerPanel.isEnabled("notes.edit")) //$NON-NLS-1$
      		return;
      	notesTextPane.setBackground(yellow);
        closeNotesDialogButton.setText(TrackerRes.getString("PrefsDialog.Button.Save")); //$NON-NLS-1$ 
      	cancelNotesDialogButton.setEnabled(true);
      }
    });
    notesTextPane.addFocusListener(new FocusAdapter() {
    	public void focusLost(FocusEvent e) {
    		if (e.getOppositeComponent()!=cancelNotesDialogButton)
    			saveNotesAction.actionPerformed(null);    		
    	}
    });
    JScrollPane textScroller = new JScrollPane(notesTextPane);
    infoContentPane.add(textScroller, BorderLayout.CENTER);
    JPanel buttonbar = new JPanel(new FlowLayout());
    infoContentPane.add(buttonbar, BorderLayout.SOUTH);
    displayWhenLoadedCheckbox = new JCheckBox(TrackerRes.getString("TFrame.NotesDialog.Checkbox.ShowByDefault")); //$NON-NLS-1$
    displayWhenLoadedCheckbox.addActionListener(new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        TrackerPanel trackerPanel = getTrackerPanel(getSelectedTab());
        if (trackerPanel!=null) {
        	trackerPanel.hideDescriptionWhenLoaded = !displayWhenLoadedCheckbox.isSelected();
        }
      }
    });
    buttonbar.add(displayWhenLoadedCheckbox); 
    buttonbar.add(Box.createHorizontalStrut(50));
    cancelNotesDialogButton = new JButton(TrackerRes.getString("Dialog.Button.Cancel")); //$NON-NLS-1$
    cancelNotesDialogButton.addActionListener(new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
      	notesDialog.setName("canceled"); //$NON-NLS-1$
      	notesDialog.setVisible(false);
      }
    });
    buttonbar.add(cancelNotesDialogButton);    
    closeNotesDialogButton = new JButton(TrackerRes.getString("Dialog.Button.Close")); //$NON-NLS-1$
    closeNotesDialogButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      	notesDialog.setVisible(false);
      }
    });
    buttonbar.add(closeNotesDialogButton);
    notesDialog.pack();
    // create the tabbed pane
    tabbedPane = new JTabbedPane(SwingConstants.BOTTOM);
    setContentPane(new JPanel(new BorderLayout()));
    getContentPane().add(tabbedPane, BorderLayout.CENTER);
    // create the default menubar
    createDefaultMenuBar();
    // add listener to change menubar, toolbar, track control when tab changes
    tabbedPane.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        TrackerPanel newPanel = null;
        TrackerPanel oldPanel = prevPanel;
        
        // hide exportZipDialog
      	if (ExportVideoDialog.videoExporter!=null) {
      		ExportVideoDialog.videoExporter.trackerPanel = null;
      	}
      	if (ThumbnailDialog.thumbnailDialog!=null) {
      		ThumbnailDialog.thumbnailDialog.trackerPanel = null;
      	}
        // update prefsDialog
        if (prefsDialog!=null) {
        	prefsDialog.trackerPanel = null;
        }
        // clean up items associated with old panel
        if (playerBar != null) {
          Container frame = playerBar.getTopLevelAncestor();
          if (frame != null && frame != TFrame.this) frame.setVisible(false);
        }
        if (prevPanel != null) {
          if (prevPanel.dataBuilder != null) {
          	boolean vis = prevPanel.dataToolVisible;
          	prevPanel.dataBuilder.setVisible(false);
          	prevPanel.dataToolVisible = vis;
          }
          if (prevPanel.getPlayer()!=null) {
	          ClipInspector ci = prevPanel.getPlayer().getVideoClip().getClipInspector();
	          if (ci != null) ci.setVisible(false);
          }
          Video vid = prevPanel.getVideo();
          if (vid != null) {
            vid.getFilterStack().setInspectorsVisible(false);
          }
        }
        // refresh current tab items
        Object[] array = tabs.get(tabbedPane.getSelectedComponent());
        if (array != null) {
          MainTView mainView = (MainTView)array[0];
          newPanel = mainView.getTrackerPanel();
          prevPanel = newPanel;
          // update prefsDialog
          if (prefsDialog!=null) {
          	prefsDialog.trackerPanel = newPanel;
          }
          // refresh the notes dialog and button
          newPanel.refreshNotesDialog();
          JButton notesButton = getToolBar(newPanel).notesButton;
          notesButton.setSelected(notesDialog.isVisible());
          // refresh trackbar
          ((TTrackBar)array[5]).refresh();
          // refresh and replace menu bar
          TMenuBar menubar = (TMenuBar)array[4];
          refreshOpenRecentMenu(menubar.openRecentMenu);
//          menubar.refresh();
          setJMenuBar(menubar);
          // show floating player
          playerBar = mainView.getPlayerBar();
          Container frame = playerBar.getTopLevelAncestor();
          if (frame != null && frame != TFrame.this)
            frame.setVisible(true);
          if (newPanel.dataBuilder != null) 
          	newPanel.dataBuilder.setVisible(newPanel.dataToolVisible);
          Video vid = newPanel.getVideo();
          if (vid != null) {
            vid.getFilterStack().setInspectorsVisible(true);
          }
        }
        else {
        	// show defaultMenuBar
        	refreshOpenRecentMenu(recentMenu);
          setJMenuBar(defaultMenuBar);
        }
        // update prefsDialog
        if (prefsDialog!=null && prefsDialog.isVisible()) {
        	prefsDialog.refreshGUI();
        }
        firePropertyChange("tab", oldPanel, newPanel); //$NON-NLS-1$
      }
    });
    closeItem = new JMenuItem();
    closeItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        removeTab(getTrackerPanel(getSelectedTab()));
      }
    });
    popup.add(closeItem);
    tabbedPane.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        int i = getSelectedTab();
        if (i < 0) return;
        TrackerPanel panel = getTrackerPanel(i);
        if (panel==null || !panel.isEnabled("file.close")) return; //$NON-NLS-1$
      	if (OSPRuntime.isPopupTrigger(e)) {
          closeItem.setText(TrackerRes.getString("TActions.Action.Close") + " \"" //$NON-NLS-1$ //$NON-NLS-2$
                            + tabbedPane.getTitleAt(getSelectedTab()) + "\""); //$NON-NLS-1$
        	FontSizer.setFonts(popup, FontSizer.getLevel());
          popup.show(tabbedPane, e.getX(), e.getY());
        }
      }
    });
  }
  
  private void createDefaultMenuBar() {
    // create the default (empty) menubar
    int keyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    defaultMenuBar = new JMenuBar();
    setJMenuBar(defaultMenuBar);
    // file menu
    JMenu fileMenu = new JMenu(TrackerRes.getString("TMenuBar.Menu.File")); //$NON-NLS-1$
    defaultMenuBar.add(fileMenu);
    // new tab item
    JMenuItem newItem = new JMenuItem(TrackerRes.getString("TActions.Action.NewTab")); //$NON-NLS-1$
    newItem.setAccelerator(KeyStroke.getKeyStroke('N', keyMask));
    newItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        TrackerPanel newPanel = new TrackerPanel();
        newPanel.changed = false;
        addTab(newPanel);
        setSelectedTab(newPanel);
        JSplitPane pane = getSplitPane(newPanel, 0);
        pane.setDividerLocation(defaultRightDivider);
        refresh();
      }
    });
    fileMenu.add(newItem);
    if( org.opensourcephysics.display.OSPRuntime.applet == null) {
      fileMenu.addSeparator();
      // open file item
      Icon icon = new ImageIcon(Tracker.class.getResource("resources/images/open.gif")); //$NON-NLS-1$
      JMenuItem openItem = new JMenuItem(TrackerRes.getString("TActions.Action.Open"), icon); //$NON-NLS-1$
      openItem.setAccelerator(KeyStroke.getKeyStroke('O', keyMask));
      openItem.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          TFrame.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
          TrackerIO.open((File)null, TFrame.this);
          TFrame.this.setCursor(Cursor.getDefaultCursor());
        }
      });
      fileMenu.add(openItem);
      // open recent menu
      recentMenu = new JMenu();
      fileMenu.add(recentMenu);
    	refreshOpenRecentMenu(recentMenu);
      fileMenu.addSeparator();
      // openBrowser item
      icon = new ImageIcon(Tracker.class.getResource("resources/images/open_catalog.gif")); //$NON-NLS-1$
      JMenuItem openBrowserItem = new JMenuItem(TrackerRes.getString("TActions.Action.OpenBrowser"), icon); //$NON-NLS-1$
      openBrowserItem.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
  	      getLibraryBrowser().setVisible(true);
        }
      });
      fileMenu.add(openBrowserItem);
      fileMenu.addSeparator();
      // exit item
      JMenuItem exitItem = new JMenuItem(TrackerRes.getString("TActions.Action.Exit")); //$NON-NLS-1$
      exitItem.setAccelerator(KeyStroke.getKeyStroke('Q', keyMask));
      exitItem.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {System.exit(0);}
      });
      fileMenu.add(exitItem);
    }
    // edit menu
    JMenu editMenu = new JMenu(TrackerRes.getString("TMenuBar.Menu.Edit")); //$NON-NLS-1$
    defaultMenuBar.add(editMenu);
    // language menu
    JMenu languageMenu = new JMenu(TrackerRes.getString("TMenuBar.MenuItem.Language")); //$NON-NLS-1$
    editMenu.add(languageMenu);
    Action languageAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        String language = e.getActionCommand();
        for (int i = 0; i < Tracker.incompleteLocales.length; i++) {
          if (language.equals(Tracker.incompleteLocales[i][0].toString())) {
          	Locale locale = (Locale)Tracker.incompleteLocales[i][0];
          	String lang = OSPRuntime.getDisplayLanguage(locale);          	
          	// the following message is purposely not translated
          	JOptionPane.showMessageDialog(TFrame.this, 
          			"This translation has not been updated since "+Tracker.incompleteLocales[i][1] //$NON-NLS-1$
          			+".\nIf you speak "+lang+" and would like to help translate" //$NON-NLS-1$ //$NON-NLS-2$ 
          			+"\nplease contact Douglas Brown at [email protected].",  //$NON-NLS-1$
          			"Incomplete Translation: "+lang,  //$NON-NLS-1$
          			JOptionPane.WARNING_MESSAGE);
          }
        }
        for (int i = 0; i < Tracker.locales.length; i++) {
          if (language.equals(Tracker.locales[i].toString())) {
          	TrackerRes.setLocale(Tracker.locales[i]);
          	return;
          }
        }
      }
    };
    ButtonGroup languageGroup = new ButtonGroup();
    for (int i = 0; i < Tracker.locales.length; i++) {
	    String lang = OSPRuntime.getDisplayLanguage(Tracker.locales[i]);
    	// special handling for portuguese BR and PT
    	if (Tracker.locales[i].getLanguage().equals("pt")) { //$NON-NLS-1$
    		lang +=" ("+Tracker.locales[i].getCountry()+")"; //$NON-NLS-1$ //$NON-NLS-2$
    	}
      JMenuItem item = new JRadioButtonMenuItem(lang);
      item.setActionCommand(Tracker.locales[i].toString());
      item.addActionListener(languageAction);
      languageMenu.add(item);
      languageGroup.add(item);
      if (Tracker.locales[i].toString().equals(TrackerRes.locale.toString())) {
      	item.setSelected(true);
      }
    }
    // add "other" language item at end
  	// the following item and message is purposely not translated
    JMenuItem otherLanguageItem = new JMenuItem("Other"); //$NON-NLS-1$
    languageMenu.addSeparator();
    languageMenu.add(otherLanguageItem);
    otherLanguageItem.addActionListener(new ActionListener() {
    	public void actionPerformed(ActionEvent e) {
        JOptionPane.showMessageDialog(TFrame.this, 
	    			"Do you speak a language not yet available in Tracker?" //$NON-NLS-1$
	    			+"\nTo learn more about translating Tracker into your language" //$NON-NLS-1$ 
	    			+"\nplease contact Douglas Brown at [email protected].",  //$NON-NLS-1$
	    			"New Translation",  //$NON-NLS-1$
	    			JOptionPane.INFORMATION_MESSAGE);
    	}
    });
    // preferences item
    JMenuItem prefsItem = new JMenuItem(TrackerRes.getString("TActions.Action.Config")); //$NON-NLS-1$
    prefsItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      	showPrefsDialog();
      }
    });
    editMenu.addSeparator();
    editMenu.add(prefsItem);

    // video menu
    JMenu videoMenu = new JMenu(TrackerRes.getString("TMenuBar.Menu.Video")); //$NON-NLS-1$
    videoMenu.setEnabled(false);
    defaultMenuBar.add(videoMenu);
    // tracks menu
    JMenu tracksMenu = new JMenu(TrackerRes.getString("TMenuBar.Menu.Tracks")); //$NON-NLS-1$
    tracksMenu.setEnabled(false);
    defaultMenuBar.add(tracksMenu);
    // coords menu
    JMenu coordsMenu = new JMenu(TrackerRes.getString("TMenuBar.Menu.Coords")); //$NON-NLS-1$
    coordsMenu.setEnabled(false);
    defaultMenuBar.add(coordsMenu);
    // help menu
    JMenu helpMenu = TMenuBar.getTrackerHelpMenu(null);
    defaultMenuBar.add(helpMenu);
  }

  /**
   * Initializes a new tracker panel.
   *
   * @param trackerPanel the tracker panel
   */
  private void initialize(TrackerPanel trackerPanel) {
    // add a background mat if none exists
    if (trackerPanel.getMat()==null) {
      trackerPanel.addDrawable(new TMat(trackerPanel)); // constructor adds mat to panel
    }
    // add coordinate axes if none exists
    if (trackerPanel.getAxes() == null) {
      Tracker.setProgress(81);
      CoordAxes axes = new CoordAxes();
      axes.setVisible(false);
      trackerPanel.addTrack(axes);
    }
    // add video filters to the tracker panel
    trackerPanel.addFilter(DeinterlaceFilter.class);
    trackerPanel.addFilter(GhostFilter.class);
    trackerPanel.addFilter(StrobeFilter.class);
    trackerPanel.addFilter(DarkGhostFilter.class);
    trackerPanel.addFilter(NegativeFilter.class);
    trackerPanel.addFilter(GrayScaleFilter.class);
    trackerPanel.addFilter(BrightnessFilter.class);
    trackerPanel.addFilter(BaselineFilter.class);
    trackerPanel.addFilter(SumFilter.class);
    trackerPanel.addFilter(ResizeFilter.class);
    trackerPanel.addFilter(RotateFilter.class);
    trackerPanel.addFilter(PerspectiveFilter.class);
    trackerPanel.addFilter(RadialDistortionFilter.class);
    // set mouse handler
    trackerPanel.mouseHandler = new TMouseHandler();
    trackerPanel.setInteractiveMouseHandler(trackerPanel.mouseHandler);
    // set file drop handler
    trackerPanel.setTransferHandler(fileDropHandler);
    // set divider locations
    validate(); // in advance of setting divider locations
    if (trackerPanel.dividerLocs != null) {
      int w = 0;
      for (int i = 0; i < trackerPanel.dividerLocs.length; i++) {
        JSplitPane pane = getSplitPane(trackerPanel, i);
        if (i==0) w = pane.getMaximumDividerLocation();
        int max = i==3? w: pane.getMaximumDividerLocation();
        int loc = (int)(trackerPanel.dividerLocs[i]*max);
        pane.setDividerLocation(loc);
      }
      trackerPanel.dividerLocs = null;
    }
    else {
      setDividerLocation(trackerPanel, 0, 1.0); // becomes previous
      setDividerLocation(trackerPanel, 0, defaultRightDivider);
      setDividerLocation(trackerPanel, 1, 0.5);
      setDividerLocation(trackerPanel, 2, defaultBottomDivider); // becomes previous
      setDividerLocation(trackerPanel, 2, 1.0);
      setDividerLocation(trackerPanel, 3, 1.0); // becomes previous
      JSplitPane pane = getSplitPane(trackerPanel, 0);
      int max = pane.getMaximumDividerLocation();
      int loc = (int)(.5*defaultRightDivider*max);
      pane = getSplitPane(trackerPanel, 3);
      pane.setDividerLocation(loc);
    }
	  validate(); // after setting divider locations
    // set track control location
    if (trackerPanel.trackControlX != Integer.MIN_VALUE) {
      Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
      TrackControl tc = TrackControl.getControl(trackerPanel);
			int x = Math.max(getLocation().x + trackerPanel.trackControlX, 0);
			x = Math.min(x, dim.width-tc.getWidth());
			int y = Math.max(getLocation().y + trackerPanel.trackControlY, 0);
			y = Math.min(y, dim.height-tc.getHeight());
  		tc.setLocation(x, y);
  		tc.positioned = true;
    }
    // show filter inspectors
    trackerPanel.showFilterInspectors();
    // set initial format patterns for existing tracks
    trackerPanel.setInitialFormatPatterns();
    Tracker.setProgress(90);
  }
  
  /**
   * Returns an ObjectLoader to save and load data for this class.
   *
   * @return the object loader
   */
  public static XML.ObjectLoader getLoader() {
    return new Loader();
  }

  /**
   * A class to save and load data for this class.
   */
  static class Loader implements XML.ObjectLoader {

    /**
     * Saves an object's data to an XMLControl.
     *
     * @param control the control to save to
     * @param obj the object to save
     */
    public void saveObject(XMLControl control, Object obj) {
      TFrame frame = (TFrame)obj;
      // save tabs with data files or unchanged videos
      // save both relative paths (relative to tabsetFile) and absolute paths
      String relativeTo = frame.tabsetFile!=null? 
      		XML.getDirectoryPath(XML.getAbsolutePath(frame.tabsetFile)): 
      		XML.getUserDirectory();
      		relativeTo = XML.forwardSlash(relativeTo);
      ArrayList<String[]> pathList = new ArrayList<String[]>();
      for (int i = 0; i < frame.getTabCount(); i++) {
      	TrackerPanel trackerPanel = frame.getTrackerPanel(i);
      	File file = trackerPanel.getDataFile();
      	if (file!=null) {
      		String path = XML.getAbsolutePath(file);
      		String relativePath = XML.getPathRelativeTo(path, relativeTo);
      		pathList.add(new String[] {path, relativePath});
      	}
      	else {
	    		Video video = trackerPanel.getVideo();
	    		if (!trackerPanel.changed && video!=null) {
	      		String path = (String)video.getProperty("absolutePath"); //$NON-NLS-1$
	      		if (path!=null) {
		      		path = XML.forwardSlash(path);
	        		String relativePath = XML.getPathRelativeTo(path, relativeTo);
	        		pathList.add(new String[] {path, relativePath});
	      		}
	    		}      		
      	}
      }
      String[][] paths = pathList.toArray(new String[0][0]);
      control.setValue("tabs", paths); //$NON-NLS-1$
    }

    /**
     * Creates a new object. This returns null--must load an existing TFrame.
     *
     * @param control the XMLControl with the object data
     * @return the newly created object
     */
    public Object createObject(XMLControl control){
      return null;
    }

    /**
     * Loads an object with data from an XMLControl.
     *
     * @param control the control
     * @param obj the object
     * @return the loaded object
     */
    public Object loadObject(XMLControl control, Object obj) {
    	TFrame frame = (TFrame) obj;
    	FileFilter videoFilter = new VideoFileFilter();
      String[][] tabs = (String[][])control.getObject("tabs"); //$NON-NLS-1$
			String base = control.getString("basepath"); //$NON-NLS-1$
			File dataFile = null;
      if (tabs!=null) {
      	boolean prev = TrackerIO.loadInSeparateThread;
      	TrackerIO.loadInSeparateThread = false;
      	for (String[] next: tabs) {
      		File file = null;
      		Resource res = null;
      		if (base!=null) {
      			file = new File(base, next[1]); // next[1] is relative path
      			res = ResourceLoader.getResource(file.getPath());
      		}
      		if (res==null) {
      			file = new File(XML.getUserDirectory(), next[1]);
      			res = ResourceLoader.getResource(file.getPath());
      		}
      		if (res==null && next[0]!=null) {
      			file = new File(next[0]); // next[0] is absolute path
      			res = ResourceLoader.getResource(file.getPath());
      		}
      		if (res==null) {
            int i = JOptionPane.showConfirmDialog(frame, "\""+next[1]+"\" "                                       //$NON-NLS-1$ //$NON-NLS-2$
                +MediaRes.getString("VideoClip.Dialog.VideoNotFound.Message"),                                  //$NON-NLS-1$
                TrackerRes.getString("TFrame.Dialog.FileNotFound.Title"),                                   //$NON-NLS-1$
                JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
            if(i==JOptionPane.YES_OPTION) {
              TrackerIO.getChooser().setSelectedFile(file);
              java.io.File[] files = TrackerIO.getChooserFiles("open");                                         //$NON-NLS-1$
              if(files!=null) {
                file = files[0];
              }
              else continue;
            }
            else continue;
      		}
      		if (res!=null && !videoFilter.accept(file)) {
      			if (dataFile==null) dataFile = file;
      			TrackerIO.open(file, frame);
      		}
      	}
      	TrackerIO.loadInSeparateThread = prev;
      }
      int n = frame.getTab(dataFile);
      OSPLog.finest("selecting first tabset tab at index "+n); //$NON-NLS-1$
      frame.setSelectedTab(n);
      return obj;
    }
  }


}