All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.truezip.file.swing.TFileTree Maven / Gradle / Ivy

Go to download

This module provides the TFile* classes for simple, uniform, transparent, thread-safe, read/write access to archive files as if they were virtual directories in a file system path. This module also provides Swing GUI classes for viewing file trees and choosing entries in archive files.

There is a newer version: 7.7.10
Show newest version
/*
 * Copyright (C) 2005-2015 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */
package de.schlichtherle.truezip.file.swing;

import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileComparator;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.awt.Toolkit;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.WillClose;
import javax.swing.CellEditor;
import javax.swing.JTree;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

/**
 * 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
 */
public final class TFileTree extends JTree {

    private static final long serialVersionUID = 1064787562479927601L;

    /** 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();

    private boolean displayingSuffixes = true;

    private boolean editingSuffixes = true;

    private @CheckForNull String defaultSuffix;

    private transient @CheckForNull TFile editedNode;

    /**
     * Creates an empty {@code TFileTree} with no root.
     * You shouldn't use this constructor.
     * It's only provided to implement the JavaBean pattern.
     */
    public TFileTree() {
        this(new TFileTreeModel(null, null, new TFileComparator()));
    }

    /**
     * Creates a new {@code TFileTree} which traverses the given
     * root {@code root} file.
     */
    public TFileTree(TFile root) {
        this(new TFileTreeModel(root, null, new TFileComparator()));
    }

    /**
     * Creates a new {@code TFileTree} which traverses the given
     * {@link TFileTreeModel}.
     */
    public TFileTree(TFileTreeModel model) {
        super(model);
        super.addTreeExpansionListener(controller);
        super.setCellRenderer(new TFileTreeCellRenderer(this));
    }

    @Override
    public TFileTreeModel getModel() {
        return (TFileTreeModel) super.getModel();
    }

