/*******************************************************************************
* Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.framework.uitools;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.eclipse.persistence.tools.workbench.framework.action.AbstractFrameworkAction;
import org.eclipse.persistence.tools.workbench.framework.context.ApplicationContext;
import org.eclipse.persistence.tools.workbench.framework.ui.view.AbstractPanel;
import org.eclipse.persistence.tools.workbench.uitools.app.ListValueModel;
import org.eclipse.persistence.tools.workbench.uitools.app.SimplePropertyValueModel;
import org.eclipse.persistence.tools.workbench.uitools.app.ValueModel;
import org.eclipse.persistence.tools.workbench.uitools.app.swing.PrimitiveListTreeModel;
import org.eclipse.persistence.tools.workbench.utility.CollectionTools;
import org.eclipse.persistence.tools.workbench.utility.string.StringTools;
public final class ClasspathPanel
extends AbstractPanel
{
private ListValueModel classpathEntriesHolder;
private ValueModel rootFolderHolder;
private DefaultClasspathDirectoryHolder defaultClasspathDirectoryHolder;
/** for the actual list/tree */
private JTree classpathTree;
private TreeModel classpathModel;
private TreeSelectionModel classpathSelectionModel;
/** this indicates whether the classpath entries should be editable */
private boolean editable;
/** Determines if the path should be converted to a relative path or not. **/
private boolean convertToRelativePath;
/** this will add a new entry directly into the tree */
private Action addEntryAction;
/** this will open a class chooser dialog */
private Action browseAction;
/** this will remove the currently selected entries from the classpath */
private Action removeAction;
/** this will move the currently selected entry up one position in the classpath */
private Action upAction;
/** this will move the currently selected entry down one position in the classpath */
private Action downAction;
// *********** most recent directory preference
public static final String MOST_RECENT_CLASSPATH_DIRECTORY_PREFERENCE = "recent classpath directory";
// ********** constructors **********
public ClasspathPanel(ApplicationContext context, ListValueModel classpathEntriesHolder, ValueModel rootFileHolder) {
this(context, classpathEntriesHolder, rootFileHolder, true, "CLASSPATH_PANEL_TITLE");
}
public ClasspathPanel(ApplicationContext context, ListValueModel classpathEntriesHolder, boolean shouldBeEditable) {
this(context, classpathEntriesHolder, shouldBeEditable, "CLASSPATH_PANEL_TITLE");
}
public ClasspathPanel(ApplicationContext context, ListValueModel classpathEntriesHolder, boolean shouldBeEditable, String title) {
this(context, classpathEntriesHolder, new SimplePropertyValueModel(), shouldBeEditable, title);
}
public ClasspathPanel(ApplicationContext context, ListValueModel classpathEntriesHolder, ValueModel rootFileHolder, boolean shouldBeEditable) {
this(context, classpathEntriesHolder, rootFileHolder, shouldBeEditable, "CLASSPATH_PANEL_TITLE");
}
public ClasspathPanel(ApplicationContext context, ListValueModel classpathEntriesHolder, ValueModel rootFileHolder, boolean shouldBeEditable, String title) {
super(context);
initialize(classpathEntriesHolder, rootFileHolder, shouldBeEditable);
initializeLayout(title);
}
// ********** initialization **********
private void initialize(ListValueModel classpathEntriesHolder, ValueModel rootFolderHolder, boolean shouldBeEditable) {
this.classpathEntriesHolder = classpathEntriesHolder;
this.rootFolderHolder = rootFolderHolder;
this.defaultClasspathDirectoryHolder = DefaultClasspathDirectoryHolder.NULL_INSTANCE;
classpathModel = this.buildClasspathModel(classpathEntriesHolder);
classpathSelectionModel = this.buildClasspathSelectionModel();
addEntryAction = this.buildAddEntryAction();
browseAction = this.buildBrowseAction();
removeAction = this.buildRemoveAction();
upAction = this.buildUpAction();
downAction = this.buildDownAction();
this.editable = shouldBeEditable;
// When all the entries are removed, root node is been used for rendering
// so it needs a user object otherwise another check will have to be
// performed
((DefaultMutableTreeNode) classpathModel.getRoot()).setUserObject("");
}
protected ApplicationContext initializeContext(ApplicationContext parentContext) {
return parentContext.buildExpandedResourceRepositoryContext(UIToolsResourceBundle.class);
}
private TreeModel buildClasspathModel(ListValueModel model) {
return new PrimitiveListTreeModel(model) {
protected void primitiveChanged(int index, Object newValue) {
ClasspathPanel.this.replaceEntry(index, newValue);
}
};
}
private TreeModelListener buildTreeModelListener(final JTree classpathTree) {
return new TreeModelListener() {
public void treeNodesChanged(TreeModelEvent e) {
}
public void treeNodesInserted(TreeModelEvent e) {
TreeModel model = (TreeModel) e.getSource();
ExpandPathRunner runner = new ExpandPathRunner(classpathTree, model.getRoot());
EventQueue.invokeLater(runner);
}
public void treeNodesRemoved(TreeModelEvent e) {
}
public void treeStructureChanged(TreeModelEvent e) {
}
};
}
private TreeSelectionModel buildClasspathSelectionModel() {
TreeSelectionModel selectionModel = new DefaultTreeSelectionModel();
selectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
return selectionModel;
}
private Action buildAddEntryAction() {
Action action = new AbstractFrameworkAction(getApplicationContext()) {
protected void initialize() {
setText(resourceRepository().getString("ADD_ENTRY_BUTTON_TEXT"));
setMnemonic(resourceRepository().getMnemonic("ADD_ENTRY_BUTTON_TEXT"));
}
public void actionPerformed(ActionEvent event) {
ClasspathPanel.this.addEntry();
}
};
action.setEnabled(true);
return action;
}
private Action buildBrowseAction() {
Action action = new AbstractFrameworkAction(getApplicationContext()) {
protected void initialize() {
this.initializeTextAndMnemonic("BROWSE_BUTTON_1");
}
public void actionPerformed(ActionEvent event) {
ClasspathPanel.this.promptToAddEntries();
}
};
action.setEnabled(true);
return action;
}
private Action buildRemoveAction() {
Action action = new AbstractFrameworkAction(getApplicationContext()) {
protected void initialize() {
setText(resourceRepository().getString("REMOVE_BUTTON_TEXT"));
setMnemonic(resourceRepository().getMnemonic("REMOVE_BUTTON_TEXT"));
}
public void actionPerformed(ActionEvent event) {
ClasspathPanel.this.removeEntries();
}
};
action.setEnabled(false);
return action;
}
private Action buildUpAction() {
Action action = new AbstractFrameworkAction(getApplicationContext()) {
protected void initialize() {
setText(resourceRepository().getString("UP_BUTTON_TEXT"));
setMnemonic(resourceRepository().getMnemonic("UP_BUTTON_TEXT"));
}
public void actionPerformed(ActionEvent event) {
ClasspathPanel.this.moveSelectedEntriesUp();
}
};
action.setEnabled(false);
return action;
}
private Action buildDownAction() {
Action action = new AbstractFrameworkAction(getApplicationContext()) {
protected void initialize() {
setText(resourceRepository().getString("DOWN_BUTTON_TEXT"));
setMnemonic(resourceRepository().getMnemonic("DOWN_BUTTON_TEXT"));
}
public void actionPerformed(ActionEvent event) {
ClasspathPanel.this.moveSelectedEntriesDown();
}
};
action.setEnabled(false);
return action;
}
protected void initializeLayout(String title) {
GridBagConstraints constraints = new GridBagConstraints();
setBorder(BorderFactory.createCompoundBorder
(
buildTitledBorder(title),
BorderFactory.createEmptyBorder(0, 5, 5, 5)
));
// Commented out until jdk gets off their butt and makes the JList cells editable.
// Replaced temporarily with JTree
/*
JList classpathListBox = new JList();
classpathListBox.setModel(classpathListModel);
classpathListBox.setSelectionModel(classpathListSelectionModel);
classpathListBox.setDoubleBuffered(true);
*/
classpathTree = buildClasspathTree();
constraints.gridx = 0;
constraints.gridy = 1;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.weighty = 1;
constraints.fill = GridBagConstraints.BOTH;
constraints.anchor = GridBagConstraints.CENTER;
constraints.insets = new Insets(1, 0, 0, 0);
JScrollPane scrollPane = new JScrollPane(classpathTree);
scrollPane.setMinimumSize(new Dimension(1, 1));
scrollPane.setPreferredSize(new Dimension(1, 1));
this.add(scrollPane, constraints);
// button panel
JPanel buttonPanel = new AccessibleTitledPanel(new GridLayout(5, 1, 0, 5));
buttonPanel.add(new JButton(addEntryAction));
buttonPanel.add(new JButton(browseAction));
buttonPanel.add(new JButton(removeAction));
buttonPanel.add(new JButton(upAction));
buttonPanel.add(new JButton(downAction));
constraints.gridx = 1;
constraints.gridy = 1;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.fill = GridBagConstraints.NONE;
constraints.anchor = GridBagConstraints.PAGE_START;
constraints.insets = new Insets(1, 5, 0, 0);
this.add(buttonPanel, constraints);
addAlignRight(buttonPanel);
}
private JTree buildClasspathTree() {
JTree classpathTree = new SwingComponentFactory.AccessibleTree(classpathModel) {
public void cancelEditing() {
if (isEditing()) {
TreePath path = getEditingPath();
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
String currentEntry = (String) node.getUserObject();
super.cancelEditing();
if (StringTools.stringIsEmpty(currentEntry)) {
super.cancelEditing();
int rowIndex = getRowForPath(path);
classpathEntriesHolder().removeItems(rowIndex, 1);
}
}
}
};
classpathTree.setSelectionModel(classpathSelectionModel);
classpathTree.addTreeSelectionListener(this.buildClasspathSelectionListener());
DefaultTreeCellRenderer renderer = this.buildTreeCellRenderer();
classpathTree.setCellRenderer(renderer);
classpathTree.setCellEditor(buildTreeCellEditor(classpathTree, renderer));
classpathTree.setRootVisible(false);
classpathTree.expandPath(new TreePath(classpathModel.getRoot()));
classpathTree.setEditable(this.editable); // key feature!
classpathTree.setRowHeight(0);
classpathTree.setExpandsSelectedPaths(true);
classpathTree.setDoubleBuffered(true);
classpathModel.addTreeModelListener(buildTreeModelListener(classpathTree));
return classpathTree;
}
private TreeSelectionListener buildClasspathSelectionListener() {
return new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
ClasspathPanel.this.classpathSelectionChanged();
}
};
}
File getRootFolder() {
return (File) this.rootFolderHolder.getValue();
}
private DefaultTreeCellRenderer buildTreeCellRenderer() {
return new DefaultTreeCellRenderer() {
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean focus) {
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, focus);
if (leaf) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
Icon icon = this.buildIconForFile(new File((String) node.getUserObject()));
this.setLeafIcon(icon);
this.setIcon(icon);
}
return this;
}
private Icon buildIconForFile(File file) {
if ( ! file.isAbsolute()) {
File rootFolder = ClasspathPanel.this.getRootFolder();
if (rootFolder != null) {
file = new File(rootFolder, file.getPath());
}
}
return ClasspathPanel.this.resourceRepository().getIcon(this.buildIconKeyForFile(file));
}
private String buildIconKeyForFile(File file) {
if ( ! file.isAbsolute()) {
// the file can still be relative if the project has not been saved yet
return "folder";
}
if (file.exists()) {
return file.isDirectory() ? "folder" : "file";
}
return "warning";
}
};
}
private TreeCellEditor buildTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
return new DefaultTreeCellEditor(tree, renderer);
}
// ********** HelpTopicID implementation **********
public String getTopicID() {
return "project.classpath";
}
// ********** queries **********
private ListValueModel classpathEntriesHolder() {
return this.classpathEntriesHolder;
}
private int lastEntryIndex() {
return classpathEntriesHolder().size() - 1;
}
// ********** behavior **********
private void classpathSelectionChanged() {
this.enableActions();
}
//TODO problem here, the up and down buttons are enabled even when only one item selected
private void enableActions() {
boolean anyEntrySelected = ! this.classpathSelectionModel.isSelectionEmpty();
boolean firstEntrySelected = this.classpathSelectionModel.getMinSelectionRow() == 0;
boolean lastEntrySelected = this.classpathSelectionModel.getMaxSelectionRow() == this.lastEntryIndex();
removeAction.setEnabled(anyEntrySelected);
upAction.setEnabled(anyEntrySelected && ! firstEntrySelected);
downAction.setEnabled(anyEntrySelected && ! lastEntrySelected);
}
private void removeEntries() {
classpathEntriesHolder().removeItems(classpathSelectionModel.getMinSelectionRow(), classpathSelectionModel.getSelectionCount());
}
private void replaceEntry(int index, Object newEntry) {
if ( ! newEntry.equals("")) {
classpathEntriesHolder().replaceItem(index, newEntry);
}
}
private void moveSelectedEntriesUp() {
// move the entry just before the selection to just after
Object entry = classpathEntriesHolder().removeItem(classpathSelectionModel.getMinSelectionRow() - 1);
classpathEntriesHolder().addItem(classpathSelectionModel.getMaxSelectionRow() + 1, entry);
// since the selection does not change, we need to recalculate the state of the actions
this.enableActions();
}
private void moveSelectedEntriesDown() {
// move the entry just after the selection to just before
Object entry = classpathEntriesHolder().removeItem(classpathSelectionModel.getMaxSelectionRow() + 1);
classpathEntriesHolder().addItem(classpathSelectionModel.getMinSelectionRow(), entry);
// since the selection does not change, we need to recalculate the state of the actions
this.enableActions();
}
private void addEntry() {
if (classpathTree.isEditing()) {
classpathTree.cancelEditing();
}
TreeNode rootNode = (TreeNode) classpathModel.getRoot();
// Get the best row for insertion
int index = rootNode.getChildCount();
int[] selectedRows = classpathTree.getSelectionRows();
if ((selectedRows != null) && (selectedRows.length > 0)) {
index = selectedRows[selectedRows.length - 1];
}
// Add the item to the holder, which will then be added to the tree model
classpathEntriesHolder().addItems(index, Collections.singletonList("")); // Empty entry
// Get the node and start editing it
TreeNode newEntryNode = rootNode.getChildAt(index);
classpathTree.startEditingAtPath(new TreePath(new Object[] { rootNode, newEntryNode }));
}
void promptToAddEntries() {
if (classpathTree.isEditing()) {
classpathTree.cancelEditing();
}
File[] selectedFiles = this.promptToAddFiles();
int len = selectedFiles.length;
if (len == 0) {
return;
}
List selectedFileNames = new ArrayList(len);
for (int i = 0; i < len; i++) {
selectedFileNames.add(selectedFiles[i].getPath());
}
// remove *exact* duplicates
CollectionTools.removeAll(selectedFileNames, (Iterator) classpathEntriesHolder().getValue());
if (selectedFileNames.isEmpty()) {
return;
}
TreeNode rootNode = (TreeNode) this.classpathModel.getRoot();
this.classpathSelectionModel.clearSelection();
int newSelectionStartIndex = this.classpathEntriesHolder().size();
this.classpathEntriesHolder().addItems(newSelectionStartIndex, selectedFileNames);
for (Iterator stream = selectedFileNames.iterator(); stream.hasNext(); ) {
this.classpathSelectionModel.addSelectionPath(this.pathFor(rootNode, stream.next()));
}
// The up/down buttons do not get disabled the first time an entry is
// added to the list. Looks like a listener order problem so I am
// calling enableActions after the selection is complete.
// classpathSelectionChanged() gets called, but the TreeSelectionModel
// and it's underlying ListSelectionModel appear out of synch
this.enableActions();
}
private TreePath pathFor(TreeNode parentNode, Object entry) {
for(Enumeration stream = parentNode.children(); stream.hasMoreElements(); ) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) stream.nextElement();
if (node.getUserObject().equals(entry)) {
return new TreePath(node.getPath());
}
}
throw new IllegalArgumentException("missing entry");
}
private File[] promptToAddFiles() {
JFileChooser fileChooser = this.buildFileChooser();
int selection = fileChooser.showDialog(SwingUtilities.windowForComponent(this), this.resourceRepository().getString("DIALOG.OK_BUTTON_TEXT"));
if (selection == JFileChooser.APPROVE_OPTION) {
this.defaultClasspathDirectoryHolder.setDefaultClasspathDirectory(fileChooser.getCurrentDirectory());
return fileChooser.getSelectedFiles();
}
return new File[0];
}
private JFileChooser buildFileChooser() {
JFileChooser fc = new FileChooser(this.getDefaultClasspathDirectory(), this.getRootFolder());
fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
fc.setDialogTitle(this.resourceRepository().getString("ADD_CLASSPATH_ENTRY_DIALOG.TITLE"));
fc.setMultiSelectionEnabled(true);
fc.setFileFilter(this.buildFileFilter());
return fc;
}
private FileFilter buildFileFilter() {
return new FileFilter() {
public boolean accept(File file) {
String name = file.getName().toLowerCase();
return name.endsWith(".jar")
|| name.endsWith(".zip")
|| file.isDirectory();
}
public String getDescription() {
return resourceRepository().getString(".jar.zip");
}
};
}
private File getDefaultClasspathDirectory() {
return this.defaultClasspathDirectoryHolder.getDefaultClasspathDirectory();
}
public void setDefaultClasspathDirectoryHolder(DefaultClasspathDirectoryHolder defaultClasspathDirectoryHolder) {
this.defaultClasspathDirectoryHolder = defaultClasspathDirectoryHolder;
}
// ********** inner classes **********
/**
*
*/
private class ExpandPathRunner implements Runnable {
private final JTree classpathTree;
private final Object rootNode;
ExpandPathRunner(JTree classpathTree, Object rootNode) {
super();
this.classpathTree = classpathTree;
this.rootNode = rootNode;
}
public void run() {
TreePath path = new TreePath(rootNode);
if (!classpathTree.isExpanded(path)) {
classpathTree.expandPath(path);
}
}
}
/**
* Used by the ClasspathPanel to indirectly reference the default
* classpath directory used to initialize the FileChooser.
*/
public interface DefaultClasspathDirectoryHolder {
/**
* Return the directory to be used as the default in the file
* chooser used by the classpath panel.
*/
File getDefaultClasspathDirectory();
/**
* Set the directory to be used as the default in the file
* chooser used by the classpath panel.
*/
void setDefaultClasspathDirectory(File defaultClasspathDirectory);
DefaultClasspathDirectoryHolder NULL_INSTANCE =
new DefaultClasspathDirectoryHolder() {
public File getDefaultClasspathDirectory() {
return null;
}
public void setDefaultClasspathDirectory(File defaultClasspathDirectory) {
// do nothing
}
public String toString() {
return "NullDefaultClasspathDirectoryHolder";
}
};
}
}