/*
* Open Source Physics software is free software as described near the bottom of this code file.
*
* For additional information and documentation on Open Source Physics please see:
* <http://www.opensourcephysics.org/>
*/
package org.opensourcephysics.tools;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.display.OSPRuntime;
/**
* A dialog for managing autoloadable functions for a FunctionTool. The functions are
* organized by directory and fileName. Each function is described by a name,
* expression and optional descriptor (eg track type for TrackDataBuilder.AutoloadManager).
*
* @author Douglas Brown
*/
public abstract class AbstractAutoloadManager extends JDialog {
SearchPathDialog searchPathDialog;
String searchPathChooserDir = System.getProperty("user.home"); //$NON-NLS-1$
Collection<String> searchPaths = new TreeSet<String>();
JPanel functionPanel, instructionPanel;
Box functionBox;
Font lightFont, heavyFont;
JButton closeButton, searchPathsButton;
JTextArea instructionArea;
Dimension defaultSize = new Dimension(450, 400);
Map<String, Map<String, ArrayList<String[]>>> autoloadData;
int inset0 = 6, inset1 = 20, inset2 = 40;
boolean refreshing = false;
protected boolean initialized = false;
/**
* Constructor for a dialog, typically a FunctionTool.
*
* @param dialog the dialog
*/
protected AbstractAutoloadManager(JDialog dialog) {
super(dialog, true);
setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
createGUI();
Dimension dim = new Dimension(defaultSize);
double factor = 1+FontSizer.getLevel()*0.25;
dim.width = (int)(dim.width*factor);
dim.height = (int)(dim.height*factor);
setSize(dim);
}
/**
* Sets the autoload data. The data is a map of directory path to directory
* contents, where directory contents is a map of file path to file contents,
* file contents is a list of function arrays, and each function array
* is {String name, String expression, optional String descriptor)
*
* @param data the data
*/
public void setAutoloadData(Map<String, Map<String, ArrayList<String[]>>> data) {
autoloadData = data;
refreshFunctionList();
}
/**
* Sets the instructions describing how to use this manager.
*
* @param instructions the instructions
*/
public void setInstructions(String instructions) {
instructionArea.setText(instructions);
if (instructions!=null) {
getContentPane().add(instructionPanel, BorderLayout.NORTH);
}
else {
getContentPane().remove(instructionPanel);
}
}
/**
* Add a search path.
*
* @param dir the (directory) search path to add
*/
public void addSearchPath(String dir) {
searchPaths.add(XML.forwardSlash(dir));
}
/**
* Gets the collection (shallow clone) of search paths.
*
* @return the search paths
*/
public Collection<String> getSearchPaths() {
Collection<String> paths = new TreeSet<String>();
paths.addAll(searchPaths);
return paths;
}
/**
* Creates the GUI.
*/
protected void createGUI() {
heavyFont = new JLabel().getFont().deriveFont(Font.BOLD);
lightFont = heavyFont.deriveFont(Font.PLAIN);
// create instructions
instructionArea = new JTextArea();
instructionArea.setEditable(false);
instructionArea.setLineWrap(true);
instructionArea.setWrapStyleWord(true);
Border etched = BorderFactory.createEtchedBorder();
Border empty = BorderFactory.createEmptyBorder(2,4,2,4);
instructionArea.setBorder(BorderFactory.createCompoundBorder(etched, empty));
instructionArea.setForeground(Color.blue);
instructionPanel = new JPanel(new BorderLayout());
instructionPanel.setBorder(BorderFactory.createEmptyBorder(6,6,6,6));
instructionPanel.add(instructionArea, BorderLayout.CENTER);
// create function panel
functionPanel = new JPanel(new BorderLayout());
functionBox = Box.createVerticalBox();
functionBox.setBackground(Color.white);
functionBox.setOpaque(true);
refreshFunctionList();
JScrollPane scroller = new JScrollPane(functionBox);
scroller.getVerticalScrollBar().setUnitIncrement(8);
functionPanel.add(scroller, BorderLayout.CENTER);
functionPanel.setBorder(BorderFactory.createEmptyBorder(0,6,0,6));
// create buttons and buttonbar
closeButton = new JButton();
closeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
setVisible(false);
}
});
searchPathsButton = new JButton();
searchPathsButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// show search dialog
if (searchPathDialog==null) {
searchPathDialog = new SearchPathDialog();
searchPathDialog.setLocationRelativeTo(AbstractAutoloadManager.this);
}
FontSizer.setFonts(searchPathDialog, FontSizer.getLevel());
searchPathDialog.refreshGUI();
searchPathDialog.refreshFileList();
searchPathDialog.setVisible(true);
}
});
JPanel buttonbar = new JPanel();
buttonbar.add(searchPathsButton);
buttonbar.add(closeButton);
// assemble content pane
JPanel contentPane = new JPanel(new BorderLayout());
setContentPane(contentPane);
contentPane.add(functionPanel, BorderLayout.CENTER);
contentPane.add(buttonbar, BorderLayout.SOUTH);
refreshGUI();
}
/**
* Refreshes the GUI including locale-based resource strings.
*/
protected void refreshGUI() {
setTitle(ToolsRes.getString("AutoloadManager.Title")); //$NON-NLS-1$
closeButton.setText(ToolsRes.getString("Button.OK")); //$NON-NLS-1$
searchPathsButton.setText(ToolsRes.getString("AutoloadManager.Button.SearchPaths")+"..."); //$NON-NLS-1$ //$NON-NLS-2$
refreshFunctionList();
}
/**
* Refreshes the function list.
*/
protected void refreshFunctionList() {
// refresh list of functions
refreshing = true;
functionBox.removeAll();
if (autoloadData==null) return;
String directoryTitle = ToolsRes.getString("AutoloadManager.Directory")+": "; //$NON-NLS-1$ //$NON-NLS-2$
for (String dir: autoloadData.keySet()) {
Box dirBox = Box.createVerticalBox();
TitledBorder border = BorderFactory.createTitledBorder(directoryTitle+XML.forwardSlash(dir));
Font titleFont = FontSizer.getResizedFont(heavyFont, FontSizer.getLevel());
border.setTitleFont(titleFont);
Border spacer = BorderFactory.createEmptyBorder(6, 0, 6, 0);
dirBox.setBorder(BorderFactory.createCompoundBorder(spacer, border));
functionBox.add(dirBox);
Map<String, ArrayList<String[]>> functionMap = autoloadData.get(dir);
// display files in alphabetical order ignoring case
Map<String, String> lowercaseNames = new TreeMap<String, String>();
for (String fileName: functionMap.keySet()) {
lowercaseNames.put(fileName.toLowerCase(), fileName);
}
for (String lowercase: lowercaseNames.keySet()) {
int top = dirBox.getComponentCount()==0? 0: 10;
String fileName = lowercaseNames.get(lowercase);
File file = new File(dir, fileName);
String filePath = XML.forwardSlash(file.getAbsolutePath());
boolean enable = getFileSelectionState(filePath)!=TristateCheckBox.NOT_SELECTED;
AutoloadFileCheckbox fileCheckbox = new AutoloadFileCheckbox(dir, fileName);
fileCheckbox.setBorder(BorderFactory.createEmptyBorder(top, inset1, 2, 0));
fileCheckbox.setFont(heavyFont);
Box bar = Box.createHorizontalBox();
bar.add(fileCheckbox);
bar.add(Box.createHorizontalGlue());
dirBox.add(bar);
Border empty = BorderFactory.createEmptyBorder(0, 0, 0, 40);
ArrayList<String[]> functionList = functionMap.get(fileName);
if (functionList.isEmpty()) {
Box labelBox = getEmptyMessage(inset2, enable);
dirBox.add(labelBox);
}
for (String[] f: functionList) {
// add category (track type) if present
JLabel label = null;
if (f.length>3) {
String s = "["+f[3]+"]"; //$NON-NLS-1$ //$NON-NLS-2$
label = new JLabel(s);
label.setBorder(empty);
label.setFont(lightFont);
}
AutoloadFunctionCheckbox checkbox = new AutoloadFunctionCheckbox(dir, fileName, f);
checkbox.setFont(lightFont);
checkbox.setEnabled(enable);
bar = Box.createHorizontalBox();
bar.add(checkbox);
bar.add(Box.createHorizontalGlue());
if (label!=null)
bar.add(label);
bar.setBorder(BorderFactory.createEmptyBorder(0, inset2, 0, 0));
dirBox.add(bar);
}
}
if (dirBox.getComponentCount()==0) {
dirBox.add(getEmptyMessage(inset1, true));
}
}
if (functionBox.getComponentCount()==0) {
functionBox.add(getEmptyMessage(inset0, true));
}
FontSizer.setFonts(functionBox, FontSizer.getLevel());
refreshing = false;
}
/**
* Sets the font level.
*
* @param level the desired font level
*/
public void setFontLevel(int level) {
FontSizer.setFonts(this, level);
FontSizer.setFonts(instructionArea, level);
}
/**
* Sets the selection state of a function.
*
* @param filePath the path to the file defining the function
* @param function the function {name, expression, optional descriptor}
* @param select true to select the function
*/
protected abstract void setFunctionSelected(String filePath, String[] function, boolean select);
/**
* Gets the selection state of a function.
*
* @param filePath the path to the file defining the function
* @param function the function {name, expression, optional descriptor}
* @return true if the function is selected
*/
protected abstract boolean isFunctionSelected(String filePath, String[] function);
/**
* Sets the selection state of a file.
*
* @param filePath the path to the file
* @param select true to select the file
*/
protected abstract void setFileSelected(String filePath, boolean select);
/**
* Gets the selection state of a file.
*
* @param filePath the path to the file
* @return TristateCheckBox.SELECTED, NOT_SELECTED or PART_SELECTED
*/
protected abstract TristateCheckBox.State getFileSelectionState(String filePath);
/**
* Refreshes the autoload data.
*/
protected abstract void refreshAutoloadData();
private Box getEmptyMessage(int inset, boolean enabled) {
JLabel label = new JLabel(ToolsRes.getString("AutoloadManager.Label.NoFunctionsFound")); //$NON-NLS-1$
label.setFont(lightFont);
label.setBorder(BorderFactory.createEmptyBorder(2, inset, 4, 0));
label.setEnabled(enabled);
Box bar = Box.createHorizontalBox();
bar.add(label);
bar.add(Box.createHorizontalGlue());
return bar;
}
/**
* A checkbox to indicate the status of an autoloadable function.
*/
private class AutoloadFunctionCheckbox extends JCheckBox {
String directory, fileName;
String[] function;
/**
* Constructs a AutoloadFunctionCheckbox.
*
* @param identifier the function identifier
*/
private AutoloadFunctionCheckbox(String dir, String name, String[] f) {
directory = dir;
fileName = name;
function = f;
File file = new File(directory, fileName);
final String filePath = XML.forwardSlash(file.getAbsolutePath());
setSelected(isFunctionSelected(filePath, function));
setText(f[0]+" = "+f[1]); //$NON-NLS-1$
setIconTextGap(10);
setOpaque(false);
setToolTipText(ToolsRes.getString("AutoloadManager.FunctionCheckbox.Tooltip")); //$NON-NLS-1$
addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
setFunctionSelected(filePath, function, AutoloadFunctionCheckbox.this.isSelected());
}
});
}
}
/**
* A tristate checkbox to indicate the status of an autoloadable file.
*/
private class AutoloadFileCheckbox extends TristateCheckBox {
String directory, fileName;
boolean selected;
/**
* Constructs a AutoloadFileCheckbox.
*
* @param identifier the file identifier
*/
private AutoloadFileCheckbox(String dir, String name) {
directory = dir;
fileName = name;
File file = new File(directory, fileName);
final String filePath = XML.forwardSlash(file.getAbsolutePath());
setState(getFileSelectionState(filePath));
setText(fileName);
setIconTextGap(10);
setOpaque(false);
setToolTipText(ToolsRes.getString("AutoloadManager.FileCheckbox.Tooltip")); //$NON-NLS-1$
// must use ChangeListener instead of ActionListener for TristateCheckbox
addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
if (refreshing) return;
if (getState()==TristateCheckBox.PART_SELECTED) {
refreshing = true;
AutoloadFileCheckbox.this.doClick(0);
refreshing = false;
return;
}
if (selected==AutoloadFileCheckbox.this.isSelected()) {
return;
}
selected = AutoloadFileCheckbox.this.isSelected();
setFileSelected(filePath, selected);
}
});
}
@Override
public void setState(State state) {
super.setState(state);
if (fileName==null) return;
if (state==null) selected = true;
else if (state==TristateCheckBox.NOT_SELECTED) selected = false;
else selected = true;
}
}
/**
* A dialog to add and remove search paths
*/
protected class SearchPathDialog extends JDialog {
HashSet<File> addedFiles = new HashSet<File>();
TreeSet<String> directoryPaths = new TreeSet<String>();
JButton okButton, addButton, removeButton;
JList directoryList;
DefaultListModel directoryListModel;
/**
* Constructor
*/
SearchPathDialog() {
super(AbstractAutoloadManager.this, true);
createGUI();
for (String dir: searchPaths) {
File file = new File(dir);
addedFiles.add(file);
}
refreshFileList();
}
/**
* Creates the GUI.
*/
private void createGUI() {
JPanel contentPane = new JPanel(new BorderLayout());
setContentPane(contentPane);
// directory list
directoryListModel = new DefaultListModel();
directoryList = new JList(directoryListModel);
directoryList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
String dir = (String)directoryList.getSelectedValue();
removeButton.setEnabled(dir!=null);
}
});
JScrollPane scroller = new JScrollPane(directoryList);
scroller.setPreferredSize(new Dimension(300, 150));
contentPane.add(scroller, BorderLayout.CENTER);
// button bar
JPanel buttonbar = new JPanel();
addButton = new JButton();
addButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// show file chooser to add folders
File file = chooseSearchDirectory(SearchPathDialog.this);
if (file==null) return; // cancelled by user
addedFiles.add(file);
String path = XML.forwardSlash(file.getAbsolutePath());
addSearchPath(path);
refreshFileList();
refreshAutoloadData();
}
});
removeButton = new JButton();
removeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String name = (String)directoryList.getSelectedValue();
if (name!=null) {
for (Iterator<File> it = addedFiles.iterator(); it.hasNext();) {
File next = it.next();
String nextPath = XML.forwardSlash(next.getAbsolutePath());
if (name.equals(nextPath)) {
it.remove();
searchPaths.remove(nextPath);
break;
}
}
refreshFileList();
refreshAutoloadData();
}
}
});
okButton = new JButton();
okButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
setVisible(false);
}
});
buttonbar.add(addButton);
buttonbar.add(removeButton);
buttonbar.add(okButton);
contentPane.add(buttonbar, BorderLayout.SOUTH);
pack();
}
/**
* Uses a JFileChooser to select a search directory.
*
* @param parent a component to own the file chooser
* @return the chosen file
*/
private File chooseSearchDirectory(Component parent) {
JFileChooser chooser = new JFileChooser(searchPathChooserDir);
if (OSPRuntime.isMac())
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
else
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
javax.swing.filechooser.FileFilter folderFilter = new javax.swing.filechooser.FileFilter() {
// accept directories only
public boolean accept(File f) {
if (f==null) return false;
return f.isDirectory();
}
public String getDescription() {
return ToolsRes.getString("LibraryTreePanel.FolderFileFilter.Description"); //$NON-NLS-1$
}
};
chooser.setAcceptAllFileFilterUsed(false);
chooser.addChoosableFileFilter(folderFilter);
String text = ToolsRes.getString("LibraryManager.Button.Add"); //$NON-NLS-1$
chooser.setDialogTitle(text);
FontSizer.setFonts(chooser, FontSizer.getLevel());
int result = chooser.showDialog(parent, text);
if (result==JFileChooser.APPROVE_OPTION) {
searchPathChooserDir = chooser.getCurrentDirectory().getAbsolutePath();
return chooser.getSelectedFile();
}
return null;
}
/**
* Refreshes the GUI.
*/
void refreshGUI() {
setTitle(ToolsRes.getString("AutoloadManager.Button.SearchPaths")); //$NON-NLS-1$
okButton.setText(ToolsRes.getString("Button.OK")); //$NON-NLS-1$
addButton.setText(ToolsRes.getString("LibraryManager.Button.Add")+"..."); //$NON-NLS-1$ //$NON-NLS-2$
removeButton.setText(ToolsRes.getString("LibraryManager.Button.Remove")); //$NON-NLS-1$
String dir = (String)directoryList.getSelectedValue();
removeButton.setEnabled(dir!=null);
}
/**
* Refreshes the file list.
*/
void refreshFileList() {
directoryListModel.clear();
directoryPaths.clear();
for (File next: addedFiles) {
directoryPaths.add(XML.forwardSlash(next.getAbsolutePath()));
}
for (String next: directoryPaths) {
directoryListModel.addElement(next);
}
}
}
}