de.schlichtherle.io.swing.JFileTree Maven / Gradle / Ivy
Show all versions of truezip Show documentation
/*
* Copyright (C) 2006-2010 Schlichtherle IT Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.schlichtherle.io.swing;
import de.schlichtherle.io.swing.tree.*;
import java.awt.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
/**
* A custom {@link JTree} to browse files and directories.
* There are a couple of file creation/modification/removal methods added
* which notify the tree of any changes in the file system and update the
* current path expansions and selection.
*
* @author Christian Schlichtherle
* @version $Id: JFileTree.java,v 1.4 2010/08/20 13:09:46 christian_schlichtherle Exp $
* @since TrueZIP 5.1
*/
public class JFileTree extends JTree {
/** The name of the property {@code displayingSuffixes}. */
private static final String PROPERTY_DISPLAYING_SUFFIXES = "displayingSuffixes"; // NOI18N
/** The name of the property {@code editingSuffixes}. */
private static final String PROPERTY_EDITING_SUFFIXES = "editingSuffixes"; // NOI18N
/** The name of the property {@code defaultSuffix}. */
private static final String PROPERTY_DEFAULT_SUFFIX = "defaultSuffix"; // NOI18N
private final Controller controller = new Controller();
/** Holds value of property displayingSuffixes. */
private boolean displayingSuffixes = true;
/** Holds value of property editingSuffixes. */
private boolean editingSuffixes = true;
/** Holds value of property defaultSuffix. */
private String defaultSuffix;
/** Holds value of property editedNode. */
private java.io.File editedNode;
/**
* Creates an empty {@code JFileTree} with no root.
* You shouldn't use this constructor.
* It's only provided to implement the JavaBean pattern.
*/
public JFileTree() {
this(new FileTreeModel());
}
/**
* Creates a new {@code JFileTree} which traverses the given
* root {@code File}.
* The ZipDetector of the given file is used to detect and configure any
* ZIP compatible files in this directory tree.
*
* @see de.schlichtherle.io.File#getDefaultArchiveDetector()
* @see de.schlichtherle.io.File#setDefaultArchiveDetector(ArchiveDetector)
*/
public JFileTree(java.io.File root) {
this(new FileTreeModel(root));
}
/**
* Creates a new {@code JFileTree} which traverses the given
* {@link FileTreeModel}.
*/
public JFileTree(FileTreeModel model) {
super(model);
super.setCellRenderer(createTreeCellRenderer());
super.addTreeExpansionListener(controller);
}
/**
* Returns a new tree cell renderer.
* This method may be overridden by subclasses.
*
* This implementation simply returns a new {@link FileTreeCellRenderer}.
*/
protected TreeCellRenderer createTreeCellRenderer() {
return new FileTreeCellRenderer(this);
}
//
// Properties.
//
/**
* @throws ClassCastException If {@code model} is not an instance
* of {@link FileTreeModel}.
*/
public void setModel(TreeModel model) {
super.setModel((FileTreeModel) model);
}
public void setEditable(final boolean editable) {
if (editable) {
super.setEditable(true);
getCellEditor().addCellEditorListener(controller);
} else {
final CellEditor ce = getCellEditor();
if (ce != null)
ce.removeCellEditorListener(controller);
super.setEditable(false);
}
}
/**
* Getter for bound property displayingSuffixes.
*
* @return Value of property displayingSuffixes.
*/
public boolean isDisplayingSuffixes() {
return this.displayingSuffixes;
}
/**
* Setter for bound property displayingSuffixes.
* If this is {@code false}, the suffix of files will not be displayed
* in this tree.
* Defaults to {@code true}.
*
* @param displayingSuffixes New value of property displayingSuffixes.
*/
public void setDisplayingSuffixes(boolean displayingSuffixes) {
boolean oldDisplayingSuffixes = this.displayingSuffixes;
this.displayingSuffixes = displayingSuffixes;
firePropertyChange(PROPERTY_DISPLAYING_SUFFIXES,
oldDisplayingSuffixes, displayingSuffixes);
}
/**
* Getter for bound property editingSuffixes.
*
* @return Value of property editingSuffixes.
*/
public boolean isEditingSuffixes() {
return this.editingSuffixes;
}
/**
* Setter for bound property editingSuffixes.
* If this is {@code false}, the suffix of a file will be truncated
* before editing its name starts.
* Defaults to {@code true}.
*
* @param editingSuffixes New value of property editingSuffixes.
*/
public void setEditingSuffixes(boolean editingSuffixes) {
boolean oldEditingSuffixes = this.editingSuffixes;
this.editingSuffixes = editingSuffixes;
firePropertyChange(PROPERTY_EDITING_SUFFIXES,
oldEditingSuffixes, editingSuffixes);
}
/**
* Getter for bound property defaultSuffix.
*
* @return Value of property defaultSuffix.
*/
public String getDefaultSuffix() {
return this.defaultSuffix;
}
/**
* Setter for bound property defaultSuffixes.
* Sets the default suffix to use when suffixes are shown and allowed to
* be edited, but the user did not provide a suffix when editing a file
* name.
* This property defaults to {@code null} and is ignored for
* directories.
*
* @param defaultSuffix The new default suffix.
* If not {@code null}, this parameter is fixed to always
* start with a {@code '.'}.
*/
public void setDefaultSuffix(String defaultSuffix) {
final String oldDefaultSuffix = this.defaultSuffix;
if (defaultSuffix != null) {
defaultSuffix = defaultSuffix.trim();
if (defaultSuffix.length() <= 0)
defaultSuffix = null;
else if (defaultSuffix.charAt(0) != '.')
defaultSuffix = "." + defaultSuffix;
}
this.defaultSuffix = defaultSuffix;
firePropertyChange(PROPERTY_DEFAULT_SUFFIX,
oldDefaultSuffix, defaultSuffix);
}
//
// Editing.
//
/**
* Returns the node that is currently edited, if any.
* This method is not intended for public use - do not use it!
*/
public java.io.File getEditedNode() {
return editedNode;
}
public boolean isEditing() {
return editedNode != null;
}
public void startEditingAtPath(TreePath path) {
editedNode = (java.io.File) path.getLastPathComponent();
super.startEditingAtPath(path);
}
public void cancelEditing() {
editedNode = null;
super.cancelEditing();
}
public boolean stopEditing() {
final boolean stop = super.stopEditing();
if (stop)
editedNode = null;
return stop;
}
/**
* Called when the editing of a cell has been stopped.
* The implementation in this class will rename the edited file,
* obeying the rules for suffix handling and updating the expanded and
* selected paths accordingly.
*
* @param evt The change event passed to
* {@link CellEditorListener#editingStopped(ChangeEvent)}.
*/
protected void onEditingStopped(final ChangeEvent evt) {
final TreeCellEditor tce = (TreeCellEditor) evt.getSource();
String base = tce.getCellEditorValue().toString().trim();
final java.io.File oldNode
= (java.io.File) getLeadSelectionPath().getLastPathComponent();
final java.io.File parent = oldNode.getParentFile();
assert parent != null;
if (!oldNode.isDirectory()) {
if (isDisplayingSuffixes() && isEditingSuffixes()) {
final String suffix = getSuffix(base);
if (suffix == null) {
final String defaultSuffix = getDefaultSuffix();
if (defaultSuffix != null)
base += defaultSuffix;
}
} else {
final String suffix = getSuffix(oldNode.getName());
if (suffix != null)
base += suffix;
}
}
final java.io.File node = new de.schlichtherle.io.File(parent, base);
if (!renameTo(oldNode, node))
Toolkit.getDefaultToolkit().beep();
}
private String getSuffix(final String base) {
final int i = base.lastIndexOf('.');
return i != -1 ? base.substring(i) : null;
}
//
// Rendering:
//
public String convertValueToText(
final Object value,
final boolean selected,
final boolean expanded,
final boolean leaf,
final int row,
final boolean hasFocus) {
final java.io.File node = (java.io.File) value;
final java.io.File editedNode = getEditedNode();
if (node != editedNode && !node.exists()) {
// You will see this occur for files which have been deleted
// concurrently or which are returned by File.listFiles(), but do
// not actually File.exists(), such as "C:\hiberfile.sys" on the
// Windows platform.
return "?"; // NOI18N
}
final String base = node.getName();
if (base.length() <= 0)
return node.getPath(); // This is a file system root.
if (node.isDirectory() ||
isDisplayingSuffixes()
&& (!node.equals(editedNode) || isEditingSuffixes()))
return base;
final int i = base.lastIndexOf('.');
return i != -1 ? base.substring(0, i) : base;
}
//
// Refreshing:
//
/**
* Refreshes the entire tree,
* restores the expanded and selected paths and scrolls to the lead
* selection path if necessary.
*/
public void refresh() {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath((java.io.File) ftm.getRoot());
if (path != null)
refresh(new TreePath[] { path });
}
/**
* Refreshes the subtree for the given node,
* restores the expanded and selected paths and scrolls to the lead
* selection path if necessary.
*
* @param node The file or directory to refresh.
* This may not be {@code null}.
*
*/
public void refresh(final java.io.File node) {
if (node == null)
throw new NullPointerException();
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path != null)
refresh(new TreePath[] { path });
}
/**
* Refreshes the subtree for the given paths,
* restores the expanded and selected paths and scrolls to the lead
* selection path if necessary.
*
* @param paths The array of {@code TreePath}s to refresh.
* This may be {@code null}.
*/
public void refresh(final TreePath paths[]) {
if (paths == null || paths.length <= 0)
return;
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath lead = getLeadSelectionPath();
final TreePath anchor = getAnchorSelectionPath();
final TreePath[] selections = getSelectionPaths();
for (int i = 0, l = paths.length; i < l; i++) {
final TreePath path = paths[i];
final Enumeration expansions = getExpandedDescendants(path);
ftm.refresh((java.io.File) path.getLastPathComponent());
setExpandedDescendants(expansions);
}
setSelectionPaths(selections);
setAnchorSelectionPath(anchor);
setLeadSelectionPath(lead);
scrollPathToVisible(lead);
}
private void setExpandedDescendants(final Enumeration expansions) {
if (expansions == null)
return;
while (expansions.hasMoreElements())
setExpandedState((TreePath) expansions.nextElement(), true);
}
//
// File operations:
//
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the newly created file
* is selected and visible.
* If you would like to create a new file with initial content, please
* check {@link #copyFrom(de.schlichtherle.io.File, InputStream)}.
*/
public boolean createNewFile(final java.io.File node) throws IOException {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.createNewFile(node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the newly created directory
* is selected and visible.
*/
public boolean mkdir(final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.mkdir(node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the newly created directory
* is selected and visible.
*/
public boolean mkdirs(final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.mkdirs(node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the copied node
* is selected and visible.
*/
public boolean copyFrom(final de.schlichtherle.io.File node, final InputStream in) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.copyFrom(node, in))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the copied node
* is selected and visible.
*/
public boolean copyTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.copyTo(oldNode, node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the recursively copied node
* is selected and visible.
*/
public boolean copyAllTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.copyAllTo(oldNode, node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the copied node
* is selected and visible.
*/
public boolean archiveCopyTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.archiveCopyTo(oldNode, node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the recursively copied node
* is selected and visible.
*/
public boolean archiveCopyAllTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
if (!ftm.archiveCopyAllTo(oldNode, node))
return false;
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel},
* restores the expanded paths, selects {@code node} and scrolls to
* it if necessary.
*/
public boolean renameTo(final java.io.File oldNode, final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
final Enumeration expansions;
final TreePath oldPath = ftm.createTreePath(oldNode);
if (oldPath != null)
expansions = getExpandedDescendants(oldPath);
else
expansions = null;
if (!ftm.renameTo(oldNode, node))
return false;
if (expansions != null)
while (expansions.hasMoreElements())
setExpandedState(
substPath((TreePath) expansions.nextElement(),
oldPath, path),
true);
setSelectionPath(path);
scrollPathToVisible(path);
return true;
}
private TreePath substPath(
final TreePath tp,
final TreePath oldPath,
final TreePath path) {
final java.io.File file = (java.io.File) tp.getLastPathComponent();
if (file.equals(oldPath.getLastPathComponent())) {
return path;
} else {
final TreePath parent = substPath(tp.getParentPath(), oldPath, path);
return parent.pathByAddingChild(
new de.schlichtherle.io.File((java.io.File) parent.getLastPathComponent(),
file.getName()));
}
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the successor to the deleted node
* is selected and visible.
*/
public boolean delete(final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
scrollPathToVisible(path);
final int row = getRowForPath(path);
if (!ftm.delete(node))
return false;
setSelectionRow(row);
return true;
}
/**
* Forwards the call to the {@link FileTreeModel}
* and scrolls the tree so that the successor to the deleted node
* is selected and visible.
*/
public boolean deleteAll(final de.schlichtherle.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path == null)
return false;
scrollPathToVisible(path);
final int row = getRowForPath(path);
if (!ftm.deleteAll(node))
return false;
setSelectionRow(row);
return true;
}
public void setSelectionNode(final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path != null) {
setSelectionPath(path);
}
}
public void setSelectionNodes(final java.io.File[] nodes) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final java.util.List list = new LinkedList();
TreePath lastPath = null;
for (int i = 0, l = nodes.length; i < l; i++) {
lastPath = ftm.createTreePath(nodes[i]);
if (lastPath != null)
list.add(lastPath);
}
final int size = list.size();
if (size > 0) {
final TreePath[] paths = new TreePath[size];
list.toArray(paths);
setSelectionPaths(paths);
}
}
public void scrollNodeToVisible(final java.io.File node) {
final FileTreeModel ftm = (FileTreeModel) getModel();
final TreePath path = ftm.createTreePath(node);
if (path != null)
scrollPathToVisible(path);
}
//
// Member classes:
//
private class Controller
implements TreeExpansionListener, CellEditorListener, Serializable {
public void treeCollapsed(TreeExpansionEvent evt) {
((FileTreeModel) getModel()).forget(
(java.io.File) evt.getPath().getLastPathComponent());
}
public void treeExpanded(TreeExpansionEvent evt) {
}
public void editingCanceled(ChangeEvent evt) {
}
public void editingStopped(ChangeEvent evt) {
onEditingStopped(evt);
}
}
}