-
Notifications
You must be signed in to change notification settings - Fork 1
DevFaqEditorTopComponent
This entry is about creating non-text-editor (e.g. graphical) editors for files or other objects. If you want a text editor, NetBeans has a lot of built-in support for text editors and you will probably want to use DataEditorSupport.create()
and its relatives (hint: New > File Type will get you basic text editor support which you can build on).
If you want to create some other kind of editor, you will probably want to start by creating a non-singleton TopComponent - a logical window, or tab, that can be opened in the editor area and can show your file or object in some way.
Our editor component will be fairly simple. It will have two constructors, one which takes a DataObject
(the file) and one which has no arguments:
public MyEditor() {
}
MyEditor(FooDataObject ob) throws IOException {
init(ob);
}
and it will have an initialization method. In our case, since this is a simple example, we will use a JTextArea
. Our DataObject
subclass will have a method setContent(String)
which is passed the updated text if the user types into the text area. The DataObject
will take care of marking the file modified and saving it when the user invokes the Save action. So we will just pass the text the user changed to the DataObject
and update the tab name of the editor to show if the file is modified in-memory or not:
void init(final FooDataObject file) throws IOException {
associateLookup(file.getLookup());
setDisplayName(file.getName());
setLayout(new BorderLayout());
add(new JLabel(getDisplayName()), BorderLayout.NORTH);
//If you expect large files, load the file in a background thread
//and set the field's text under its Document's lock
final JTextField field = new JTextField(file.getPrimaryFile().asText());
add(field, BorderLayout.CENTER);
field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
changedUpdate(e);
}
@Override
public void removeUpdate(DocumentEvent e) {
changedUpdate(e);
}
@Override
public void changedUpdate(DocumentEvent e) {
FooDataObject foo = getLookup().lookup(FooDataObject.class);
foo.setContent(field.getText());
}
});
file.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (DataObject.PROP_MODIFIED.equals(evt.getPropertyName())) {
//fire a dummy event
setDisplayName(Boolean.TRUE.equals(evt.getNewValue()) ? file.getName() + "*" : file.getName());
}
}
});
}
As of NetBeans 6.8, modified files are usually shown with a boldface tab name, so for consistency we should too:
@Override
public String getHtmlDisplayName() {
DataObject dob = getLookup().lookup(DataObject.class);
if (dob != null && dob.isModified()) {
return "<html>*" + dob.getName();
}
return super.getHtmlDisplayName();
}
The persistence code (described here) will save the file’s path on disk, and on restart, reinitialize the editor (if the file still exists).
The code to do this is actually quite simple - it can be boiled down to loading:
init (DataObject.find(FileUtil.toFileObject(FileUtil.normalizeFile(new File(properties.getProperty("path"))));
and saving
properties.setProperty (FileUtil.toFile(dataObject.getPrimaryFile()).getAbsolutePath());
That is, all we are doing is saving a path on shutdown, and on restart looking that file up, transforming it into a NetBeans FileObject, and initializing with the DataObject for that. It just happens that we have to handle a few corner cases involving missing files and checked exceptions:
-
The file never really existed on disk (editing a template)
-
The file was deleted
-
The file cannot be read for some reason
So our persistence code looks like this:
private static final String KEY_FILE_PATH = "path";
void readProperties(java.util.Properties p) {
String path = p.getProperty(KEY_FILE_PATH);
try {
File f = new File(path);
if (f.exists()) {
FileObject fileObject = FileUtil.toFileObject(FileUtil.normalizeFile(f));
DataObject dob = DataObject.find(fileObject);
//A DataObject always has itself in its Lookup, so do this to cast
FooDataObject fooDob = dob.getLookup().lookup(FooDataObject.class);
if (fooDob == null) {
throw new IOException("Wrong file type");
}
init(fooDob);
//Ensure Open does not create another editor by telling the DataObject about this editor
fooDob.editorInitialized(this);
} else {
throw new IOException(path + " does not exist");
}
} catch (IOException ex) {
//Could not load the file for some reason
throw new IllegalStateException(ex);
}
}
void writeProperties(java.util.Properties p) {
FooDataObject dob = getLookup().lookup(FooDataObject.class);
if (dob != null) {
File file = FileUtil.toFile(dob.getPrimaryFile());
if (file != null) { //could be a virtual template file not really on disk
String path = file.getAbsolutePath();
p.setProperty(KEY_FILE_PATH, path);
}
}
}
The skeleton of our DataObject class is generated from the New > File Type template - this includes registering our DataObject subclass and associating it with a file extension. What we need to do is
-
Modify it so that Open on it will open our editor TopComponent, not a normal text editor
-
We will implement our own subclass of
OpenCookie
, which can create and open an instance of our editor, and remember and reuse that editor on subsequent invocations -
Modify it so that we can pass the text the user typed to it, and it will mark itself modified and become savable (causing File > Save and File > Save All to become enabled)
-
We will implement the setContent(String) method to
-
Make a
SaveCookie
available, which is what the various built-in Save actions operate on -
Call
DataObject.setModified()
—this guarantees that the user will be given a chance to save the file if they shut down the application before saving.
public class FooDataObject extends MultiDataObject { private String content; private final Saver saver = new Saver(); public FooDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException { super(pf, loader); CookieSet cookies = getCookieSet(); cookies.add(new Opener()); } @Override public Lookup getLookup() { return getCookieSet().getLookup(); } synchronized void setContent(String text) { this.content = text; if (text != null) { setModified(true); getCookieSet().add(saver); } else { setModified(false); getCookieSet().remove(saver); } } void editorInitialized(MyEditor ed) { Opener op = getLookup().lookup(Opener.class); op.editor = ed; } private class Opener implements OpenCookie { private MyEditor editor; @Override public void open() { if (editor == null) { try { editor = new MyEditor(FooDataObject.this); } catch (IOException ex) { Exceptions.printStackTrace(ex); } } editor.open(); editor.requestActive(); } } private class Saver implements SaveCookie { @Override public void save() throws IOException { String txt; synchronized (FooDataObject.this) { //synchronize access to the content field txt = content; setContent(null); } FileObject fo = getPrimaryFile(); OutputStream out = new BufferedOutputStream(fo.getOutputStream()); PrintWriter writer = new PrintWriter(out); try { writer.print(txt); } finally { writer.close(); out.close(); } } } }
A few things may be worth considering if you want to use code like this in a production environment:
-
File loading should usually happen on a background thread - put up some sort of progress bar inside the editor component, and replace its contents on the event thread after the load is completed - use RequestProcessor and EventQueue.invokeLater().
-
If it is expected that there will be a lot of FooDataObjects, Opener should instead keep a WeakReference to the editor component so that closed editors can be garbage collected. The following other changes would need to be made:
-
MyEditor should implement PropertyChangeListener directly
-
Use WeakListeners.propertyChange (this, file) rather than directly adding the editor as a listener to the DataObject
-
As of 6.9, the
Openable
interface is preferred toOpenCookie
; a similarSavable
interface is probably on the horizon to replaceSaveCookie
-
The DataObject’s lookup could alternately be implemented using ProxyLookup and AbstractLookup and this will probably be the preferred way in the future
The content in this page was kindly donated by Oracle Corp. to the Apache Software Foundation.
This page was exported from http://wiki.netbeans.org/DevFaqEditorTopComponent , that was last modified by NetBeans user Tboudreau on 2010-03-13T07:34:06Z.
NOTE: This document was automatically converted to the AsciiDoc format on 2018-01-26, and needs to be reviewed.
Apache NetBeans is an effort undergoing incubation at The Apache Software Foundation (ASF).
Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision making process have stabilized in a manner consistent with other successful ASF projects.
While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.
This wiki is an experiment pending Apache NetBeans Community approval.