/*
* 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.io.*;
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.text.html.HTMLDocument;
import javax.swing.tree.*;
import org.opensourcephysics.controls.*;
import org.opensourcephysics.display.OSPRuntime;
import org.opensourcephysics.tools.LaunchNode.DisplayTab;
/**
* This is a panel that displays a tree with a LaunchNode root.
*
* @author Douglas Brown
* @version 1.0
*/
public class LaunchPanel extends JPanel {
// static fields
protected static final String TEXT_TYPE = "text"; // text editor type //$NON-NLS-1$
// instance fields
protected JTree tree;
protected DefaultTreeModel treeModel;
protected JSplitPane splitPane;
protected JPanel dataPanel; // on splitPane right
protected JTabbedPane tabbedPane; // each tab is an html page or Java model
protected JEditorPane descriptionPane;
protected JScrollPane descriptionScroller;
protected boolean showAllNodes;
protected Map<LaunchNode, VisibleNode> visibleNodeMap = new HashMap<LaunchNode, VisibleNode>();
protected Launcher launcher;
protected boolean rebuildingTabs;
protected Map<String, String> htmlSubstitutions = new TreeMap<String, String>();
/**
* Constructor.
*
* @param rootNode the root node
* @param launcher the Launcher that is creating this panel
*/
public LaunchPanel(LaunchNode rootNode, Launcher launcher) {
showAllNodes = launcher instanceof LaunchBuilder;
this.launcher = launcher;
createGUI();
createTree(rootNode);
setSelectedNode(rootNode);
}
/**
* Sets the selected node.
*
* @param node the node to select
*/
public void setSelectedNode(LaunchNode node) {
if(node==null) {
return;
}
if(node==getSelectedNode()) {
displayTabs(node);
} else {
tree.setSelectionPath(new TreePath(node.getPath()));
}
}
/**
* Sets the selected nodes.
*
* @param nodes the nodes to select
*/
public void setSelectedNodes(ArrayList<LaunchNode> nodes) {
if(nodes==null || nodes.size()==0) {
return;
}
TreePath[] paths = new TreePath[nodes.size()];
int i = 0;
for (LaunchNode node: nodes) {
paths[i++] = new TreePath(node.getPath());
}
tree.setSelectionPaths(paths);
}
/**
* Sets the selected node.
*
* @param node the node to select
* @param tabNumber the tab to display
*/
public void setSelectedNode(LaunchNode node, int tabNumber) {
setSelectedNode(node, tabNumber, null);
}
/**
* Sets the selected node and displays a URL.
*
* @param node the node to select
* @param tabNumber the tab to display
* @param url the URL to display in the tab
*/
public void setSelectedNode(LaunchNode node, int tabNumber, java.net.URL url) {
node.tabNumber = ((url==null)&&(node.getDisplayTabCount()==0))
? -1
: tabNumber;
LaunchNode.DisplayTab htmlData = null;
URL prevURL = null;
if (node.tabNumber>-1) {
htmlData = node.tabData.get(node.tabNumber);
}
if (htmlData!=null) {
prevURL = htmlData.url;
}
if (url!=null) {
// set url to include anchor, if any
node.htmlURL = url;
if (htmlData!=null) {
htmlData.url = url;
}
}
setSelectedNode(node);
// restore previous URL
if (htmlData!=null) {
htmlData.url = prevURL;
}
}
/**
* Gets the selected node.
*
* @return the selected node
*/
public LaunchNode getSelectedNode() {
TreePath path = tree.getSelectionPath();
if(path==null) {
return null;
}
return(LaunchNode) path.getLastPathComponent();
}
/**
* Gets the selected nodes.
*
* @return the selected nodes
*/
public ArrayList<LaunchNode> getSelectedNodes() {
TreePath[] paths = tree.getSelectionPaths();
if(paths==null) {
return null;
}
// assemble list of selected nodes
ArrayList<LaunchNode> temp = new ArrayList<LaunchNode>();
for (int i = 0; i < paths.length; i++) {
temp.add((LaunchNode)paths[i].getLastPathComponent());
}
// use preorder enumeration to put list in top-bottom order
ArrayList<LaunchNode> nodes = new ArrayList<LaunchNode>();
Enumeration<?> e = getRootNode().preorderEnumeration();
while(e.hasMoreElements()) {
LaunchNode next = (LaunchNode) e.nextElement();
if (temp.contains(next))
nodes.add(next);
}
return nodes;
}
/**
* Gets the selected display tab.
*
* @return the selected display tab
*/
public int getSelectedDisplayTab() {
return tabbedPane.getSelectedIndex();
}
/**
* Gets the root node.
*
* @return the root node
*/
public LaunchNode getRootNode() {
return(LaunchNode) treeModel.getRoot();
}
/**
* Gets the HTML substitution map. This maps target to replacement Strings to be
* substituted in HTML documents.
*
* @return the HTML substitution map
*/
public Map<String, String> getHTMLSubstitutionMap() {
return htmlSubstitutions;
}
// ______________________________ protected methods _____________________________
/**
* Returns the node with the same file name as the specified node.
* May return null.
*
* @param node the node to match
* @return the first node with the same file name
*/
protected LaunchNode getClone(LaunchNode node) {
if(node.getFileName()==null) {
return null;
}
Enumeration<?> e = getRootNode().breadthFirstEnumeration();
while(e.hasMoreElements()) {
LaunchNode next = (LaunchNode) e.nextElement();
if(node.getFileName().equals(next.getFileName())) {
return next;
}
}
return null;
}
/**
* Displays all tabs for the specified node.
*
* @param node the LaunchNode
*/
protected void displayTabs(LaunchNode node) {
if(node!=null) {
OSPLog.finer(LaunchRes.getString("Log.Message.NodeSelected") //$NON-NLS-1$
+" "+node); //$NON-NLS-1$
boolean isBuilder = launcher instanceof LaunchBuilder;
String noTitle = LaunchRes.getString("HTMLTab.Title.Untitled"); //$NON-NLS-1$
java.net.URL url = node.htmlURL; // what URL to display
// don't display PDF files in Launcher
if (url!=null && url.getPath().toLowerCase().endsWith("pdf")) //$NON-NLS-1$
url = null;
int tabNumber = node.tabNumber; // which tab to display it in
boolean hasModel = false;
if(url==null && node.getDisplayTabCount()>0) {
// skip display tabs with PDFs
int k = 0;
for (int i=tabNumber; i<node.getDisplayTabCount(); i++) {
DisplayTab next = node.getDisplayTab(i);
// next!=null condiiton added by W. Christian
if(next!=null && next.url!=null && next.url.getPath().toLowerCase().endsWith(".pdf")) { //$NON-NLS-1$
k++;
}
else break;
}
// node has multiple URLs so pick the tab-associated one
tabNumber = Math.max(0, tabNumber);
DisplayTab displayTab = node.getDisplayTab(tabNumber+k);
if(displayTab==null){ // null check added by W.Chrisitan
// do nothing
}else if(displayTab.url!=null) {
url = displayTab.url;
} else {
hasModel = displayTab.getModelClass() != null;
}
}
// don't display PDF files in Launcher
if (url!=null && url.getPath().toLowerCase().endsWith("pdf")) //$NON-NLS-1$
url = null;
// rebuild tabs
rebuildingTabs = true;
int tabCount = 0;
if(!isBuilder) {
tabbedPane.removeAll();
}
Iterator<?> it = node.tabData.iterator();
while(it.hasNext()) {
final LaunchNode.DisplayTab displayTab = (LaunchNode.DisplayTab) it.next();
if(displayTab.url!=null && !displayTab.url.getPath().toLowerCase().endsWith(".pdf")) { //$NON-NLS-1$
try {
if(displayTab.url.getContent()!=null) {
final Launcher.HTMLPane html = launcher.getHTMLTab(tabCount);
final java.net.URL theURL = ((tabNumber==tabCount)&&(url!=null))
? url
: displayTab.url;
final boolean nodeEnabled = node.enabled;
Runnable runner = new Runnable() {
public void run() {
try {
if (htmlSubstitutions.isEmpty()) {
// the traditional method
html.editorPane.setPage(theURL);
}
else {
// read url into a string
Scanner scanner = new Scanner(theURL.openStream(), "UTF-8"); //$NON-NLS-1$
String text = scanner.useDelimiter("\\A").next(); //$NON-NLS-1$
scanner.close();
// make html substitutions
for (String target: htmlSubstitutions.keySet()) {
String newValue = htmlSubstitutions.get(target);
text = text.replaceAll(target, newValue);
}
// set document base for relative paths
HTMLDocument doc = (HTMLDocument)html.editorPane.getDocument();
doc.setBase(theURL);
// set editorPane text
html.editorPane.setText(text);
// scroll to top
html.editorPane.setCaretPosition(0);
}
} catch(IOException e) {
}
if(theURL.getRef()!=null) {
html.editorPane.scrollToReference(theURL.getRef());
if (FontSizer.getLevel()>0) {
// invoke scrollToReference again later at high font levels
Timer timer = new Timer(100, new ActionListener() {
public void actionPerformed(ActionEvent e) {
html.editorPane.scrollToReference(theURL.getRef());
}
});
timer.setRepeats(false);
timer.start();
}
}
launcher.setLinksEnabled(html.editorPane, displayTab.hyperlinksEnabled && nodeEnabled);
}
};
if(SwingUtilities.isEventDispatchThread()) {
runner.run();
} else {
SwingUtilities.invokeLater(runner);
}
if(!isBuilder) {
String title = (displayTab.title==null)
? noTitle
: displayTab.title;
tabbedPane.addTab(title, html.scroller);
tabCount++;
}
}
} catch(IOException ex) {
OSPLog.fine(LaunchRes.getString("Log.Message.BadURL") //$NON-NLS-1$
+" "+displayTab.url); //$NON-NLS-1$
}
}
else if (displayTab.getModelScroller() != null) {
if(!isBuilder) {
String title = (displayTab.title==null)
? noTitle
: displayTab.title;
tabbedPane.addTab(title, displayTab.getModelScroller());
tabCount++;
}
}
}
// display appropriate tabs
if(!isBuilder) {
if(url!=null || hasModel) {
if((tabbedPane.getTabCount()==1)&&tabbedPane.getTitleAt(0).equals(noTitle)) {
splitPane.setRightComponent(tabbedPane.getComponentAt(0));
}
else if(tabbedPane.getTabCount()>0) {
splitPane.setRightComponent(tabbedPane);
if(tabbedPane.getTabCount()>tabNumber) {
tabbedPane.setSelectedIndex(tabNumber);
}
}
}
else {
JScrollPane launchPane = node.getLaunchModelScroller();
if (launchPane != null) {
splitPane.setRightComponent(launchPane);
}
else {
descriptionPane.setText(node.description);
splitPane.setRightComponent(descriptionScroller);
}
}
}
rebuildingTabs = false;
launcher.refreshGUI();
}
}
/**
* Creates the GUI.
*/
protected void createGUI() {
setPreferredSize(new Dimension(400, 200));
setLayout(new BorderLayout());
splitPane = new JSplitPane();
add(splitPane, BorderLayout.CENTER);
dataPanel = new JPanel(new BorderLayout());
descriptionPane = new JTextPane() {
public void paintComponent(Graphics g) {
if(OSPRuntime.antiAliasText) {
Graphics2D g2 = (Graphics2D) g;
RenderingHints rh = g2.getRenderingHints();
rh.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
super.paintComponent(g);
}
};
descriptionPane.setEditable(false);
descriptionPane.setContentType(LaunchPanel.TEXT_TYPE);
descriptionScroller = new JScrollPane(descriptionPane);
// create the tabbed pane
tabbedPane = new JTabbedPane(SwingConstants.TOP);
tabbedPane.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
LaunchNode node = getSelectedNode();
if((node!=null)&&(node==launcher.selectedNode)) {
if (rebuildingTabs && node.htmlURL != null) return;
// save prev html properties
node.prevTabNumber = node.tabNumber;
node.prevURL = node.htmlURL;
// set new html properties
int n = Math.max(0, tabbedPane.getSelectedIndex());
node.tabNumber = node.tabData.isEmpty()
? -1
: n;
if(node.tabNumber>-1) {
DisplayTab displayTab = node.getDisplayTab(node.tabNumber);
if(displayTab.url!=null) {
node.htmlURL = displayTab.url;
}
else {
Launcher.HTMLPane tab = launcher.getHTMLTab(node.tabNumber);
node.htmlURL = tab.editorPane.getPage();
}
// OSPLog.info("set node "+node+" to "+node.htmlURL //$NON-NLS-1$
// +"and tabnumber "+node.tabNumber); //$NON-NLS-1$
}
}
}
});
tabbedPane.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
LaunchNode node = getSelectedNode();
if((node!=null)&&launcher.postEdits) {
String nodePath = node.getPathString();
Integer undoPage = new Integer(node.prevTabNumber);
Integer redoPage = new Integer(node.tabNumber);
Object[] undoData = new Object[] {null, nodePath, undoPage, node.prevURL};
Object[] redoData = new Object[] {null, nodePath, redoPage, node.htmlURL};
LauncherUndo.NavEdit edit = launcher.undoManager.new NavEdit(undoData, redoData);
launcher.undoSupport.postEdit(edit);
}
}
});
splitPane.setRightComponent(dataPanel);
splitPane.setDividerLocation(160);
}
/**
* Creates the tree.
*
* @param rootNode the root node
*/
protected void createTree(LaunchNode rootNode) {
// if not showing all nodes, create the VisibleNode structure
if(!showAllNodes) {
VisibleNode visibleRoot = new VisibleNode(rootNode);
visibleNodeMap.put(rootNode, visibleRoot);
addVisibleNodes(visibleRoot);
}
treeModel = new LaunchTreeModel(rootNode);
tree = new JTree(treeModel);
tree.setToolTipText(""); // enables tool tips for nodes //$NON-NLS-1$
tree.setRootVisible(!rootNode.hiddenWhenRoot);
if (launcher instanceof LaunchBuilder)
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
else
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
LaunchNode node = launcher.getSelectedNode();
if(launcher.postEdits) {
// post undoable NavEdit if prev treePath is not null
TreePath treePath = e.getOldLeadSelectionPath();
if((treePath!=null)&&(node!=null)) {
LaunchNode prevNode = (LaunchNode) treePath.getLastPathComponent();
// set html properties of newly selected node
if(!node.tabData.isEmpty()) {
int page = Math.max(0, node.tabNumber);
LaunchNode.DisplayTab htmlData = node.tabData.get(page);
node.htmlURL = htmlData.url;
node.tabNumber = page;
}
LauncherUndo.NavEdit edit = launcher.undoManager.new NavEdit(prevNode, node);
launcher.undoSupport.postEdit(edit);
}
}
displayTabs(node);
}
});
// put tree in a scroller and add to the split pane
JScrollPane treeScroller = new JScrollPane(tree);
splitPane.setLeftComponent(treeScroller);
}
/**
* Returns a collection of nodes that are currently expanded.
*
* @return the expanded nodes
*/
protected Collection<String> getExpandedNodes() {
Collection<String> list = new ArrayList<String>();
TreePath path = new TreePath(getRootNode());
Enumeration<?> en = tree.getExpandedDescendants(path);
while((en!=null)&&en.hasMoreElements()) {
TreePath next = (TreePath) en.nextElement();
LaunchNode node = (LaunchNode) next.getLastPathComponent();
list.add(node.getPathString());
}
return list;
}
/**
* Expands the specified nodes.
*
* @param expanded the nodes to expand
*/
protected void setExpandedNodes(Collection<?> expanded) {
Iterator<?> it = expanded.iterator();
while(it.hasNext()) {
// get node path
String path = it.next().toString();
Enumeration<?> e = getRootNode().breadthFirstEnumeration();
while(e.hasMoreElements()) {
LaunchNode node = (LaunchNode) e.nextElement();
if(path.equals(node.getPathString())) { // found node
// get treePath
TreePath treePath = new TreePath(node.getPath());
tree.expandPath(treePath);
}
}
}
}
/**
* A tree model class for launch nodes.
*/
class LaunchTreeModel extends DefaultTreeModel {
LaunchTreeModel(LaunchNode root) {
super(root);
}
public Object getChild(Object parent, int index) {
if(showAllNodes) {
return super.getChild(parent, index);
}
VisibleNode visibleParent = visibleNodeMap.get(parent);
if(visibleParent!=null) {
VisibleNode visibleChild = (VisibleNode) visibleParent.getChildAt(index);
if(visibleChild!=null) {
return visibleChild.node;
}
}
return null;
}
public int getChildCount(Object parent) {
if(showAllNodes) {
return super.getChildCount(parent);
}
VisibleNode visibleParent = visibleNodeMap.get(parent);
if(visibleParent!=null) {
return visibleParent.getChildCount();
}
return 0;
}
public int getIndexOfChild(Object parent, Object child) {
if(showAllNodes) {
return super.getIndexOfChild(parent, child);
}
VisibleNode visibleParent = visibleNodeMap.get(parent);
VisibleNode visibleChild = visibleNodeMap.get(child);
if((visibleParent!=null)&&(visibleChild!=null)) {
return visibleParent.getIndex(visibleChild);
}
return -1;
}
}
/**
* A tree node that references a launch node.
*/
private class VisibleNode extends DefaultMutableTreeNode {
LaunchNode node;
VisibleNode(LaunchNode node) {
this.node = node;
}
}
private void addVisibleNodes(VisibleNode visibleParent) {
int n = visibleParent.node.getChildCount();
for(int i = 0; i<n; i++) {
LaunchNode child = (LaunchNode) visibleParent.node.getChildAt(i);
if(child.isHiddenInLauncher()) {
continue;
}
VisibleNode visibleChild = new VisibleNode(child);
visibleNodeMap.put(child, visibleChild);
visibleParent.add(visibleChild);
addVisibleNodes(visibleChild);
}
}
}
/*
* Open Source Physics software is free software; you can redistribute
* it and/or modify it under the terms of the GNU General Public License (GPL) as
* published by the Free Software Foundation; either version 2 of the License,
* or(at your option) any later version.
* Code that uses any portion of the code in the org.opensourcephysics package
* or any subpackage (subdirectory) of this package must must also be be released
* under the GNU GPL license.
*
* This software 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 this; 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
*
* Copyright (c) 2007 The Open Source Physics project
* http://www.opensourcephysics.org
*/