/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.openide.loaders;
import java.awt.datatransfer.*;
import java.beans.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import org.openide.ErrorManager;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileStatusListener;
import org.openide.filesystems.FileStatusEvent;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileObject;
import org.openide.util.datatransfer.*;
import org.openide.util.HelpCtx;
import org.openide.util.RequestProcessor;
import org.openide.util.NbBundle;
import org.openide.util.WeakListener;
import org.openide.util.actions.SystemAction;
import org.openide.nodes.*;
/** Standard node representing a data object.
*
* @author Jaroslav Tulach
*/
public class DataNode extends AbstractNode {
/** generated Serialized Version UID */
static final long serialVersionUID = -7882925922830244768L;
/** DataObject of this node. */
private DataObject obj;
/** property change listener */
private PropL propL;
/** should file extensions be displayed? */
private static boolean showFileExtensions = false;
/** Create a data node for a given data object.
* The provided children object will be used to hold all child nodes.
* The name is always set to the base name of the primary file;
* the display name may instead be set to the base name with extension.
* @param obj object to work with
* @param ch children container for the node
* @see #getShowFileExtensions
*/
public DataNode (DataObject obj, Children ch) {
super (ch);
this.obj = obj;
propL = new PropL ();
obj.addPropertyChangeListener (WeakListener.propertyChange (propL, obj));
super.setName (obj.getName ());
updateDisplayName ();
}
private void updateDisplayName () {
FileObject prim = obj.getPrimaryFile ();
String newDisplayName = null;
if (showFileExtensions || obj instanceof DataFolder || obj instanceof DefaultDataObject) {
newDisplayName = prim.getNameExt();
} else {
newDisplayName = prim.getName ();
}
if (displayFormat != null)
setDisplayName (displayFormat.format (new Object[] { newDisplayName }));
else
setDisplayName (newDisplayName);
}
/** Get the represented data object.
* @return the data object
*/
public DataObject getDataObject() {
return obj;
}
/** Changes the name of the node and may also rename the data object.
* If the object is renamed and file extensions are to be shown,
* the display name is also updated accordingly.
*
* @param name new name for the object
* @param rename rename the data object?
* @exception IllegalArgumentException if the rename failed
*/
public void setName (String name, boolean rename) {
try {
if (rename) {
obj.rename (name);
}
super.setName (name);
if (rename) updateDisplayName ();
} catch (IOException ex) {
String msg = null;
if ((ex.getLocalizedMessage() == null) ||
(ex.getLocalizedMessage().equals(ex.getMessage()))) {
msg = NbBundle.getMessage (DataNode.class, "MSG_renameError", getName (), name); // NOI18N
} else {
msg = ex.getLocalizedMessage();
}
RuntimeException e = new IllegalArgumentException();
ErrorManager.getDefault().copyAnnotation (e, ex);
ErrorManager.getDefault().annotate (e, ErrorManager.USER, null, msg, null, null);
throw e;
}
}
/* Rename the data object.
* @param name new name for the object
* @exception IllegalArgumentException if the rename failed
*/
public void setName (String name) {
setName (name, true);
}
/** Get the display name for the node.
* A filesystem may {@link org.openide.filesystems.FileSystem#getStatus specially alter} this.
* Subclassers overriding this method should consider the recommendations
* in {@link DataObject#createNodeDelegate}.
* @return the desired name
*/
public String getDisplayName () {
String s = super.getDisplayName ();
try {
s = obj.getPrimaryFile ().getFileSystem ().getStatus ().annotateName (s, obj.files ());
} catch (FileStateInvalidException e) {
// no fs, do nothing
}
return s;
}
/** Get the displayed icon for this node.
* A filesystem may {@link org.openide.filesystems.FileSystem#getStatus specially alter} this.
* Subclassers overriding this method should consider the recommendations
* in {@link DataObject#createNodeDelegate}.
* @param type the icon type from {@link java.beans.BeanInfo}
* @return the desired icon
*/
public java.awt.Image getIcon (int type) {
java.awt.Image img = super.getIcon (type);
try {
img = obj.getPrimaryFile ().getFileSystem ().getStatus ().annotateIcon (img, type, obj.files ());
} catch (FileStateInvalidException e) {
// no fs, do nothing
}
return img;
}
/** Get the displayed icon for this node.
* A filesystem may {@link org.openide.filesystems.FileSystem#getStatus specially alter} this.
* Subclassers overriding this method should consider the recommendations
* in {@link DataObject#createNodeDelegate}.
* @param type the icon type from {@link java.beans.BeanInfo}
* @return the desired icon
*/
public java.awt.Image getOpenedIcon (int type) {
java.awt.Image img = super.getOpenedIcon(type);
try {
img = obj.getPrimaryFile ().getFileSystem ().getStatus ().annotateIcon (img, type, obj.files ());
} catch (FileStateInvalidException e) {
// no fs, do nothing
}
return img;
}
public HelpCtx getHelpCtx () {
return obj.getHelpCtx ();
}
/** Indicate whether the node may be renamed.
* @return tests {@link DataObject#isRenameAllowed}
*/
public boolean canRename () {
return obj.isRenameAllowed ();
}
/** Indicate whether the node may be destroyed.
* @return tests {@link DataObject#isDeleteAllowed}
*/
public boolean canDestroy () {
return obj.isDeleteAllowed ();
}
/* Destroyes the node
*/
public void destroy () throws IOException {
if (obj.isDeleteAllowed ()) {
obj.delete ();
}
super.destroy ();
}
/* Returns true if this object allows copying.
* @returns true if this object allows copying.
*/
public boolean canCopy () {
return obj.isCopyAllowed ();
}
/* Returns true if this object allows cutting.
* @returns true if this object allows cutting.
*/
public boolean canCut () {
return obj.isMoveAllowed ();
}
/** This method returns null to signal that actions
* provide by DataLoader.getActions should be returned from
* method getActions. If overriden to provide some actions,
* then these actions will be preferred to the loader's ones.
*
* @return null
*/
protected SystemAction[] createActions () {
return null;
}
/** Get actions for this data object.
* @see DataLoader#getActions
* @return array of actions or <code>null</code>
*/
public SystemAction[] getActions () {
if (systemActions == null) {
systemActions = createActions ();
}
if (systemActions != null) {
return systemActions;
}
return obj.getLoader ().getActions ();
}
/** Get default action. In the current implementation the
*<code>null</code> is returned in case the underlying data
* object is a template. The templates should not have any default
* action.
* @return no action if the underlying data object is a template.
* Otherwise the abstract node's default action is returned, if <code>null</code> then
* the first action returned from getActions () method is used.
*/
public SystemAction getDefaultAction () {
if (obj.isTemplate ()) {
return null;
} else {
SystemAction action = super.getDefaultAction ();
if (action != null) {
return action;
}
SystemAction[] arr = getActions ();
if (arr != null && arr.length > 0) {
return arr[0];
}
return null;
}
}
/** Get a cookie.
* First of all {@link DataObject#getCookie} is
* called. If it produces non-<code>null</code> result, that is returned.
* Otherwise the superclass is tried.
* Subclassers overriding this method should consider the recommendations
* in {@link DataObject#createNodeDelegate}.
*
* @return the cookie or <code>null</code>
*/
public Node.Cookie getCookie (Class cl) {
Node.Cookie c = obj.getCookie (cl);
if (c != null) {
return c;
} else {
return super.getCookie (cl);
}
}
/* Initializes sheet of properties. Allow subclasses to
* overwrite it.
* @return the default sheet to use
*/
protected Sheet createSheet () {
Sheet s = Sheet.createDefault ();
Sheet.Set ss = s.get (Sheet.PROPERTIES);
Node.Property p;
p = createNameProperty (obj);
ss.put (p);
if ( !getDataObject().getPrimaryFile().isReadOnly() )
try {
p = new PropertySupport.Reflection (
obj, Boolean.TYPE, "isTemplate", "setTemplate" // NOI18N
);
p.setName (DataObject.PROP_TEMPLATE);
p.setDisplayName (DataObject.getString("PROP_template"));
p.setShortDescription (DataObject.getString("HINT_template"));
ss.put (p);
} catch (Exception ex) {
throw new InternalError ();
}
/*
// Add a property with a list of all contained files, sorted by primary and then alphabetically:
ss.put (new PropertySupport.ReadOnly (DataObject.PROP_FILES, String[].class, "Files", "Files contained in this object.") { // [PENDING] I18N
public Object getValue () {
Set files = obj.files ();
String[] toret = new String[files.size ()];
int i = 0; for (Iterator it = files.iterator (); it.hasNext (); i++)
toret[i] = getNameExt ((FileObject) it.next ());
final String pfilename = getNameExt (obj.getPrimaryFile ());
Arrays.sort (toret, new Comparator () {
public int compare (Object o1, Object o2) {
String fname1 = (String) o1;
String fname2 = (String) o2;
if (fname1.equals (pfilename))
return -1;
else if (fname2.equals (pfilename))
return 1;
else
return fname1.compareTo (fname2);
}
});
return toret;
}
private String getNameExt (FileObject fo) {
if (fo.isRoot ()) return "<root of filesystem>"; // [PENDING] I18N
String name = fo.getName ();
String ext = fo.getExt ();
return ext == null || ext.equals ("") ? name : name + '.' + ext;
}
});
*/
return s;
}
/** Copy this node to the clipboard.
*
* @return {@link org.openide.util.datatransfer.ExTransferable.Single} with one copy flavor
* @throws IOException if it could not copy
* @see NodeTransfer
*/
public Transferable clipboardCopy () throws IOException {
ExTransferable t = ExTransferable.create (super.clipboardCopy ());
t.put (LoaderTransfer.transferable (
getDataObject (),
LoaderTransfer.CLIPBOARD_COPY)
);
return t;
}
/** Cut this node to the clipboard.
*
* @return {@link org.openide.util.datatransfer.ExTransferable.Single} with one cut flavor
* @throws IOException if it could not cut
* @see NodeTransfer
*/
public Transferable clipboardCut () throws IOException {
ExTransferable t = ExTransferable.create (super.clipboardCut ());
t.put (LoaderTransfer.transferable (
getDataObject (),
LoaderTransfer.CLIPBOARD_CUT)
);
return t;
}
/** Creates a name property for given data object.
*/
static Node.Property createNameProperty (final DataObject obj) {
Node.Property p = new PropertySupport.ReadWrite (
DataObject.PROP_NAME,
String.class,
DataObject.getString("PROP_name"),
DataObject.getString("HINT_name")
) {
public Object getValue () {
return obj.getName();
}
public void setValue (Object val) throws IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
if (!canWrite())
throw new IllegalAccessException();
if (!(val instanceof String))
throw new IllegalArgumentException();
try {
obj.rename ((String)val);
} catch (IOException ex) {
String msg = null;
if ((ex.getLocalizedMessage() == null) ||
(ex.getLocalizedMessage().equals(ex.getMessage()))) {
msg = NbBundle.getMessage (DataNode.class, "MSG_renameError", obj.getName(), val); // NOI18N
} else {
msg = ex.getLocalizedMessage();
}
ErrorManager.getDefault().annotate (ex, ErrorManager.USER, null, msg, null, null);
throw new InvocationTargetException(ex);
}
}
public boolean canWrite () {
return obj.isRenameAllowed();
}
};
return p;
}
/** Support for firing property change.
* @param ev event describing the change
*/
void fireChange (PropertyChangeEvent ev) {
if (DataFolder.PROP_CHILDREN.equals (ev.getPropertyName ())) {
// the node is not interested in children changes
return;
}
if (DataObject.PROP_PRIMARY_FILE.equals (ev.getPropertyName ())) {
// the node is not interested in children changes
propL.updateStatusListener ();
setName (obj.getName (), false);
return;
}
if (DataObject.PROP_NAME.equals(ev.getPropertyName())) {
super.setName (obj.getName ());
updateDisplayName();
return;
}
if (DataObject.PROP_COOKIE.equals(ev.getPropertyName())) {
fireCookieChange ();
} else {
firePropertyChange (ev.getPropertyName (), ev.getOldValue (), ev.getNewValue ());
}
// if the DataOjbect is not valid the node should be
// removed
if (DataObject.PROP_VALID.equals (ev.getPropertyName ())) {
Object newVal = ev.getNewValue();
if ((newVal instanceof Boolean)&&(! ((Boolean)newVal).booleanValue())) {
fireNodeDestroyed();
}
}
}
/** Handle for location of given data object.
* @return handle that remembers the data object.
*/
public Handle getHandle () {
return new ObjectHandle(obj, obj.isValid() ? (this != obj.getNodeDelegate()) : /* to be safe */ true);
}
/** Access method to fire icon change.
*/
final void fireChangeAccess (boolean icon, boolean name) {
if (name) {
fireDisplayNameChange (null, null);
}
if (icon) {
fireIconChange ();
}
}
/** Determine whether file extensions should be shown by default.
* By default, no.
* @return <code>true</code> if so
*/
public static boolean getShowFileExtensions () {
return showFileExtensions;
}
/** Set whether file extensions should be shown by default.
* @param s <code>true</code> if so
*/
public static void setShowFileExtensions (boolean s) {
boolean refresh = ( showFileExtensions != s );
showFileExtensions = s;
if ( refresh ) {
// refresh current nodes display name
RequestProcessor.postRequest(new Runnable () {
public void run () {
Iterator it = DataObjectPool.getPOOL().getActiveDataObjects();
while ( it.hasNext() ) {
DataObject obj = ((DataObjectPool.Item)it.next()).getDataObjectOrNull();
if ( obj != null && obj.getNodeDelegate() instanceof DataNode ) {
((DataNode)obj.getNodeDelegate()).updateDisplayName();
}
}
}
}, 300, Thread.MIN_PRIORITY);
}
}
/** Request processor task to update a bunch of names/icons.
* Potentially faster to do many nodes at once; see #16478.
*/
private static RequestProcessor.Task refreshNamesIconsTask = null;
/** nodes which should be refreshed */
private static Set refreshNameNodes = null; // Set<DataNode>
private static Set refreshIconNodes = null; // Set<DataNode>
/** whether the task is current scheduled and will still look in above sets */
private static boolean refreshNamesIconsRunning = false;
private static final Object refreshNameIconLock = "DataNode.refreshNameIconLock"; // NOI18N
/** Property listener on data object that delegates all changes of
* properties to this node.
*/
private class PropL extends Object
implements PropertyChangeListener, FileStatusListener, Runnable {
/** weak version of this listener */
private FileStatusListener weakL;
/** previous filesystem we were attached to */
private FileSystem previous;
public PropL () {
updateStatusListener ();
}
public void propertyChange (PropertyChangeEvent ev) {
fireChange (ev);
}
/** Updates listening on a status of filesystem.
*/
private void updateStatusListener () {
if (previous != null) {
previous.removeFileStatusListener (weakL);
}
try {
previous = obj.getPrimaryFile ().getFileSystem ();
if (weakL == null) {
weakL = WeakListener.fileStatus (this, null);
}
previous.addFileStatusListener (weakL);
} catch (FileStateInvalidException ex) {
previous = null;
}
}
/** Notifies listener about change in annotataion of a few files.
* @param ev event describing the change
*/
public void annotationChanged (FileStatusEvent ev) {
// #16541: listen for changes in both primary and secondary files
boolean thisChanged = false;
Iterator it = obj.files().iterator();
while (it.hasNext()) {
FileObject fo = (FileObject)it.next();
if (ev.hasChanged(fo)) {
thisChanged = true;
break;
}
}
if (thisChanged) {
// #12368: fire display name & icon changes asynch
synchronized (refreshNameIconLock) {
boolean post = false;
if (ev.isNameChange()) {
if (refreshNameNodes == null) {
refreshNameNodes = new HashSet();
}
post |= refreshNameNodes.add(DataNode.this);
}
if (ev.isIconChange()) {
if (refreshIconNodes == null) {
refreshIconNodes = new HashSet();
}
post |= refreshIconNodes.add(DataNode.this);
}
if (post && !refreshNamesIconsRunning) {
refreshNamesIconsRunning = true;
if (refreshNamesIconsTask == null) {
refreshNamesIconsTask = RequestProcessor.postRequest(this);
} else {
// Should be OK even if it is running right now.
// (Cf. RequestProcessorTest.testScheduleWhileRunning.)
refreshNamesIconsTask.schedule(0);
}
}
}
}
}
/** Refreshes names and icons for a whole batch of data nodes at once.
*/
public void run() {
DataNode[] _refreshNameNodes, _refreshIconNodes;
synchronized (refreshNameIconLock) {
if (refreshNameNodes != null) {
_refreshNameNodes = (DataNode[])refreshNameNodes.toArray(new DataNode[refreshNameNodes.size()]);
refreshNameNodes.clear();
} else {
_refreshNameNodes = new DataNode[0];
}
if (refreshIconNodes != null) {
_refreshIconNodes = (DataNode[])refreshIconNodes.toArray(new DataNode[refreshIconNodes.size()]);
refreshIconNodes.clear();
} else {
_refreshIconNodes = new DataNode[0];
}
refreshNamesIconsRunning = false;
}
for (int i = 0; i < _refreshNameNodes.length; i++) {
_refreshNameNodes[i].fireChangeAccess(false, true);
}
for (int i = 0; i < _refreshIconNodes.length; i++) {
_refreshIconNodes[i].fireChangeAccess(true, false);
}
}
}
/** Handle for data object nodes */
private static class ObjectHandle extends Object implements Handle {
private FileObject obj;
private boolean clone;
static final long serialVersionUID =6616060729084681518L;
public ObjectHandle (DataObject obj, boolean clone) {
this.obj = obj.getPrimaryFile ();
this.clone = clone;
}
public Node getNode () throws DataObjectNotFoundException {
Node n = DataObject.find (obj).getNodeDelegate ();
return clone ? n.cloneNode () : n;
}
}
}