/* * @(#)SidebarTreeModel.java * * Copyright (c) 2011 Werner Randelshofer, Immensee, Switzerland. * All rights reserved. * * The copyright of this software is owned by Werner Randelshofer. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Werner Randelshofer. For details see accompanying license terms. */ package ch.randelshofer.quaqua.lion.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.*; /** * SidebarTreeModel. * * @author Werner Randelshofer * @version $Id: SidebarTreeModel.java 405 2011-07-26 11:17:31Z wrandelshofer $ */ public class SidebarTreeModel extends DefaultTreeModel implements TreeModelListener { /** * 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 volumesPath to the /Volumes folder. */ private TreePath volumesPath; /** * Holds the FileSystemTreeModel. */ private TreeModel model; /** * Represents the "Devices" node in the sidebar. */ private DefaultMutableTreeNode devicesNode; /** * Represents the "Places" node in the sidebar. */ private DefaultMutableTreeNode favoritesNode; /** * Intervals between validations. */ private final static long VALIDATION_TTL = 60000; /** * Time for next validation of the model. */ private long bestBefore; /** * The JFileChooser. */ private JFileChooser fileChooser; /** * Sequential dispatcher for the lazy creation of icons. */ private SequentialDispatcher dispatcher = new SequentialDispatcher(); /** * 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(); /** * The defaultUserItems are used when we fail to read the user items from * the sidebarFile. */ private final static File[] defaultUserItems; static { if (QuaquaManager.isOSX() || QuaquaManager.getOS() == QuaquaManager.DARWIN) { defaultUserItems = new File[]{ new File(QuaquaManager.getProperty("user.home"), "Desktop"), new File(QuaquaManager.getProperty("user.home"), "Documents"), new File(QuaquaManager.getProperty("user.home")) }; } else if (QuaquaManager.getOS() == QuaquaManager.WINDOWS) { defaultUserItems = new File[]{ 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 if (QuaquaManager.getOS() == QuaquaManager.LINUX) { defaultUserItems = new File[]{ new File(QuaquaManager.getProperty("user.home"), "Desktop"), new File("/media"), new File(QuaquaManager.getProperty("user.home"), "Documents"), new File(QuaquaManager.getProperty("user.home")) }; } else { defaultUserItems = new File[]{ new File(QuaquaManager.getProperty("user.home")) }; } } /** Creates a new instance. */ public SidebarTreeModel(JFileChooser fileChooser, TreePath path, TreeModel model) { super(new DefaultMutableTreeNode(), true); this.fileChooser = fileChooser; this.volumesPath = path; this.model = model; devicesNode = new DefaultMutableTreeNode(UIManager.getString("FileChooser.devices")); devicesNode.setAllowsChildren(true); favoritesNode = new DefaultMutableTreeNode(UIManager.getString("FileChooser.favorites")); favoritesNode.setAllowsChildren(true); DefaultMutableTreeNode r = (DefaultMutableTreeNode) getRoot(); r.add(favoritesNode); r.add(devicesNode); validate(); updateDevicesNode(); model.addTreeModelListener(this); } public void lazyValidate() { // throw new UnsupportedOperationException("Not yet implemented"); } /** * 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]; update(freshUserItems); } @Override public void failed(Throwable value) { ArrayList freshUserItems; System.err.println("Warning: SidebarTreeModel uses default user items."); 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 FileNode(defaultUserItems[i])); } } update(freshUserItems); } private void update(ArrayList freshUserItems) { int oldUserItemsSize = favoritesNode.getChildCount(); if (oldUserItemsSize > 0) { int[] removedIndices = new int[oldUserItemsSize]; Object[] removedChildren = new Object[oldUserItemsSize]; for (int i = 0; i < oldUserItemsSize; i++) { removedIndices[i] = i; removedChildren[i] = favoritesNode.getChildAt(i); } favoritesNode.removeAllChildren(); fireTreeNodesRemoved( SidebarTreeModel.this, favoritesNode.getPath(), removedIndices, removedChildren); } if (freshUserItems.size() > 0) { int[] insertedIndices = new int[freshUserItems.size()]; Object[] insertedChildren = new Object[freshUserItems.size()]; for (int i = 0; i < freshUserItems.size(); i++) { insertedIndices[i] = i; insertedChildren[i] = freshUserItems.get(i); if (freshUserItems.get(i) == null) { favoritesNode.add(new DefaultMutableTreeNode("null?")); } else { favoritesNode.add((DefaultMutableTreeNode) freshUserItems.get(i)); } } fireTreeNodesInserted( SidebarTreeModel.this, favoritesNode.getPath(), insertedIndices, insertedChildren); } bestBefore = System.currentTimeMillis() + VALIDATION_TTL; } }); } private void updateDevicesNode() { FileSystemTreeModel.Node modelDevicesNode = (FileSystemTreeModel.Node) volumesPath.getLastPathComponent(); // Remove nodes from the view which are not present in the model for (int i = devicesNode.getChildCount() - 1; i >= 0; i--) { SidebarViewToModelNode viewNode = (SidebarViewToModelNode) devicesNode.getChildAt(i); if (viewNode.getTarget().getParent() != modelDevicesNode) { removeNodeFromParent(viewNode); } } // Add nodes to the view, wich are present in the model, but not // in the view. Only add non-leaf nodes for (int i = 0, n = modelDevicesNode.getChildCount(); i < n; i++) { FileSystemTreeModel.Node modelNode = (FileSystemTreeModel.Node) modelDevicesNode.getChildAt(i); if (!modelNode.isLeaf()) { boolean isInView = false; for (int j = 0, m = devicesNode.getChildCount(); j < m; j++) { SidebarViewToModelNode viewNode = (SidebarViewToModelNode) devicesNode.getChildAt(j); if (viewNode.getTarget() == modelNode) { isInView = true; break; } } if (!isInView) { SidebarViewToModelNode newNode = new SidebarViewToModelNode(modelNode); int insertionIndex = 0; SideBarViewToModelNodeComparator comparator=new SideBarViewToModelNodeComparator(); while (insertionIndex < devicesNode.getChildCount() && comparator.compare((SidebarViewToModelNode) devicesNode.getChildAt(insertionIndex),newNode) < 0) { insertionIndex++; } insertNodeInto(newNode, devicesNode, insertionIndex); } } } // Update the view if (devicesNode.getChildCount() > 0) { int[] childIndices = new int[devicesNode.getChildCount()]; Object[] childNodes = new Object[devicesNode.getChildCount()]; for (int i = 0; i < childIndices.length; i++) { childIndices[i] = i; childNodes[i] = devicesNode.getChildAt(i); } fireTreeNodesChanged(this, devicesNode.getPath(), childIndices, childNodes); } } /** * 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 userItems = 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("favorites")) { 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; int entryType=0; byte[] serializedAlias = null; boolean isVisible=true; 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 (key5.equals("EntryType")) { // EntryType marks items which have been added // by the System. try { entryType=Integer.parseInt(xml5.getContent()); } catch (NumberFormatException e) { entryType=1; } } if (key5.equals("Visibility")) { if (xml5.getContent()!=null&&xml5.getContent().equals("NeverVisible")) { isVisible=false; } } } if (serializedAlias != null && aliasName != null && entryType==0 && isVisible) { // Suppress the "All My Files" folder. if (aliasName.equals("All My Files")) continue; // Try to resolve the alias without user interaction File f = OSXFile.resolveAlias(serializedAlias, true); if (f != null) { userItems.add(new FileNode(f)); } else { userItems.add(new AliasNode(serializedAlias, aliasName)); } } } } } } } } finally { if (reader != null) { reader.close(); } } return new Object[]{sysItemsMap, userItems}; } public void treeNodesChanged(TreeModelEvent e) { if (e.getTreePath().equals(volumesPath)) { updateDevicesNode(); } } public void treeNodesInserted(TreeModelEvent e) { if (e.getTreePath().equals(volumesPath)) { updateDevicesNode(); } } public void treeNodesRemoved(TreeModelEvent e) { if (e.getTreePath().equals(volumesPath)) { updateDevicesNode(); } } public void treeStructureChanged(TreeModelEvent e) { if (e.getTreePath().equals(volumesPath)) { updateDevicesNode(); } } private class FileNode extends Node { 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 FileNode(File file) { this.file = file; // userName = fileChooser.getName(file); isTraversable = true; } public File lazyGetResolvedFile() { return file; } public File getResolvedFile() { return file; } public File getFile() { return file; } public boolean allowsChildren() { return false; } 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; int[] changedIndices = {getParent().getIndex(FileNode.this)}; Object[] changedChildren = {FileNode.this}; SidebarTreeModel.this.fireTreeNodesChanged( SidebarTreeModel.this, favoritesNode.getPath(), changedIndices, changedChildren); } }); } } 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; } } /** * An AliasNode is resolved as late as possible. */ private abstract class Node extends DefaultMutableTreeNode implements FileInfo { @Override public boolean getAllowsChildren() { return false; } public boolean isHidden() { return false; } } /** * An AliasNode is resolved as late as possible. */ private class AliasNode extends Node { 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 AliasNode(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) { // Note: We clear this icon, when we resolve the alias 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; int[] changedIndices = new int[]{getParent().getIndex(AliasNode.this)}; Object[] changedChildren = new Object[]{AliasNode.this}; SidebarTreeModel.this.fireTreeNodesChanged( SidebarTreeModel.this, ((DefaultMutableTreeNode) AliasNode.this.getParent()).getPath(), changedIndices, changedChildren); } }); } } 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; } } private static class SystemItemInfo { String name = ""; int sequenceNumber = 0; boolean isVisible = true; } /** Note: SidebaViewToModelNode must not implement Comparable and must * not override equals()/hashCode(), because this confuses the layout algorithm * in JTree. */ private class SidebarViewToModelNode extends Node /*implements Comparable*/ { private FileSystemTreeModel.Node target; public SidebarViewToModelNode(FileSystemTreeModel.Node target) { this.target = target; } public File getFile() { return target.getFile(); } public File getResolvedFile() { return target.getResolvedFile(); } public File lazyGetResolvedFile() { return target.lazyGetResolvedFile(); } public boolean isTraversable() { return target.isTraversable(); } public boolean isAcceptable() { return target.isAcceptable(); } public int getFileLabel() { return target.getFileLabel(); } public String getUserName() { return target.getUserName(); } public Icon getIcon() { return target.getIcon(); } public long getFileLength() { return target.getFileLength(); } public String getFileKind() { return target.getFileKind(); } public boolean isValidating() { return target.isValidating(); } public FileSystemTreeModel.Node getTarget() { return target; } @Override public String toString() { return target.toString(); } /* public int compareTo(Object o) { return compareTo((SidebarViewToModelNode) o); } public int compareTo(SidebarViewToModelNode that) { FileSystemTreeModel.Node o1 = this.getTarget(); FileSystemTreeModel.Node o2 = that.getTarget(); 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 0; } @Override public boolean equals(Object o) { return (o instanceof SidebarViewToModelNode) // ? compareTo((SidebarViewToModelNode) o) == 0 // : false; } @Override public int hashCode() { return getTarget() == null ? 0 : getTarget().getUserName().hashCode(); }*/ } private class SideBarViewToModelNodeComparator implements Comparator<SidebarViewToModelNode> { public int compare(SidebarViewToModelNode n1, SidebarViewToModelNode n2) { FileSystemTreeModel.Node o1 = n1.getTarget(); FileSystemTreeModel.Node o2 = n2.getTarget(); 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 0; } } }