/*
* 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.Font;
import java.awt.Toolkit;
import java.awt.event.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.io.*;
import java.util.*;
import java.awt.print.PrinterJob;
import java.awt.print.Pageable;
import java.awt.print.Printable;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.awt.print.PrinterAbortException;
import java.beans.PropertyChangeSupport;
import javax.swing.JEditorPane;
import javax.swing.SwingUtilities;
import javax.swing.text.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.UndoableEdit;
import org.openide.DialogDisplayer;
import org.openide.awt.UndoRedo;
import org.openide.actions.*;
import org.openide.ErrorManager;
import org.openide.NotifyDescriptor;
import org.openide.awt.StatusDisplayer;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.*;
import org.openide.loaders.*;
import org.openide.windows.*;
import org.openide.util.Task;
import org.openide.util.TaskListener;
//import org.openide.util.actions.SystemAction;
import org.openide.util.RequestProcessor;
import org.openide.util.NbBundle;
import org.openide.util.UserQuestionException;
import org.openide.text.EnhancedChangeEvent;
/** Support for associating an editor and a Swing {@link Document}.
* Can be assigned as a cookie to any editable data object.
* This class is abstract, so any subclass has to provide implementation
* for abstract method (usually for generating of messages) and also
* provide environment {@link Env} to give this support access to
* input/output streams, mime type and other features of edited object.
*
* <P>
* This class implements methods of the interfaces
* {@link org.openide.cookies.EditorCookie}, {@link org.openide.cookies.OpenCookie},
* {@link org.openide.cookies.EditCookie},
* {@link org.openide.cookies.ViewCookie}, {@link org.openide.cookies.LineCookie},
* {@link org.openide.cookies.CloseCookie}, and {@link org.openide.cookies.PrintCookie}
* but does not implement
* those interfaces. It is up to the subclass to decide which interfaces
* really implement and which not.
*
* @author Jaroslav Tulach
*/
public abstract class CloneableEditorSupport extends CloneableOpenSupport {
/** Common name for editor mode. */
public static final String EDITOR_MODE = "editor"; // NOI18N
/** Flag saying if the CloneableEditorSupport handles already the UserQuestionException*/
private boolean inUserQuestionExceptionHandler;
/** Used for allowing to pass getDocument method
* when called from loadDocument from loadTask. */
private static final ThreadLocal LOCAL_LOAD_TASK = new ThreadLocal();
/** Task for loading the document. */
private Task loadTask;
/** Task for preparing the document. Consists for loading a document (runs loadTask),
* firing </code>stateChange</code> and
* initializing it by attaching listeners listening to document changes, such as SavingManager and
* LineSet.
*/
private Task prepareTask;
/** editor kit to work with */
private EditorKit kit;
/** document we work with */
private StyledDocument doc;
/** Non default MIME type used to editing */
private String mimeType;
/** Actions to show in toolbar */
// private SystemAction[] actions;
/** Listener to the document changes and all other changes */
private Listener listener;
/** the undo/redo manager to use for this document */
private UndoRedo.Manager undoRedo;
/** lines set for this object */
private Line.Set lineSet;
/** Lock used when for updating lineSet. */
private final Object LOCK_LINE_SET = new Object();
/** Helper variable to prevent multiple cocurrent printing of this
* instance. */
private boolean printing;
/** Lock used for access to <code>printing</code> variable. */
private final Object LOCK_PRINTING = new Object();
/** position manager */
private PositionRef.Manager positionManager;
/** The string which will be appended to the name of top component
* when top component becomes modified */
// protected String modifiedAppendix = " *"; // NOI18N
/** Listeners for the changing of the state - document in memory X closed. */
private HashSet listeners;
/** last selected editor. */
transient CloneableEditor lastSelected;
/** The time of the last save to determine the real external modifications */
private long lastSaveTime;
/** Whether the reload dialog is currently opened. Prevents poping of multiple
* reload dialogs if there is more external saves.
*/
private boolean reloadDialogOpened;
/** Support for property change listeners*/
private PropertyChangeSupport propertyChangeSupport;
/** Creates new CloneableEditorSupport attached to given environment.
*
* @param env environment that is source of all actions around the
* data object
*/
public CloneableEditorSupport(Env env) {
super (env);
}
//
// abstract messages section
//
/** Constructs message that should be displayed when the data object
* is modified and is being closed.
*
* @return text to show to the user
*/
protected abstract String messageSave ();
/** Constructs message that should be used to name the editor component.
*
* @return name of the editor
*/
protected abstract String messageName ();
/** Text to use as tooltip for component.
*
* @return text to show to the user
*/
protected abstract String messageToolTip ();
//
// Section of getter of default objects
//
/** Getter for the environment that was provided in the constructor.
* @return the environment
*/
final Env env () {
return (Env)env;
}
/** Getter for the kit that loaded the document.
*/
final EditorKit kit () {
return kit;
}
/** Getter for undo redo manager.
*/
protected final synchronized UndoRedo.Manager getUndoRedo() {
if(undoRedo == null) {
undoRedo = createUndoRedoManager();
}
return undoRedo;
}
/** Provides access to position manager for the document.
* It maintains a set of positions even the document is in memory
* or is on the disk.
*
* @return position manager
*/
final synchronized PositionRef.Manager getPositionManager() {
if(positionManager == null) {
positionManager = new PositionRef.Manager(this);
}
return positionManager;
}
/** Overrides superclass method, first processes document preparation.
* @see #prepareDocument */
public void open() {
prepareDocument().waitFinished();
super.open();
}
//
// EditorCookie.Observable implementation
//
/** Add a PropertyChangeListener to the listener list.
* See {@link org.openide.cookies.EditorCookie.Observable}.
* @param l the PropertyChangeListener to be added
* @since 3.40
*/
public final void addPropertyChangeListener(java.beans.PropertyChangeListener l) {
getPropertyChangeSupport().addPropertyChangeListener (l);
}
/** Remove a PropertyChangeListener from the listener list.
* See {@link org.openide.cookies.EditorCookie.Observable}.
* @param l the PropertyChangeListener to be removed
* @since 3.40
*/
public final void removePropertyChangeListener(java.beans.PropertyChangeListener l) {
getPropertyChangeSupport().removePropertyChangeListener (l);
}
/** Report a bound property update to any registered listeners.
* @param propertyName the programmatic name of the property that was changed.
* @param oldValue rhe old value of the property.
* @param newValue the new value of the property.
* @since 3.40
*/
protected final void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
getPropertyChangeSupport().firePropertyChange(propertyName, oldValue, newValue);
}
private synchronized PropertyChangeSupport getPropertyChangeSupport() {
if (propertyChangeSupport == null) {
propertyChangeSupport = new PropertyChangeSupport(this);
}
return propertyChangeSupport;
}
//
// EditorCookie implementation
//
// editor cookie .......................................................................
/** Load the document into memory. This is done
* in different thread. A task for the thread is returned
* so anyone may test whether the loading has been finished
* or is still in process.
*
* @return task for control over loading
*/
public Task prepareDocument() {
return prepareDocument(false);
}
/** @param clearDocument indicates whether the document is needed
* to clear before (used for reloading) */
private Task prepareDocument(final boolean clearDocument) {
// first test is done outside of getLock block because the getLock
// can be held for a long time
Task t = prepareTask;
if (t != null) {
return t;
}
synchronized (getLock ()) {
if (prepareTask != null)
return prepareTask;
// listen to modifications on env, but remove
// previous instance first
env.removePropertyChangeListener(getListener());
env.addPropertyChangeListener(getListener());
// after call to this method the originalDoc and kit are initialized
// in spite of that the document is not yet fully read in
kit = createEditorKit ();
if (doc == null) {
doc = createStyledDocument (kit);
}
// The thread nume should be: "Loading document " + env; // NOI18N
prepareTask = RequestProcessor.getDefault().post(new Runnable () {
public void run () {
try {
synchronized (getLock ()) {
if(clearDocument) {
// #24676. Reloading: Put positions into memory
// and fire document is closing (little trick
// to detach annotations).
getPositionManager().documentClosed();
fireDocumentChange(doc, true);
clearDocument();
}
// uses the listener's run method to initialize whole document
loadTask = new Task(getListener());
loadTask.run ();
}
fireDocumentChange(doc, false);
} catch (RuntimeException t) {
t.printStackTrace();
throw t;
}
}
});
return prepareTask;
}
}
/** Clears the <code>doc</code> document. Helper method. */
private void clearDocument() {
NbDocument.runAtomic(doc, new Runnable() {
public void run() {
try {
doc.removeDocumentListener(getListener());
doc.remove(0, doc.getLength()); // remove all text
doc.addDocumentListener(getListener());
} catch(BadLocationException ble) {
ErrorManager.getDefault().notify(
ErrorManager.INFORMATIONAL, ble);
}
}
});
}
/** Get the document associated with this cookie.
* It is an instance of Swing's {@link StyledDocument} but it should
* also understand the NetBeans {@link NbDocument#GUARDED} to
* prevent certain lines from being edited by the user.
* <P>
* If the document is not loaded the method blocks until
* it is.
*
* @return the styled document for this cookie that
* understands the guarded attribute
* @exception IOException if the document could not be loaded
*/
public StyledDocument openDocument () throws IOException {
for (;;) {
// load the document
prepareDocument ().waitFinished ();
IOException loadExc = getListener().checkLoadException();
if (loadExc != null) {
throw loadExc;
}
StyledDocument d = doc;
if (d != null)
return d;
}
}
/** Get the document. This method may be called before the document initialization
* (<code>prepareTask</code>)
* has been completed, in such a case the document must not be modified.
* @return document or <code>null</code> if it is not yet loaded
*/
public StyledDocument getDocument () {
// XXX #16048. In case there is called this method from loadTask
// (possible only via LineListener->DocumentLine..).
// PENDING Needs to be tried to redesign DocumentLine to avoid this.
if(LOCAL_LOAD_TASK.get() != null) {
return doc;
}
for (;;) {
Task t = loadTask;
if (t != null) {
// if an task exists
t.waitFinished ();
return doc;
} else {
return null;
}
}
}
/** Test whether the document is modified.
* @return <code>true</code> if the document is in memory and is modified;
* otherwise <code>false</code>
*/
public boolean isModified () {
return env ().isModified ();
}
/* Whether the file was externally modified or not.
* This flag is used only in saveDocument to prevent
* overriding of externally modified file. See issue #32777.
*/
private boolean externallyModified;
/** Save the document in this thread.
* Create 'orig' document for the case that the save would fail.
* @exception IOException on I/O error
*/
public void saveDocument () throws IOException {
// #17714: Don't try to save unmodified doc.
if(!env().isModified()) {
return;
}
//#32777: check that file was not modified externally.
// If it was then cancel saving operation. It is not absolutely
// correct, but there is no other way.
if (lastSaveTime != -1) {
externallyModified = false;
// asking for time should if necessary refresh the underlaying object
// (eg. FileObject) and this change can result in document reload task
// which will set externallyModified to true
env().getTime();
if (externallyModified) {
// save operation must be cancelled now. The user get message box
// asking user to reload externally modified file.
return;
}
}
StyledDocument myDoc = getDocument();
OutputStream os = null;
// write the document
long oldSaveTime = lastSaveTime;
try {
lastSaveTime = -1;
os = new BufferedOutputStream(env ().outputStream());
saveFromKitToStream (myDoc, kit, os);
if (os != null) {
os.close(); // peforms firing
os = null;
}
// remember time of last save
lastSaveTime = System.currentTimeMillis();
notifyUnmodified ();
} catch (BadLocationException ex) {
ErrorManager.getDefault().notify(ex);
} finally {
if (lastSaveTime == -1) // restore for unsuccessful save
lastSaveTime = oldSaveTime;
if (os != null) // try to close if not yet done
os.close();
}
// Insert before-save undo event to enable unmodifying undo
getUndoRedo().undoableEditHappened(
new UndoableEditEvent(this, new BeforeSaveEdit(lastSaveTime)));
// update cached info about lines
updateLineSet (true);
updateTitles ();
}
/* List of all JEditorPane's opened by this editor support.
* The first item in the array should represent the component
* that is currently selected or has been selected lastly.
*
* If you override this method and return also your own editor
* panes which are not descendants of {@link CloneableEditor}
* then it is your responsibility to fire
* {@link org.openide.cookies.EditorCookie.Observable#PROP_OPENED_PANES}
* property change whenever the list of opened panes changes.
*
* @return array of panes or null if no pane is opened.
* In no case empty array is returned.
*/
public JEditorPane[] getOpenedPanes () {
LinkedList ll = new LinkedList ();
Enumeration en = allEditors.getComponents ();
while (en.hasMoreElements ()) {
Object o = en.nextElement ();
if (o instanceof CloneableEditor) {
CloneableEditor ed = (CloneableEditor)o;
// #23491: pane could be still null, not yet shown component.
// [PENDING] Right solution? TopComponent opened, but pane not.
if(ed.pane == null) {
continue;
}
if (lastSelected == ed) {
ll.addFirst (ed.pane);
} else {
ll.add (ed.pane);
}
}
}
return ll.isEmpty () ?
null : (JEditorPane[])ll.toArray (new JEditorPane[ll.size ()]);
}
//
// LineSet interface impl
//
/** Get the line set for all paragraphs in the document.
* @return positions of all paragraphs on last save
*/
public Line.Set getLineSet () {
return updateLineSet (false);
}
//
// Print interface
//
/** A printing implementation suitable for {@link org.openide.cookies.PrintCookie}. */
public void print() {
synchronized(LOCK_PRINTING) {
if(printing) {
return;
}
printing = true;
}
try {
PrinterJob job = PrinterJob.getPrinterJob();
Object o = NbDocument.findPageable(openDocument());
if (o instanceof Pageable) {
job.setPageable((Pageable) o);
} else {
PageFormat pf = PrintSettings.getPageFormat(job);
job.setPrintable((Printable) o, pf);
}
if (job.printDialog()) {
job.print();
}
} catch (FileNotFoundException e) {
ErrorManager.getDefault().notify(e);
String msg = NbBundle.getBundle(CloneableEditorSupport.class)
.getString("CTL_Bad_File"); // NOI18N
notifyInAWT(msg);
} catch (IOException e) {
ErrorManager.getDefault().notify(e);
} catch (PrinterAbortException e) { // user exception
String msg = NbBundle.getBundle(CloneableEditorSupport.class)
.getString("CTL_Printer_Abort"); // NOI18N
notifyInAWT(msg);
} catch (PrinterException e) {
ErrorManager.getDefault().notify(e);
} finally {
synchronized(LOCK_PRINTING) {
printing = false;
}
}
}
static void notifyInAWT(final String msg) {
if (java.awt.EventQueue.isDispatchThread()) {
DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(msg));
} else {
java.awt.EventQueue.invokeLater(new Runnable() { // display in the awt thread
public void run() {
DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(msg));
}
});
}
}
//
// Methods overriden from CloneableOpenSupport
//
/** Prepares document, creates and initializes
* new <code>CloneableEditor</code> component.
* Typically do not override this method.
* For creating your own <code>CloneableEditor</code> type component
* override {@link #createCloneableEditor} method.
*
* @return the {@link CloneableEditor} for this support
*/
protected CloneableTopComponent createCloneableTopComponent () {
// initializes the document if not initialized
prepareDocument ();
CloneableEditor ed = createCloneableEditor ();
initializeCloneableEditor (ed);
return ed;
}
/** Should test whether all data is saved, and if not, prompt the user
* to save.
*
* @return <code>true</code> if everything can be closed
*/
protected boolean canClose () {
if (env ().isModified ()) {
String msg = messageSave ();
ResourceBundle bundle = NbBundle.getBundle(CloneableEditorSupport.class);
String saveOption = bundle.getString("CTL_Save");
String discardOption = bundle.getString("CTL_Discard");
NotifyDescriptor nd = new NotifyDescriptor(
msg,
bundle.getString("LBL_SaveFile_Title"),
NotifyDescriptor.YES_NO_CANCEL_OPTION,
NotifyDescriptor.QUESTION_MESSAGE,
new Object[] {saveOption, discardOption, NotifyDescriptor.CANCEL_OPTION},
saveOption
);
Object ret = DialogDisplayer.getDefault().notify(nd);
if (NotifyDescriptor.CANCEL_OPTION.equals(ret)
|| NotifyDescriptor.CLOSED_OPTION.equals(ret)
) {
return false;
}
if (saveOption.equals(ret)) {
try {
saveDocument ();
} catch (IOException e) {
ErrorManager.getDefault().notify(e);
return false;
}
}
}
return true;
/* old code was:
SaveCookie savec = (SaveCookie) entry.getDataObject().getCookie(SaveCookie.class);
if (savec != null) {
MessageFormat format = new MessageFormat(NbBundle.getBundle(EditorSupport.class).getString("MSG_SaveFile")); // NOI18N
String msg = format.format(new Object[] { entry.getDataObject().getName()});
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(msg, NotifyDescriptor.YES_NO_CANCEL_OPTION);
Object ret = DialogDisplayer.getDefault().notify(nd);
if (NotifyDescriptor.CANCEL_OPTION.equals(ret)
|| NotifyDescriptor.CLOSED_OPTION.equals(ret)
) {
return false;
}
if (NotifyDescriptor.YES_OPTION.equals(ret)) {
try {
savec.save();
}
catch (IOException e) {
ErrorManager.getDefault().notify(e);
return false;
}
}
}
*/
}
//
// public methods provided by this class
//
/** Test whether the document is in memory, or whether loading is still in progress.
* @return <code>true</code> if document is loaded
*/
public boolean isDocumentLoaded() {
return loadTask != null;
}
/**
* Set the MIME type for the document.
* @param s the new MIME type
*/
public void setMIMEType (String s) {
mimeType = s;
}
/** Adds a listener for status changes. An event is fired
* when the document is moved or removed from memory.
* @param l new listener
* @deprecated Deprecated since 3.40. Use {@link #addPropertyChangeListener} instead.
* See also {@link org.openide.cookies.EditorCookie.Observable}.
*/
public synchronized void addChangeListener (ChangeListener l) {
if (listeners == null)
listeners = new HashSet (8);
listeners.add (l);
}
/** Removes a listener for status changes.
* @param l listener to remove
* @deprecated Deprecated since 3.40. Use {@link #removePropertyChangeListener} instead.
* See also {@link org.openide.cookies.EditorCookie.Observable}.
*/
public synchronized void removeChangeListener (ChangeListener l) {
if (listeners != null)
listeners.remove (l);
}
// Position management methods
/** Create a position reference for the given offset.
* The position moves as the document is modified and
* reacts to closing and opening of the document.
*
* @param offset the offset to create position at
* @param bias the Position.Bias for new creating position.
* @return position reference for that offset
*/
public final PositionRef createPositionRef (int offset, Position.Bias bias) {
return new PositionRef (getPositionManager (), offset, bias);
}
//
// Methods that can be overriden by subclasses
//
/** Allows subclasses to create their own version
* of <code>CloneableEditor</code> component.
* @return the {@link CloneableEditor} for this support
*/
protected CloneableEditor createCloneableEditor () {
return new CloneableEditor (this);
}
/** Initialize the editor. This method is called after the editor component
* is deserialized and also when the component is created. It allows
* the subclasses to annotate the component with icon, selected nodes, etc.
*
* @param editor the editor that has been created and should be annotated
*/
protected void initializeCloneableEditor (CloneableEditor editor) {
}
/** Create an undo/redo manager.
* This manager is then attached to the document, and listens to
* all changes made in it.
* <P>
* The default implementation simply uses <code>UndoRedo.Manager</code>.
*
* @return the undo/redo manager
*/
protected UndoRedo.Manager createUndoRedoManager () {
return new UndoRedo.Manager ();
}
/**
* Actually write file data to an output stream from an editor kit's document.
* Called during a file save by {@link #saveDocument}.
* <p>The default implementation just calls {@link EditorKit#write(OutputStream, Document, int, int) EditorKit.write(...)}.
* Subclasses could override this to provide support for persistent guard blocks, for example.
* @param doc the document to write from
* @param kit the associated editor kit
* @param stream the open stream to write to
* @throws IOException if there was a problem writing the file
* @throws BadLocationException should not normally be thrown
* @see #loadFromStreamToKit
*/
protected void saveFromKitToStream (StyledDocument doc, EditorKit kit, OutputStream stream) throws IOException, BadLocationException {
kit.write(stream, doc, 0, doc.getLength());
}
/**
* Actually read file data into an editor kit's document from an input stream.
* Called during a file load by {@link #prepareDocument}.
* <p>The default implementation just calls {@link EditorKit#read(InputStream, Document, int) EditorKit.read(...)}.
* Subclasses could override this to provide support for persistent guard blocks, for example.
* @param doc the document to read into
* @param stream the open stream to read from
* @param kit the associated editor kit
* @throws IOException if there was a problem reading the file
* @throws BadLocationException should not normally be thrown
* @see #saveFromKitToStream
*/
protected void loadFromStreamToKit (StyledDocument doc, InputStream stream, EditorKit kit) throws IOException, BadLocationException {
kit.read(stream, doc, 0);
}
/** Reload the document in response to external modification.
* @return task that reloads the document. It can be also obtained
* by calling <tt>prepareDocument()</tt>.
*/
protected Task reloadDocument() {
synchronized (getLock ()) {
if (doc != null) {
// UndoManager must be detached from document here because it will be attached in loadDocument()
doc.removeUndoableEditListener (getUndoRedo ());
// Remember caret positions in all opened panes
final JEditorPane[] panes = getOpenedPanes();
final int[] carets;
if (panes != null) {
carets = new int[panes.length];
for(int i = 0; i < panes.length; i++) {
carets[i] = panes[i].getCaretPosition();
}
} else {
carets = new int[0];
}
prepareTask = null; // make sure new loading will occur
final Task docLoadTask = prepareDocument(true);
docLoadTask.addTaskListener(
new TaskListener() {
public void taskFinished (Task task) {
//Bugfix #12338: This Swing code replanned to AWT thread
SwingUtilities.invokeLater(new Runnable() {
public void run () {
if (panes != null) {
for (int i = 0; i < panes.length; i++) {
// #26407 Adjusts caret position,
// (reloaded doc could be shorter).
int textLength = panes[i].getText().length();
if(carets[i] > textLength) {
carets[i] = textLength;
}
panes[i].setCaretPosition(carets[i]);
}
}
getUndoRedo().discardAllEdits(); // reset undo manager
// Insert before-save undo event to enable unmodifying undo
getUndoRedo().undoableEditHappened(
new UndoableEditEvent(
CloneableEditorSupport.this,
new BeforeSaveEdit(lastSaveTime)
)
);
notifyUnmodified ();
updateLineSet(true);
}
});
docLoadTask.removeTaskListener(this);
}
}
);
return docLoadTask;
}
}
return prepareDocument();
}
/** Creates editor kit for this source.
* @return editor kit
*/
protected EditorKit createEditorKit () {
if (kit != null) return kit;
if (mimeType != null) {
kit = JEditorPane.createEditorKitForContentType (mimeType);
} else {
String defaultMIMEType = env ().getMimeType ();
kit = JEditorPane.createEditorKitForContentType (defaultMIMEType);
}
if (isDumbKit (kit)) {
kit = JEditorPane.createEditorKitForContentType ("text/plain"); // NOI18N
}
if (isDumbKit (kit)) {
kit = new PlainEditorKit ();
}
return kit;
}
/** Is this a useless default kit?
* @param kit the kit to test
* @return true if so
*/
private boolean isDumbKit (EditorKit kit) {
if (kit == null) return true;
String clazz = kit.getClass ().getName ();
return (clazz.equals ("javax.swing.text.DefaultEditorKit") || // NOI18N
clazz.equals ("javax.swing.JEditorPane$PlainEditorKit") || // NOI18N
clazz.equals ("javax.swing.text.html.HTMLEditorKit")); // NOI18N
}
/** Method that can be overriden by children to create empty
* styled document or attach additional document properties to it.
*
* @param kit the kit to use
* @return styled document to use
*/
protected StyledDocument createStyledDocument (EditorKit kit) {
StyledDocument sd = createNetBeansDocument (kit.createDefaultDocument ());
sd.putProperty("mimeType", mimeType != null ? mimeType : env().getMimeType()); // NOI18N
return sd;
}
/** Notification method called when the document become unmodified.
* Called after save or after reload of document.
* <P>
* This implementation simply marks the associated
* environement unmodified and updates titles of all components.
*/
protected void notifyUnmodified () {
env.unmarkModified ();
updateTitles ();
}
/** Called when the document is being modified.
* The responsibility of this method is to inform the environment
* that its document is modified. Current implementation
* Just calls env.setModified (true) to notify it about
* modification.
*
* @return true if the environment accepted being marked as modified
* or false if it refused it and the document should still be unmodified
*/
protected boolean notifyModified () {
boolean locked = true;
try {
env.markModified ();
} catch (final UserQuestionException ex) {
synchronized (this) {
if (! this.inUserQuestionExceptionHandler){
this.inUserQuestionExceptionHandler = true;
RequestProcessor.getDefault().post(new Runnable() {
public void run () {
NotifyDescriptor nd = new NotifyDescriptor.Confirmation (ex.getLocalizedMessage ());
Object res = DialogDisplayer.getDefault ().notify (nd);
if (NotifyDescriptor.OK_OPTION.equals(res)) {
try {
ex.confirmed ();
} catch (IOException ex1) {
ErrorManager.getDefault ().notify (ex1);
}
}
synchronized (CloneableEditorSupport.this) {
CloneableEditorSupport.this.inUserQuestionExceptionHandler = false;
}
}
});
}
}
locked = false;
} catch (IOException e) { // locking failed
if (e.getMessage () != e.getLocalizedMessage ()) {
StatusDisplayer.getDefault().setStatusText(e.getLocalizedMessage());
}
locked = false;
}
if (!locked) {
revertUpcomingUndo();
return false;
}
updateTitles ();
return true;
}
/** Resets listening on <code>UndoRedo</code>,
* and in case next undo edit comes, schedules processesing of it.
* Used to revert modification e.g. of document of [read-only] env. */
private void revertUpcomingUndo() {
Listener l = getListener();
l.setUndoTask(createUndoTask());
UndoRedo ur = getUndoRedo();
ur.removeChangeListener(l);
ur.addChangeListener(l);
}
/** Creates <code>Runnable</code> which tries to make one undo. Helper method.
* @see #revertUpcomingUndo */
private Runnable createUndoTask() {
return new Runnable() {
public void run() {
StyledDocument sd = doc;
if(sd == null) {
// #20883, doc can be null(!), doCloseDocument was faster.
return;
}
UndoRedo ur = getUndoRedo();
sd.removeDocumentListener(getListener());
try {
if(ur.canUndo()) {
Toolkit.getDefaultToolkit().beep();
ur.undo();
}
} catch(CannotUndoException cne) {
ErrorManager.getDefault().notify(
ErrorManager.INFORMATIONAL, cne);
} finally {
sd.addDocumentListener(getListener());
}
}
};
}
/** Method that is called when all components of the support are
* closed. The default implementation closes the document.
*
*/
protected void notifyClosed () {
closeDocument();
}
// XXX #25762 [PENDING] Needed protected method to allow subclasses to alter it.
/** Indicates whether the <code>Env</code> is read only. */
boolean isEnvReadOnly() {
return false;
}
/** Allows access to the document without any checking.
*/
final StyledDocument getDocumentHack () {
return doc;
}
/** Getter for data object associated with this
* data object.
*/
DataObject getDataObjectHack () {
return null;
}
// LineSet methods .....................................................................
/** Updates the line set.
* @param clear clear any cached set?
* @return the set
*/
Line.Set updateLineSet (boolean clear) {
synchronized(LOCK_LINE_SET) {
if(lineSet != null && !clear) {
return lineSet;
}
Line.Set oldSet = lineSet;
if (doc == null) {
lineSet = new EditorSupportLineSet.Closed(CloneableEditorSupport.this);
} else {
lineSet = new EditorSupportLineSet(CloneableEditorSupport.this, doc);
}
if(oldSet != null) {
synchronized(oldSet.lines) {
lineSet.lines.putAll(oldSet.lines);
}
}
return lineSet;
}
}
// other public methods ................................................................
/* JST: Commented out
* Set actions for toolbar.
* @param actions list of actions
*
public void setActions (SystemAction[] actions) {
this.actions = actions;
}
/** Utility method which enables or disables listening to modifications
* on asociated document.
* <P>
* Could be useful if we have to modify document, but do not want the
* Save and Save All actions to be enabled/disabled automatically.
* Initially modifications are listened to.
* @param listenToModifs whether to listen to modifications
*
public void setModificationListening (final boolean listenToModifs) {
if (this.listenToModifs == listenToModifs) return;
this.listenToModifs = listenToModifs;
if (doc == null) return;
if (listenToModifs)
doc.addi(getModifL());
else
doc.removeDocumentListener(getModifL());
}
*/
/** Loads the document for this object.
* @param kit kit to use
* @param d original document to load data into
*/
private void loadDocument (EditorKit kit, StyledDocument doc) throws IOException {
Throwable aProblem = null;
try {
InputStream is = new BufferedInputStream(env ().inputStream ());
try {
// read the document
loadFromStreamToKit (doc, is, kit);
} finally {
is.close ();
}
// attach undo/redo manager
doc.addUndoableEditListener (getUndoRedo ());
} catch (IOException ex) {
aProblem = ex;
throw ex;
} catch (Exception e) { // incl. BadLocationException
aProblem = e;
} finally {
if (aProblem != null) {
ErrorManager err = ErrorManager.getDefault ();
err.annotate (aProblem, NbBundle.getMessage (
CloneableEditorSupport.class,
"EXC_LoadDocument", // NOI18N
messageName ()
));
err.notify (aProblem);
}
}
}
/** Closes all opened editors (if the user agrees) and
* flushes content of the document to the file.
*
* @param ask ask whether to save the document or not?
* @return <code>false</code> if the operation is cancelled
*/
protected boolean close (boolean ask) {
if (!super.close (ask)) {
// if not all editors has been closed
return false;
}
notifyClosed ();
return true;
}
/** Clears all data from memory.
*/
private void closeDocument () {
for (;;) {
Task prep;
synchronized (getLock()) {
if (doc == null) {
return;
}
if (loadTask == null) {
return;
}
prep = prepareTask;
if (prep == null) {
return;
}
if (prep.isFinished ()) {
doCloseDocument ();
return;
}
}
/* Wait for loading task to be finished
* so that the document etc. stays valid
* during the load operation.
*/
prep.waitFinished();
}
}
/** Is called under getLock () to close the document.
*/
private void doCloseDocument () {
loadTask = null;
prepareTask = null;
// notifies the support that
env ().removePropertyChangeListener(getListener());
notifyUnmodified ();
if (doc != null) {
getUndoRedo().discardAllEdits();
doc.removeUndoableEditListener (getUndoRedo ());
doc.removeDocumentListener(getListener());
}
if (positionManager != null) {
positionManager.documentClosed ();
fireDocumentChange(doc, true);
}
doc = null;
kit = null;
updateLineSet (true);
}
/** Handles the actual reload of document.
* @param doReload false if we should first ask the user
*/
private void checkReload(boolean doReload) {
StyledDocument doc = this.doc;
if (doc == null) {
return;
}
if (!doReload && !reloadDialogOpened) {
String msg = NbBundle.getMessage (CloneableEditorSupport.class,
"FMT_External_change", // NOI18N
doc.getProperty (javax.swing.text.Document.TitleProperty)
);
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(msg, NotifyDescriptor.YES_NO_OPTION);
reloadDialogOpened = true;
try {
Object ret = DialogDisplayer.getDefault().notify(nd);
if (NotifyDescriptor.YES_OPTION.equals(ret)) {
doReload = true;
}
} finally {
reloadDialogOpened = false;
}
}
if (doReload) {
//Bugfix #9612: Call of reloadDocument() is now posted to
//RequestProcessor
RequestProcessor.getDefault().post(new Runnable() {
public void run () {
reloadDocument();
}
});
}
}
/** Creates netbeans document for a given document.
* @param d document to use as underlaying one
* @return styled document that could support Guarded.ATTRIBUTE
*/
private static StyledDocument createNetBeansDocument (Document d) {
if (d instanceof StyledDocument) {
return (StyledDocument)d;
} else {
// create filter
return new FilterDocument (d);
}
}
private final void fireDocumentChange(StyledDocument document, boolean closing) {
fireStateChangeEvent(document, closing);
firePropertyChange(EditorCookie.Observable.PROP_DOCUMENT, null, null);
}
/** Fires a status change event to all listeners. */
private final void fireStateChangeEvent(StyledDocument document, boolean closing) {
if (listeners != null) {
EnhancedChangeEvent event = new EnhancedChangeEvent(this, document, closing);
HashSet s;
synchronized (this) {
s = ((HashSet)listeners.clone ());
}
Iterator it = s.iterator ();
while (it.hasNext ()) {
ChangeListener l = (ChangeListener) it.next();
l.stateChanged(event);
}
}
}
/** Updates titles of all editors.
*/
protected void updateTitles () {
Enumeration en = allEditors.getComponents ();
while (en.hasMoreElements()) {
Object o = en.nextElement();
if (o instanceof CloneableEditor) {
CloneableEditor e = (CloneableEditor)o;
e.updateName();
}
}
}
// #18981. There could happen a thing also another class type
// of CloneableTopCoponent then CloneableEditor could be in allEditors.
/** Opens a <code>CloneableEditor</code> component. */
private CloneableEditor openEditorComponent() {
synchronized (getLock()) {
CloneableEditor ce = getAnyEditor();
if(ce != null) {
ce.open();
return ce;
} else {
// no opened editor
String msg = messageOpening ();
if (msg != null) {
StatusDisplayer.getDefault().setStatusText(msg);
}
// initializes the document if not initialized
prepareDocument();
ce = createCloneableEditor ();
initializeCloneableEditor(ce);
ce.setReference(allEditors);
ce.open();
msg = messageOpened ();
if (msg == null) {
msg = ""; // NOI18N
}
StatusDisplayer.getDefault().setStatusText(msg);
return ce;
}
}
}
/** If one or more editors are opened finds one.
* @return an editor or null if none is opened
*/
CloneableEditor getAnyEditor () {
CloneableTopComponent ctc;
ctc = allEditors.getArbitraryComponent();
if(ctc == null) {
return null;
}
if(ctc instanceof CloneableEditor) {
return (CloneableEditor)ctc;
} else {
Enumeration en = allEditors.getComponents();
while(en.hasMoreElements()) {
Object o = en.nextElement();
if(o instanceof CloneableEditor) {
return (CloneableEditor)o;
}
}
return null;
}
}
/** Forcibly create one editor component. Then set the caret
* to the given position.
* @param pos where to place the caret
* @return always non-<code>null</code> editor
*/
final CloneableEditor openAt(final PositionRef pos, final int column) {
final CloneableEditor e = openEditorComponent();
final Task t = prepareDocument ();
e.open();
e.requestVisible();
class Selector implements TaskListener, Runnable {
public void taskFinished (org.openide.util.Task t2) {
javax.swing.SwingUtilities.invokeLater (this);
t2.removeTaskListener (this);
}
public void run () {
// #25435. Pane can be null.
JEditorPane ePane = e.pane;
if(ePane == null) {
return;
}
Caret caret = ePane.getCaret();
if(caret == null) {
return;
}
int offset;
if (column >= 0) {
javax.swing.text.Element el = NbDocument.findLineRootElement (getDocument ());
el = el.getElement (el.getElementIndex (pos.getOffset ()));
offset = el.getStartOffset () + column;
if (offset > el.getEndOffset ()) {
offset = el.getEndOffset ();
}
} else {
offset = pos.getOffset ();
}
caret.setDot(offset);
}
}
t.addTaskListener (new Selector ());
return e;
}
/** Access to lock on operations on the support
*/
Object getLock () {
return allEditors;
}
/** Accessor to the <code>Listener</code> instance, lazy created on demand.
* The instance serves as a listener on document, environment
* and also provides document initialization task for this support.
* @see Listener */
private Listener getListener () {
// Should not need to lock; it is always first
// called within a synchronized(getLock()) block anyway.
if(listener == null) {
listener = new Listener();
}
return listener;
}
/** Default editor kit.
*/
private static final class PlainEditorKit extends DefaultEditorKit
implements ViewFactory {
static final long serialVersionUID =-5788777967029507963L;
PlainEditorKit() {}
/** @return cloned instance
*/
public Object clone () {
return new PlainEditorKit ();
}
/** @return this (I am the ViewFactory)
*/
public ViewFactory getViewFactory() {
return this;
}
/** Plain view for the element
*/
public View create(Element elem) {
return new WrappedPlainView(elem);
}
/** Set to a sane font (not proportional!). */
public void install (JEditorPane pane) {
super.install (pane);
pane.setFont (new Font ("Monospaced", Font.PLAIN, pane.getFont().getSize() + 1)); //NOI18N
}
}
/** The listener that this support uses to communicate with
* document, environment and also temporarilly on undoredo.
*/
private final class Listener extends Object
implements ChangeListener, DocumentListener, PropertyChangeListener, Runnable {
Listener() {}
/** Stores exception from loadDocument, can be set in run method */
private IOException loadExc;
/** Stores temporarilly undo task for reverting prohibited changes.
* @see CloneableEditorSupport#createUndoTask */
private Runnable undoTask;
/** Returns exception from loadDocument, caller thread can check
* it after load task finishes. Returns null if no exception happened.
* It resets loadExc to null. */
public IOException checkLoadException() {
IOException ret = loadExc;
loadExc = null;
return ret;
}
/** Sets undo task used to revert prohibited change. */
public void setUndoTask(Runnable undoTask) {
this.undoTask = undoTask;
}
/** Schedules reverting(undoing) of prohibited change.
* Implements <code>ChangeListener</code>.
* @see #revertUpcomingUndo */
public void stateChanged(ChangeEvent evt) {
getUndoRedo().removeChangeListener(this);
SwingUtilities.invokeLater(undoTask);
undoTask = null;
}
/** Gives notification that an attribute or set of attributes changed.
* @param ev event describing the action
*/
public void changedUpdate(DocumentEvent ev) {
//modified(); (bugfix #1492)
}
/** Gives notification that there was an insert into the document.
* @param ev event describing the action
*/
public void insertUpdate(DocumentEvent ev) {
notifyModified ();
}
/** Gives notification that a portion of the document has been removed.
* @param ev event describing the action
*/
public void removeUpdate(DocumentEvent ev) {
notifyModified ();
}
/** Listener to changes in the Env.
*/
public void propertyChange(PropertyChangeEvent ev) {
if (Env.PROP_TIME.equals (ev.getPropertyName ())) {
// empty new value means to force reload all the time
final Date time = (Date)ev.getNewValue ();
if (lastSaveTime != -1
&& (time == null || time.getTime () > lastSaveTime)
) {
//#32777 - set externallyModified to true because file was externally modified
externallyModified = true;
// post in AWT event thread because of possible dialog popup
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
checkReload(time == null || !isModified());
}
}
);
}
}
if (Env.PROP_MODIFIED.equals(ev.getPropertyName())) {
CloneableEditorSupport.this.firePropertyChange(EditorCookie.Observable.PROP_MODIFIED, null, null);
}
}
/** Initialization of the document.
*/
public void run () {
synchronized (getLock ()) {
/* Remove existing listener before running the loading task
* This should prevent firing of insertUpdate() during load (or reload)
* which can prevent dedloks that sometimes occured during file reload.
*/
doc.removeDocumentListener(getListener());
try {
loadExc = null;
LOCAL_LOAD_TASK.set(Boolean.TRUE);
loadDocument (kit, doc);
} catch (IOException e) {
loadExc = e;
} finally {
LOCAL_LOAD_TASK.set(null);
}
// opening the document, inform position manager
getPositionManager ().documentOpened (doc);
// create new description of lines
updateLineSet (true);
lastSaveTime = System.currentTimeMillis();
// Insert before-save undo event to enable unmodifying undo
getUndoRedo().undoableEditHappened(
new UndoableEditEvent(this, new BeforeSaveEdit(lastSaveTime)));
// Start listening on changes in document
doc.addDocumentListener(getListener());
}
}
}
//
// Interfaces to abstract away from the DataSystem and FileSystem level
//
/** Interface for providing data for the support and also
* locking the source of data.
*/
public static interface Env extends CloneableOpenSupport.Env {
/** property that is fired when time of the data is changed */
public static final String PROP_TIME = "time"; // NOI18N
/** Obtains the input stream.
* @exception IOException if an I/O error occures
*/
public InputStream inputStream () throws IOException;
/** Obtains the output stream.
* @exception IOException if an I/O error occures
*/
public OutputStream outputStream () throws IOException;
/** The time when the data has been modified
*/
public Date getTime ();
/** Mime type of the document.
* @return the mime type to use for the document
*/
public String getMimeType ();
}
/** Generic undoable edit that delegates to the given undoable edit. */
private class FilterUndoableEdit implements UndoableEdit {
protected UndoableEdit delegate;
FilterUndoableEdit() {
}
public void undo() throws CannotUndoException {
if (delegate != null) {
delegate.undo();
}
}
public boolean canUndo() {
if (delegate != null) {
return delegate.canUndo();
} else {
return false;
}
}
public void redo() throws CannotRedoException {
if (delegate != null) {
delegate.redo();
}
}
public boolean canRedo() {
if (delegate != null) {
return delegate.canRedo();
} else {
return false;
}
}
public void die() {
if (delegate != null) {
delegate.die();
}
}
public boolean addEdit(UndoableEdit anEdit) {
if (delegate != null) {
return delegate.addEdit(anEdit);
} else {
return false;
}
}
public boolean replaceEdit(UndoableEdit anEdit) {
if (delegate != null) {
return delegate.replaceEdit(anEdit);
} else {
return false;
}
}
public boolean isSignificant() {
if (delegate != null) {
return delegate.isSignificant();
} else {
return true;
}
}
public String getPresentationName() {
if (delegate != null) {
return delegate.getPresentationName();
} else {
return ""; // NOI18N
}
}
public String getUndoPresentationName() {
if (delegate != null) {
return delegate.getUndoPresentationName();
} else {
return ""; // NOI18N
}
}
public String getRedoPresentationName() {
if (delegate != null) {
return delegate.getRedoPresentationName();
} else {
return ""; // NOI18N
}
}
}
/** Undoable edit that is put before the savepoint. Its replaceEdit()
* method will consume and wrap the edit that precedes the save.
* If the edit is added to the begining of the queue then
* the isSignificant() implementation guarantees that the edit
* will not be removed from the queue.
* When redone it marks the document as not modified.
*/
private class BeforeSaveEdit extends FilterUndoableEdit {
private long saveTime;
BeforeSaveEdit(long saveTime) {
this.saveTime = saveTime;
}
public boolean replaceEdit(UndoableEdit anEdit) {
if (delegate == null) {
delegate = anEdit;
return true; // signal consumed
}
return false;
}
public boolean addEdit(UndoableEdit anEdit) {
if (!(anEdit instanceof BeforeModificationEdit)) {
/* UndoRedo.addEdit() must not be done lazily
* because the edit must be "inserted" before the current one.
*/
getUndoRedo().addEdit(new BeforeModificationEdit(saveTime, anEdit));
return true;
}
return false;
}
public void redo() {
super.redo();
if (saveTime == lastSaveTime) {
notifyUnmodified();
}
}
public boolean isSignificant() {
return (delegate != null);
}
}
/** Edit that is created by wrapping the given edit.
* When undone it marks the document as not modified.
*/
private class BeforeModificationEdit extends FilterUndoableEdit {
private long saveTime;
BeforeModificationEdit(long saveTime, UndoableEdit delegate) {
this.saveTime = saveTime;
this.delegate = delegate;
}
public boolean addEdit(UndoableEdit anEdit) {
if (delegate == null) {
delegate = anEdit;
return true;
}
return false;
}
public void undo() {
super.undo();
if (saveTime == lastSaveTime) {
notifyUnmodified();
}
}
}
}