    /**
     * {@inheritDoc}
     * 

* @throws ClassCastException if {@code model} is not an instance * of {@link TFileTreeModel}. */ @Override @edu.umd.cs.findbugs.annotations.SuppressWarnings("BC_UNCONFIRMED_CAST") public void setModel(TreeModel model) { if (null == model) throw new NullPointerException(); super.setModel((TFileTreeModel) model); } @Override 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 @Nullable 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(@CheckForNull String defaultSuffix) { final String oldDefaultSuffix = this.defaultSuffix; if (null != defaultSuffix) { 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); } /** Returns the node that is currently edited, if any. */ @Nullable TFile getEditedNode() { return editedNode; } @Override public boolean isEditing() { return null != editedNode; } @Override public void startEditingAtPath(TreePath path) { editedNode = (TFile) path.getLastPathComponent(); super.startEditingAtPath(path); } @Override public void cancelEditing() { editedNode = null; super.cancelEditing(); } @Override 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 member = tce.getCellEditorValue().toString().trim(); final TFile oldNode = (TFile) getLeadSelectionPath().getLastPathComponent(); final TFile parent = oldNode.getParentFile(); assert parent != null; if (!oldNode.isDirectory()) { if (isDisplayingSuffixes() && isEditingSuffixes()) { final String suffix = getSuffix(member); if (null == suffix) { final String defaultSuffix = getDefaultSuffix(); if (defaultSuffix != null) member += defaultSuffix; } } else { final String suffix = getSuffix(oldNode.getName()); if (null != suffix) member += suffix; } } final TFile node = new TFile(parent, member); try { mv(oldNode, node); } catch (IOException ex) { Logger .getLogger(TFileTree.class.getName()) .log(Level.WARNING, ex.toString(), ex); Toolkit.getDefaultToolkit().beep(); } } private @Nullable String getSuffix(final String base) { final int i = base.lastIndexOf('.'); return i != -1 ? base.substring(i) : null; } @Override public String convertValueToText( final Object value, final boolean selected, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { final TFile node = (TFile) value; final TFile editedNode = getEditedNode(); if (node != editedNode && !node.exists()) { // You will see this occur for files which have been deleted // concurrently or which are returned by TFile.listFiles(), but do // not actually TFile.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; } /** * Refreshes the entire tree, * restores the expanded and selected paths and scrolls to the lead * selection path if necessary. */ public void refresh() { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(ftm.getRoot()); if (null != path) 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 TFile node) { if (node == null) throw new NullPointerException(); final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(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 TFileTreeModel ftm = 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((TFile) 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(expansions.nextElement(), true); } /** * Forwards the call to the {@link TFileTreeModel} * 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 #cp(InputStream, TFile)}. * * @throws IOException On any I/O failure. */ public boolean createNewFile(final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) return false; if (!ftm.createNewFile(node)) return false; setSelectionPath(path); scrollPathToVisible(path); return true; } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the newly created directory * is selected and visible. * * @throws IOException On any I/O failure. */ public void mkdir(final TFile node, final boolean recursive) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) return; ftm.mkdir(node, recursive); setSelectionPath(path); scrollPathToVisible(path); } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the copied node * is selected and visible. * * @throws IOException On any I/O failure. */ public void cp(final @WillClose InputStream in, final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); ftm.cp(in, node); setSelectionPath(path); scrollPathToVisible(path); } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the copied node * is selected and visible. * * @throws IOException On any I/O failure. */ public void cp(final TFile oldNode, final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); ftm.cp(oldNode, node); setSelectionPath(path); scrollPathToVisible(path); } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the recursively copied node * is selected and visible. * * @throws IOException On any I/O failure. */ public void cp_r(final TFile oldNode, final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); ftm.cp_r(oldNode, node); setSelectionPath(path); scrollPathToVisible(path); } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the copied node * is selected and visible. * * @throws IOException On any I/O failure. */ public void cp_p(final TFile oldNode, final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); ftm.cp_p(oldNode, node); setSelectionPath(path); scrollPathToVisible(path); } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the recursively copied node * is selected and visible. * * @throws IOException On any I/O failure. */ public void cp_rp(final TFile oldNode, final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); ftm.cp_rp(oldNode, node); setSelectionPath(path); scrollPathToVisible(path); } /** * Forwards the call to the {@link TFileTreeModel}, * restores the expanded paths, selects {@code node} and scrolls to * it if necessary. * * @throws IOException On any I/O failure. */ public void mv(final TFile oldNode, final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); final Enumeration expansions; final TreePath oldPath = ftm.newTreePath(oldNode); expansions = oldPath == null ? null : getExpandedDescendants(oldPath); ftm.mv(oldNode, node); if (null != expansions) while (expansions.hasMoreElements()) setExpandedState( substPath(expansions.nextElement(), oldPath, path), true); setSelectionPath(path); scrollPathToVisible(path); } private TreePath substPath( final TreePath tp, final TreePath oldPath, final TreePath path) { final TFile file = (TFile) tp.getLastPathComponent(); if (file.equals(oldPath.getLastPathComponent())) { return path; } else { final TreePath parent = substPath(tp.getParentPath(), oldPath, path); return parent.pathByAddingChild( new de.schlichtherle.truezip.file.TFile((TFile) parent.getLastPathComponent(), file.getName())); } } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the successor to the deleted node * is selected and visible. * * @throws IOException On any I/O failure. */ public void rm(final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); scrollPathToVisible(path); final int row = getRowForPath(path); ftm.rm(node); setSelectionRow(row); } /** * Forwards the call to the {@link TFileTreeModel} * and scrolls the tree so that the successor to the deleted node * is selected and visible. * * @throws IOException On any I/O failure. */ public void rm_r(final TFile node) throws IOException { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null == path) throw new IllegalArgumentException("node"); scrollPathToVisible(path); final int row = getRowForPath(path); ftm.rm_r(node); setSelectionRow(row); } public void setSelectionNode(final TFile node) { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (null != path) setSelectionPath(path); } public void setSelectionNodes(final TFile[] nodes) { final TFileTreeModel ftm = getModel(); final java.util.List list = new LinkedList(); for (int i = 0, l = nodes.length; i < l; i++) { final TreePath lastPath = ftm.newTreePath(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 TFile node) { final TFileTreeModel ftm = getModel(); final TreePath path = ftm.newTreePath(node); if (path != null) scrollPathToVisible(path); } private final class Controller implements TreeExpansionListener, CellEditorListener, Serializable { private static final long serialVersionUID = 6402557248752695675L; @Override public void treeCollapsed(TreeExpansionEvent evt) { getModel().forget((TFile) evt.getPath().getLastPathComponent()); } @Override public void treeExpanded(TreeExpansionEvent evt) { } @Override public void editingCanceled(ChangeEvent evt) { } @Override public void editingStopped(ChangeEvent evt) { onEditingStopped(evt); } } // class Controller }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy