/* * 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.text; import java.awt.event.*; import java.beans.*; import java.io.*; import java.util.*; import java.lang.ref.Reference; import javax.swing.text.*; import org.openide.DialogDisplayer; import org.openide.actions.*; import org.openide.ErrorManager; import org.openide.NotifyDescriptor; import org.openide.filesystems.*; import org.openide.nodes.Node; import org.openide.nodes.NodeAdapter; import org.openide.nodes.NodeListener; import org.openide.loaders.*; import org.openide.windows.*; import org.openide.util.WeakListener; import org.openide.util.NbBundle; /** Support for associating an editor and a Swing {@link Document} to a data object. * * * @author Jaroslav Tulach */ public class DataEditorSupport extends CloneableEditorSupport { /** Which data object we are associated with */ private final DataObject obj; /** listener to asociated node's events */ private NodeListener nodeL; /** Editor support for a given data object. The file is taken from the * data object and is updated if the object moves or renames itself. * @param obj object to work with * @param env environment to pass to */ public DataEditorSupport (DataObject obj, CloneableEditorSupport.Env env) { super (env); this.obj = obj; } /** Getter of the data object that this support is associated with. * @return data object passed in constructor */ public final DataObject getDataObject () { return obj; } /** Message to display when an object is being opened. * @return the message or null if nothing should be displayed */ protected String messageOpening () { return NbBundle.getMessage (DataEditorSupport.class , "CTL_ObjectOpen", // NOI18N obj.getName(), obj.getPrimaryFile().toString() ); } /** Message to display when an object has been opened. * @return the message or null if nothing should be displayed */ protected String messageOpened () { return NbBundle.getMessage (DataEditorSupport.class, "CTL_ObjectOpened", // NOI18N obj.getName (), obj.getPrimaryFile ().toString () ); } /** Constructs message that should be displayed when the data object * is modified and is being closed. * * @return text to show to the user */ protected String messageSave () { return NbBundle.getMessage ( DataEditorSupport.class, "MSG_SaveFile", // NOI18N obj.getName() ); } /** Constructs message that should be used to name the editor component. * * @return name of the editor */ protected String messageName () { if (! obj.isValid()) return ""; // NOI18N String name = obj.getNodeDelegate().getDisplayName(); int version = 3; if (isModified ()) { if (obj.getPrimaryFile ().isReadOnly ()) { version = 2; } else { version = 1; } } else if (obj.getPrimaryFile ().isReadOnly ()) { version = 0; } return NbBundle.getMessage (DataEditorSupport.class, "LAB_EditorName", new Integer (version), name ); } /** Text to use as tooltip for component. * * @return text to show to the user */ protected String messageToolTip () { // update tooltip FileObject fo = obj.getPrimaryFile (); try { File f = FileUtil.toFile (fo); if (f!=null) { return f.getAbsolutePath (); } else { return NbBundle.getMessage (DataEditorSupport.class, "LAB_EditorToolTip", new Object[] { fo.getPath(), fo.getFileSystem ().getDisplayName () }); } } catch (FileStateInvalidException fsie) { return fo.getPath(); } } /** Annotates the editor with icon from the data object and also sets * appropriate selected node. But only in the case the data object is valid. * This implementation also listen to display name and icon chamges of the * node and keeps editor top component up-to-date. If you override this * method and not call super, please note that you will have to keep things * synchronized yourself. * * @param editor the editor that has been created and should be annotated */ protected void initializeCloneableEditor (CloneableEditor editor) { // Prevention to bug similar to #17134. Don't call getNodeDelegate // on invalid data object. Top component should be discarded later. if(obj.isValid()) { Node ourNode = obj.getNodeDelegate(); editor.setActivatedNodes (new Node[] { ourNode }); editor.setIcon(ourNode.getIcon (java.beans.BeanInfo.ICON_COLOR_16x16)); NodeListener nl = new DataNodeListener(editor); ourNode.addNodeListener(WeakListener.node(nl, ourNode)); nodeL = nl; } } /** Called when closed all components. Overrides superclass method, * also unregisters listening on node delegate. */ protected void notifyClosed() { // #27645 All components were closed, unregister weak listener on node. nodeL = null; super.notifyClosed(); } /** Let's the super method create the document and also annotates it * with Title and StreamDescription properities. * * @param kit kit to user to create the document * @return the document annotated by the properties */ protected StyledDocument createStyledDocument (EditorKit kit) { StyledDocument doc = super.createStyledDocument (kit); // set document name property doc.putProperty(javax.swing.text.Document.TitleProperty, obj.getPrimaryFile ().getPath() ); // set dataobject to stream desc property doc.putProperty(javax.swing.text.Document.StreamDescriptionProperty, obj ); return doc; } /** Checks whether is possible to close support components. * Overrides superclass method, adds checking * for read-only property of saving file and warns user in that case. */ protected boolean canClose() { if(env().isModified() && isEnvReadOnly()) { Object result = DialogDisplayer.getDefault().notify( new NotifyDescriptor.Confirmation( NbBundle.getMessage(DataEditorSupport.class, "MSG_FileReadOnlyClosing", new Object[] {((Env)env).getFileImpl().getNameExt()}), NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.WARNING_MESSAGE )); return result == NotifyDescriptor.OK_OPTION; } return super.canClose(); } /** Saves document. Overrides superclass method, adds checking * for read-only property of saving file and warns user in that case. */ public void saveDocument() throws IOException { if(env().isModified() && isEnvReadOnly()) { DialogDisplayer.getDefault().notify( new NotifyDescriptor.Message( NbBundle.getMessage(DataEditorSupport.class, "MSG_FileReadOnlySaving", new Object[] {((Env)env).getFileImpl().getNameExt()}), NotifyDescriptor.WARNING_MESSAGE )); return; } super.saveDocument(); } /** Indicates whether the <code>Env</code> is read only. */ boolean isEnvReadOnly() { CloneableEditorSupport.Env env = env(); return env instanceof Env && ((Env)env).getFileImpl().isReadOnly(); } /** Getter for data object associated with this * data object. */ final DataObject getDataObjectHack () { return obj; } /** Environment that connects the data object and the CloneableEditorSupport. */ public static abstract class Env extends OpenSupport.Env implements CloneableEditorSupport.Env, java.io.Serializable, PropertyChangeListener, VetoableChangeListener { /** generated Serialized Version UID */ static final long serialVersionUID = -2945098431098324441L; /** The file object this environment is associated to. * This file object can be changed by a call to refresh file. */ private transient FileObject fileObject; /** Lock acquired after the first modification and used in save. * Transient => is not serialized. */ private transient FileLock fileLock; /** Constructor. * @param obj this support should be associated with */ public Env (DataObject obj) { super (obj); } /** Getter for the file to work on. * @return the file */ private FileObject getFileImpl () { // updates the file if there was a change changeFile(); return fileObject; } /** Getter for file associated with this environment. * @return the file input/output operation should be performed on */ protected abstract FileObject getFile (); /** Locks the file. * @return the lock on the file getFile () * @exception IOException if the file cannot be locked */ protected abstract FileLock takeLock () throws IOException; /** Method that allows subclasses to notify this environment that * the file associated with this support has changed and that * the environment should listen on modifications of different * file object. */ protected final void changeFile () { FileObject newFile = getFile (); if (newFile.equals (fileObject)) { // the file has not been updated return; } boolean lockAgain; if (fileLock != null) { fileLock.releaseLock (); lockAgain = true; } else { lockAgain = false; } fileObject = newFile; fileObject.addFileChangeListener (new EnvListener (this)); if (lockAgain) { // refresh lock try { fileLock = takeLock (); } catch (IOException e) { ErrorManager.getDefault ().notify ( ErrorManager.INFORMATIONAL, e); } } } /** Obtains the input stream. * @exception IOException if an I/O error occures */ public InputStream inputStream() throws IOException { InputStream is = getFileImpl ().getInputStream (); return is; } /** Obtains the output stream. * @exception IOException if an I/O error occures */ public OutputStream outputStream() throws IOException { return getFileImpl ().getOutputStream (fileLock); } /** The time when the data has been modified */ public Date getTime() { // #32777 - refresh file object and return always the actual time getFileImpl().refresh(false); return getFileImpl ().lastModified (); } /** Mime type of the document. * @return the mime type to use for the document */ public String getMimeType() { return getFileImpl ().getMIMEType (); } /** First of all tries to lock the primary file and * if it succeeds it marks the data object modified. * <p><b>Note: There is a contract (better saying a curse) * that this method has to call {@link #takeLock} method * in order to keep working some special filesystem's feature. * See <a href="http://www.netbeans.org/issues/show_bug.cgi?id=28212">issue #28212</a></b>. * * @exception IOException if the environment cannot be marked modified * (for example when the file is readonly), when such exception * is the support should discard all previous changes * @see org.openide.filesystems.FileObject#isReadOnly */ public void markModified() throws java.io.IOException { // XXX This shouldn't be here. But it is due to the 'contract', // see javadoc to this method. if (fileLock == null || !fileLock.isValid()) { fileLock = takeLock (); } if(getFileImpl().isReadOnly()) { if(fileLock != null && fileLock.isValid()) { fileLock.releaseLock(); } throw new IOException("File " // NOI18N + getFileImpl().getNameExt() + " is read-only!"); // NOI18N } this.getDataObject ().setModified (true); } /** Reverse method that can be called to make the environment * unmodified. */ public void unmarkModified() { if (fileLock != null && fileLock.isValid()) { fileLock.releaseLock(); } this.getDataObject ().setModified (false); } /** Called from the EnvListener * @param expected is the change expected * @param time of the change */ final void fileChanged (boolean expected, long time) { if (expected) { // newValue = null means do not ask user whether to reload firePropertyChange (PROP_TIME, null, null); } else { firePropertyChange (PROP_TIME, null, new Date (time)); } } /** Called from the <code>EnvListener</code>. * The components are going to be closed anyway and in case of * modified document its asked before if to save the change. */ private void fileRemoved(boolean canBeVetoed) { if (canBeVetoed) { try { // Causes the 'Save' dialog to show if necessary. fireVetoableChange(Env.PROP_VALID, Boolean.TRUE, Boolean.FALSE); } catch(PropertyVetoException pve) { // Ignore it and close anyway. File doesn't exist anymore. } } // Closes the components. firePropertyChange(Env.PROP_VALID, Boolean.TRUE, Boolean.FALSE); } } // end of Env /** Listener on file object that notifies the Env object * that a file has been modified. */ private static final class EnvListener extends FileChangeAdapter { /** Reference (Env) */ private Reference env; /** @param env environement to use */ public EnvListener (Env env) { this.env = new java.lang.ref.WeakReference (env); } /** Handles <code>FileObject</code> deletion event. */ public void fileDeleted(FileEvent fe) { Env env = (Env)this.env.get(); FileObject fo = fe.getFile(); if(env == null || env.getFileImpl() != fo) { // the Env change its file and we are not used // listener anymore => remove itself from the list of listeners fo.removeFileChangeListener(this); return; } fo.removeFileChangeListener(this); // #30210 - when edited file was deleted the "Do you want to save changes" // dialog should not be shown env.fileRemoved(false); fo.addFileChangeListener(this); } /** Fired when a file is changed. * @param fe the event describing context where action has taken place */ public void fileChanged(FileEvent fe) { Env env = (Env)this.env.get (); if (env == null || env.getFileImpl () != fe.getFile ()) { // the Env change its file and we are not used // listener anymore => remove itself from the list of listeners fe.getFile ().removeFileChangeListener (this); return; } // #16403. Added handling for virtual property of the file. if(fe.getFile().isVirtual()) { // Remove file event coming as consequence of this change. fe.getFile().removeFileChangeListener(this); // File doesn't exist on disk -> simulate env is invalid, // even the fileObject could be valid, see VCS FS. env.fileRemoved(true); fe.getFile().addFileChangeListener(this); } else { env.fileChanged (fe.isExpected (), fe.getTime ()); } } } /** Listener on node representing asociated data object, listens to the * property changes of the node and updates state properly */ private final class DataNodeListener extends NodeAdapter { /** Asociated editor */ CloneableEditor editor; DataNodeListener (CloneableEditor editor) { this.editor = editor; } public void propertyChange (java.beans.PropertyChangeEvent ev) { if (Node.PROP_DISPLAY_NAME.equals(ev.getPropertyName())) { updateTitles(); } if (Node.PROP_ICON.equals(ev.getPropertyName())) { if (obj.isValid()) { editor.setIcon(obj.getNodeDelegate().getIcon (java.beans.BeanInfo.ICON_COLOR_16x16)); } } } } // end of DataNodeListener }