/*
* @(#)SidebarListModel.java 3.0.3 2008-04-17
*
* Copyright (c) 2004-2010 Werner Randelshofer, Immensee, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package ch.randelshofer.quaqua.panther.filechooser;
import ch.randelshofer.quaqua.osx.OSXFile;
import ch.randelshofer.quaqua.filechooser.*;
import ch.randelshofer.quaqua.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.io.*;
import java.util.*;
import ch.randelshofer.quaqua.*;
import ch.randelshofer.quaqua.ext.base64.*;
import ch.randelshofer.quaqua.ext.nanoxml.*;
/**
* This is the list model used to display a sidebar in the PantherFileChooserUI.
* The list consists of two parts: the system items and the user items.
* The user items are read from the file "~/Library/Preferences/com.apple.sidebarlists.plist".
* The system items is the contents of the "/Volumes" directory plus the
* "/Networks" directory.
* <p>
* Each element of the SidebarListModel implements the interface FileInfo.
*
*
* @author Werner Randelshofer
* @version $Id: SidebarListModel.java 362 2010-11-21 17:35:47Z wrandelshofer $
*/
public class SidebarListModel
extends AbstractListModel
implements TreeModelListener {
private final static boolean DEBUG = false;
/**
* This file contains information about the system list and holds the aliases
* for the user list.
*/
private final static File sidebarFile = new File(QuaquaManager.getProperty("user.home"), "Library/Preferences/com.apple.sidebarlists.plist");
/**
* Holds the tree path to the /Volumes folder.
*/
private TreePath path;
/**
* Holds the FileSystemTreeModel.
*/
private TreeModel model;
/**
* The JFileChooser.
*/
private JFileChooser fileChooser;
/**
* Sequential dispatcher for the lazy creation of icons.
*/
private SequentialDispatcher dispatcher = new SequentialDispatcher();
/**
* Set this to true, if the computer shall be listed in the sidebar.
*/
private boolean isComputerVisible;
private final static File[] defaultUserItems;
static {
if (QuaquaManager.getProperty("os.name").equals("Mac OS X")) {
defaultUserItems = new File[]{
null, // null is used to specify a divider
new File(QuaquaManager.getProperty("user.home"), "Desktop"),
new File(QuaquaManager.getProperty("user.home"), "Documents"),
new File(QuaquaManager.getProperty("user.home"))
};
} else if (QuaquaManager.getProperty("os.name").startsWith("Windows")) {
defaultUserItems = new File[]{
null, // null is used to specify a divider
new File(QuaquaManager.getProperty("user.home"), "Desktop"),
// Japanese ideographs for Desktop:
new File(QuaquaManager.getProperty("user.home"), "\u684c\u9762"),
new File(QuaquaManager.getProperty("user.home"), "My Documents"),
new File(QuaquaManager.getProperty("user.home"))
};
} else {
defaultUserItems = new File[]{
null, // null is used to specify a divider
new File(QuaquaManager.getProperty("user.home"))
};
}
}
/**
* This array list holds the user items.
*/
private ArrayList userItems = new ArrayList();
/**
* This array holds the view to model mapping of the system items.
*/
private Row[] viewToModel = new Row[0];
/**
* This array holds the model to view mapping of the system items.
*/
private int[] modelToView = new int[0];
/**
* This hash map is used to determine the sequence and visibility of the
* items in the system list.
* HashMap<String,SystemItemInfo>
*/
private HashMap systemItemsMap = new HashMap();
private static class SystemItemInfo {
String name = "";
int sequenceNumber = 0;
boolean isVisible = true;
}
/**
* Intervals between validations.
*/
private final static long VALIDATION_TTL = 60000;
/**
* Time for next validation of the model.
*/
private long bestBefore;
/** Creates a new instance. */
public SidebarListModel(JFileChooser fileChooser, TreePath path, TreeModel model) {
this.fileChooser = fileChooser;
this.path = path;
this.model = model;
model.addTreeModelListener(this);
sortSystemItems();
validate();
}
public void dispose() {
model.removeTreeModelListener(this);
}
public int getSize() {
return (isComputerVisible)
? 1 + viewToModel.length + userItems.size()
: viewToModel.length + userItems.size();
}
private void sortSystemItems() {
FileSystemTreeModel.Node parent = (FileSystemTreeModel.Node) path.getLastPathComponent();
if (modelToView.length != parent.getChildCount()) {
viewToModel = new Row[parent.getChildCount()];
modelToView = new int[viewToModel.length];
}
for (int i = 0; i < viewToModel.length; i++) {
viewToModel[i] = new Row(i);
}
Arrays.sort(viewToModel);
for (int i = 0; i < viewToModel.length; i++) {
modelToView[viewToModel[i].modelIndex] = i;
}
// remove leaf nodes from system items
int j = 0;
for (int i = 0; i < viewToModel.length; i++) {
FileSystemTreeModel.Node node = (FileSystemTreeModel.Node) parent.getChildAt(viewToModel[i].modelIndex);
if (!node.isLeaf()) {
viewToModel[j] = viewToModel[i];
modelToView[viewToModel[j].modelIndex] = i;
j++;
}
}
if (j < viewToModel.length) {
Row[] helper = new Row[j];
System.arraycopy(viewToModel, 0, helper, 0, j);
viewToModel = helper;
}
}
public Object getElementAt(int row) {
if (isComputerVisible) {
if (row == 0) {
return path.getPathComponent(0);
} else if (row <= viewToModel.length) {
return ((FileSystemTreeModel.Node) model.getChild(path.getLastPathComponent(), viewToModel[row - 1].modelIndex));
} else {
return userItems.get(row - viewToModel.length - 1);
}
} else {
return (row < viewToModel.length)
? ((FileSystemTreeModel.Node) model.getChild(path.getLastPathComponent(), viewToModel[row].modelIndex))
: userItems.get(row - viewToModel.length);
}
}
public void treeNodesChanged(TreeModelEvent e) {
if (e.getTreePath().equals(path)) {
int[] indices = e.getChildIndices();
fireContentsChanged(this, modelToView[indices[0]], modelToView[indices[indices.length - 1]]);
}
}
public void treeNodesInserted(TreeModelEvent e) {
if (e.getTreePath().equals(path)) {
sortSystemItems();
int[] indices = e.getChildIndices();
for (int i = 0; i < indices.length; i++) {
int index = modelToView[indices[i]];
fireIntervalAdded(this, index, index);
}
}
}
public void treeNodesRemoved(TreeModelEvent e) {
if (e.getTreePath().equals(path)) {
int[] indices = e.getChildIndices();
int[] oldModelToView = (int[]) modelToView.clone();
sortSystemItems();
for (int i = 0; i < indices.length; i++) {
int index = oldModelToView[indices[i]];
int offset = 0;
for (int j = 0; j < i; j++) {
if (oldModelToView[indices[i]] < index) {
offset++;
}
}
fireIntervalRemoved(this, index - offset, index - offset);
}
}
}
public void treeStructureChanged(TreeModelEvent e) {
if (e.getTreePath().equals(path)) {
sortSystemItems();
fireContentsChanged(this, 0, getSize() - 1);
}
}
private class FileItem implements FileInfo {
private File file;
private Icon icon;
private String userName;
private boolean isTraversable;
/**
* Holds a Finder label for the file represented by this node.
* The label is a value in the interval from 0 through 7.
* The value -1 is used, if the label could not be determined.
*/
protected int fileLabel = -1;
public FileItem(File file) {
this.file = file;
userName = fileChooser.getName(file);
isTraversable = true;
//isTraversable = file.isDirectory();
}
public File lazyGetResolvedFile() {
return file;
}
public File getResolvedFile() {
return file;
}
public File getFile() {
return file;
}
public String getFileKind() {
return null;
}
public int getFileLabel() {
return -1;
}
public long getFileLength() {
return -1;
}
public Icon getIcon() {
if (icon == null) {
icon = (isTraversable())
? UIManager.getIcon("FileView.directoryIcon")
: UIManager.getIcon("FileView.fileIcon");
//
if (!UIManager.getBoolean("FileChooser.speed")) {
dispatcher.dispatch(new Worker<Icon>() {
public Icon construct() {
return fileChooser.getIcon(file);
}
@Override
public void done(Icon value) {
icon = value;
SidebarListModel.this.fireContentsChanged(SidebarListModel.this, 0, SidebarListModel.this.getSize() - 1);
}
});
}
}
return icon;
}
public String getUserName() {
/*
if (userName == null) {
userName = fileChooser.getName(file);
}*/
return userName;
}
public boolean isTraversable() {
return isTraversable;
}
public boolean isAcceptable() {
return true;
}
public boolean isValidating() {
return false;
}
public boolean isHidden() {
return false;
}
}
/**
* An AliasItem is resolved as late as possible.
*/
private class AliasItem implements FileInfo {
private byte[] serializedAlias;
private File file;
private Icon icon;
private String userName;
private String aliasName;
private boolean isTraversable;
/**
* Holds a Finder label for the file represented by this node.
* The label is a value in the interval from 0 through 7.
* The value -1 is used, if the label could not be determined.
*/
protected int fileLabel = -1;
public AliasItem(byte[] serializedAlias, String aliasName) {
this.file = null;
this.aliasName = aliasName;
this.serializedAlias = serializedAlias;
isTraversable = true;
}
public File lazyGetResolvedFile() {
return getResolvedFile();
}
public File getResolvedFile() {
if (file == null) {
icon = null; // clear cached icon!
file = OSXFile.resolveAlias(serializedAlias, false);
}
return file;
}
public File getFile() {
return file;
}
public String getFileKind() {
return null;
}
public int getFileLabel() {
return -1;
}
public long getFileLength() {
return -1;
}
public Icon getIcon() {
if (icon == null) {
icon = (isTraversable())
? UIManager.getIcon("FileView.directoryIcon")
: UIManager.getIcon("FileView.fileIcon");
//
if (file != null && !UIManager.getBoolean("FileChooser.speed")) {
dispatcher.dispatch(new Worker<Icon>() {
public Icon construct() {
return fileChooser.getIcon(file);
}
@Override
public void done(Icon value) {
icon = value;
SidebarListModel.this.fireContentsChanged(SidebarListModel.this, 0, SidebarListModel.this.getSize() - 1);
}
});
}
}
return icon;
}
public String getUserName() {
if (userName == null) {
if (file != null) {
userName = fileChooser.getName(file);
}
}
return (userName == null) ? aliasName : userName;
}
public boolean isTraversable() {
return isTraversable;
}
public boolean isAcceptable() {
return true;
}
public boolean isValidating() {
return false;
}
public boolean isHidden() {
return false;
}
}
/**
* Validates the model if needed.
*/
public void lazyValidate() {
if (bestBefore < System.currentTimeMillis()) {
validate();
}
}
/**
* Immediately validates the model.
*/
private void validate() {
// Prevent multiple invocations of this method by lazyValidate(),
// while we are validating;
bestBefore = Long.MAX_VALUE;
dispatcher.dispatch(
new Worker<Object[]>() {
public Object[] construct() throws IOException {
return read();
}
@Override
public void done(Object[] value) {
ArrayList freshUserItems;
systemItemsMap = (HashMap) value[0];
freshUserItems = (ArrayList) value[1];
freshUserItems.add(0, null);
update(freshUserItems);
}
@Override
public void failed(Throwable value) {
ArrayList freshUserItems;
freshUserItems = new ArrayList(defaultUserItems.length);
for (int i = 0; i < defaultUserItems.length; i++) {
if (defaultUserItems[i] == null) {
freshUserItems.add(null);
} else if (defaultUserItems[i].exists()) {
freshUserItems.add(new FileItem(defaultUserItems[i]));
}
}
update(freshUserItems);
}
private void update(ArrayList freshUserItems) {
int systemItemsSize = model.getChildCount(path.getLastPathComponent());
int oldUserItemsSize = userItems.size();
userItems.clear();
if (oldUserItemsSize > 0) {
fireIntervalRemoved(
SidebarListModel.this,
systemItemsSize,
systemItemsSize + oldUserItemsSize - 1);
}
userItems = freshUserItems;
if (userItems.size() > 0) {
if (DEBUG) {
System.out.println("SidebarListModel.fireIntervalAdded " + systemItemsSize + ".." + (systemItemsSize + +userItems.size() - 1) + ", list size=" + getSize());
}
fireIntervalAdded(
SidebarListModel.this,
systemItemsSize,
systemItemsSize + userItems.size() - 1);
}
bestBefore = System.currentTimeMillis() + VALIDATION_TTL;
}
});
}
/**
* Reads the sidebar preferences file.
*/
private Object[] read() throws IOException {
if (!OSXFile.canWorkWithAliases()) {
throw new IOException("Unable to work with aliases");
}
HashMap sysItemsMap = new HashMap();
ArrayList usrItems = new ArrayList();
FileReader reader = null;
try {
reader = new FileReader(sidebarFile);
XMLElement xml = new XMLElement(new HashMap(), false, false);
try {
xml.parseFromReader(reader);
} catch (XMLParseException e) {
xml = new BinaryPListParser().parse(sidebarFile);
}
String key2 = "", key3 = "", key5 = "";
for (Iterator i0 = xml.iterateChildren(); i0.hasNext();) {
XMLElement xml1 = (XMLElement) i0.next();
for (Iterator i1 = xml1.iterateChildren(); i1.hasNext();) {
XMLElement xml2 = (XMLElement) i1.next();
if (xml2.getName().equals("key")) {
key2 = xml2.getContent();
}
if (xml2.getName().equals("dict") && key2.equals("systemitems")) {
for (Iterator i2 = xml2.iterateChildren(); i2.hasNext();) {
XMLElement xml3 = (XMLElement) i2.next();
if (xml3.getName().equals("key")) {
key3 = xml3.getContent();
}
if (xml3.getName().equals("array") && key3.equals("VolumesList")) {
for (Iterator i3 = xml3.iterateChildren(); i3.hasNext();) {
XMLElement xml4 = (XMLElement) i3.next();
if (xml4.getName().equals("dict")) {
SystemItemInfo info = new SystemItemInfo();
for (Iterator i4 = xml4.iterateChildren(); i4.hasNext();) {
XMLElement xml5 = (XMLElement) i4.next();
if (xml5.getName().equals("key")) {
key5 = xml5.getContent();
}
info.sequenceNumber = sysItemsMap.size();
if (xml5.getName().equals("string") && key5.equals("Name")) {
info.name = xml5.getContent();
}
if (xml5.getName().equals("string") && key5.equals("Visibility")) {
info.isVisible = xml5.getContent().equals("AlwaysVisible");
}
}
if (info.name != null) {
sysItemsMap.put(info.name, info);
}
}
}
}
}
}
if (xml2.getName().equals("dict") && key2.equals("useritems")) {
for (Iterator i2 = xml2.iterateChildren(); i2.hasNext();) {
XMLElement xml3 = (XMLElement) i2.next();
for (Iterator i3 = xml3.iterateChildren(); i3.hasNext();) {
XMLElement xml4 = (XMLElement) i3.next();
String aliasName = null;
byte[] serializedAlias = null;
for (Iterator i4 = xml4.iterateChildren(); i4.hasNext();) {
XMLElement xml5 = (XMLElement) i4.next();
if (xml5.getName().equals("key")) {
key5 = xml5.getContent();
}
if (xml5.getName().equals("string") && key5.equals("Name")) {
aliasName = xml5.getContent();
}
if (!xml5.getName().equals("key") && key5.equals("Alias")) {
serializedAlias = Base64.decode(xml5.getContent());
}
}
if (serializedAlias != null && aliasName != null) {
// Try to resolve the alias without user interaction
File f = OSXFile.resolveAlias(serializedAlias, true);
if (f != null) {
usrItems.add(new FileItem(f));
} else {
usrItems.add(new AliasItem(serializedAlias, aliasName));
}
}
}
}
}
}
}
} finally {
if (reader != null) {
reader.close();
}
}
return new Object[]{sysItemsMap, usrItems};
}
// Helper classes
private class Row implements Comparable {
private int modelIndex;
public Row(int index) {
this.modelIndex = index;
}
public int compareTo(Object o) {
int row1 = modelIndex;
int row2 = ((Row) o).modelIndex;
FileSystemTreeModel.Node o1 = ((FileSystemTreeModel.Node) model.getChild(path.getLastPathComponent(), row1));
FileSystemTreeModel.Node o2 = ((FileSystemTreeModel.Node) model.getChild(path.getLastPathComponent(), row2));
SystemItemInfo i1 = (SystemItemInfo) systemItemsMap.get(o1.getUserName());
if (i1 == null && o1.getResolvedFile().getName().equals("")) {
i1 = (SystemItemInfo) systemItemsMap.get("Computer");
}
SystemItemInfo i2 = (SystemItemInfo) systemItemsMap.get(o2.getUserName());
if (i2 == null && o2.getResolvedFile().getName().equals("")) {
i2 = (SystemItemInfo) systemItemsMap.get("Computer");
}
if (i1 != null && i2 != null) {
return i1.sequenceNumber - i2.sequenceNumber;
}
if (i1 != null) {
return -1;
}
if (i2 != null) {
return 1;
}
return row1 - row2;
}
@Override
public int hashCode() {
int row1 = modelIndex;
FileSystemTreeModel.Node o1 = ((FileSystemTreeModel.Node) model.getChild(path.getLastPathComponent(), row1));
SystemItemInfo i1 = (SystemItemInfo) systemItemsMap.get(o1.getUserName());
if (i1 == null && o1.getResolvedFile().getName().equals("")) {
i1 = (SystemItemInfo) systemItemsMap.get("Computer");
}
if (i1 != null) {
return i1.sequenceNumber;
}
return row1;
}
@Override
public boolean equals(Object o) {
return (o instanceof Row)//
? compareTo(o)==0
: false;
}
}
}