/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander 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 program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.main.tree;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.filter.FileFilter;
import com.mucommander.commons.file.util.FileComparator;
import com.mucommander.ui.icon.FileIcons;
import com.mucommander.ui.icon.SpinningDial;
import javax.swing.*;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.util.Arrays;
/**
* A tree model for files.
* This class contains a tree structure defined by AbstractFile objects.
* @author Mariusz Jakubowski
*
*/
public class FilesTreeModel implements TreeModel, CachedDirectoryListener {
private DirectoryCache cache;
/** Comparator used to sort folders */
private FileComparator sort;
/** Listeners. */
protected EventListenerList listenerList = new EventListenerList();
/** Root of the directory tree. */
private AbstractFile root;
/** number of caching children at the time, used to control spinning icon */
private int cachingNum = 0;
/** icon used to show that a children of a directory are being cached */
private SpinningDial spinningIcon = new SpinningDial(16, 16, false);
public FilesTreeModel(FileFilter filter, FileComparator sort) {
super();
this.sort = sort;
cache = new DirectoryCache(filter, sort);
cache.addCachedDirectoryListener(this);
}
/**
* Changes the current root of a tree
* Fires 'tree structure changed' event.
* @param newRoot the new root of a tree
*/
public void setRoot(AbstractFile newRoot) {
final CachedDirectory cachedRoot = new CachedDirectory(newRoot, cache);
cachedRoot.setCachedIcon(FileIcons.getFileIcon(newRoot));
SwingUtilities.invokeLater(new Runnable() {
public void run() {
root = cachedRoot.getProxiedFile();
cache.clear();
cache.put(root, cachedRoot);
TreePath path = new TreePath(root);
fireTreeStructureChanged(this, path);
}
});
}
public Object getRoot() {
return root;
}
/**
* Returns children folders of a parent folder sorted by name.
* @param parent parent folder
* @return children folders
*/
private AbstractFile[] getChildren(AbstractFile parent) {
CachedDirectory cachedDir = cache.getOrAdd(parent);
if (cachedDir.isCached())
return cachedDir.get();
else
return null;
}
public Object getChild(Object parent, int index) {
AbstractFile[] children = getChildren((AbstractFile) parent);
if (children != null) {
return children[index];
}
return null;
}
public int getChildCount(Object parent) {
AbstractFile[] children = getChildren((AbstractFile) parent);
if (children != null) {
return children.length;
}
return 0;
}
public int getIndexOfChild(Object parent, Object child) {
AbstractFile[] children = getChildren((AbstractFile) parent);
if (children != null) {
return Arrays.binarySearch(children, (AbstractFile)child, sort);
}
return 0;
}
public boolean isLeaf(Object node) {
return false;
}
public void valueForPathChanged(TreePath path, Object newValue) {
}
/**
* Notifies all listeners that have registered interest for notification on
* this event type.
* @param source the node where the tree model has changed
* @param path the path to the root node
* @see EventListenerList
*/
void fireTreeStructureChanged(Object source, TreePath path) {
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
TreeModelEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TreeModelListener.class) {
// Lazily create the event:
if (e == null) {
e = new TreeModelEvent(source, path);
}
((TreeModelListener) listeners[i + 1]).treeStructureChanged(e);
}
}
}
/**
* Builds the parents of node up to and including the root node,
* where the original node is the last element in the returned array.
* The length of the returned array gives the node's depth in the
* tree.
*
* @param aNode the TreeNode to get the path for
*/
public AbstractFile[] getPathToRoot(AbstractFile aNode) {
return getPathToRoot(aNode, 0);
}
/**
* Builds the parents of node up to and including the root node,
* where the original node is the last element in the returned array.
* The length of the returned array gives the node's depth in the
* tree.
*
* @param aNode the TreeNode to get the path for
* @param depth an int giving the number of steps already taken towards
* the root (on recursive calls), used to size the returned array
* @return an array of TreeNodes giving the path from the root to the
* specified node
*/
protected AbstractFile[] getPathToRoot(AbstractFile aNode, int depth) {
AbstractFile[] retNodes;
// This method recurses, traversing towards the root in order
// size the array. On the way back, it fills in the nodes,
// starting from the root and working back to the original node.
/* Check for null, in case someone passed in a null node, or
they passed in an element that isn't rooted at root. */
if(aNode == null) {
if(depth == 0)
return null;
else
retNodes = new AbstractFile[depth];
}
else {
depth++;
if(aNode == root) {
retNodes = new AbstractFile[depth];
} else {
retNodes = getPathToRoot(aNode.getParent(), depth);
}
retNodes[retNodes.length - depth] = aNode;
cache.getOrAdd(aNode).isCached(); // ensures that a path is in cache
}
return retNodes;
}
public void addTreeModelListener(TreeModelListener l) {
listenerList.add(TreeModelListener.class, l);
}
public void removeTreeModelListener(TreeModelListener l) {
listenerList.remove(TreeModelListener.class, l);
}
/**
* Refreshes tree model from given path.
* @param path a path to refresh
*/
public void refresh(TreePath path) {
AbstractFile folder = (AbstractFile) path.getLastPathComponent();
CachedDirectory cached = cache.get(folder);
Icon cachedIcon = cached.getCachedIcon();
cache.removeWithChildren(folder);
cached = cache.getOrAdd(folder);
cached.setCachedIcon(cachedIcon);
fireTreeStructureChanged(this, path);
}
public void cachingStarted(AbstractFile parent) {
cachingNum++;
if (cachingNum == 1) {
spinningIcon.setAnimated(true);
}
}
public void cachingEnded(AbstractFile parent) {
cachingNum--;
if (cachingNum == 0) {
spinningIcon.setAnimated(false);
}
TreePath path = new TreePath(getPathToRoot(parent));
fireTreeStructureChanged(this, path);
}
/**
* Returns an icon of this directory or spinning icon if this directory is
* being cached.
* @return an icon of this directory or spinning icon if this directory is
* being cached.
*/
public Icon getCurrentIcon(AbstractFile file) {
CachedDirectory cached = cache.get(file);
if (cached != null) {
if (cached.isReadingChildren()) {
return spinningIcon;
}
return cached.getCachedIcon();
}
return spinningIcon;
}
}