blob: 0799453109408d97fc49d75ba64533ac41179f9a [file] [log] [blame]
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
= DevFaqEditorTopComponent
:jbake-type: wiki
:jbake-tags: wiki, devfaq, needsreview
:jbake-status: published
:keywords: Apache NetBeans wiki DevFaqEditorTopComponent
:description: Apache NetBeans wiki DevFaqEditorTopComponent
:toc: left
:toc-title:
:syntax: true
==== I want to create a TopComponent class to use as an editor, not a singleton
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 `link:http://bits.netbeans.org/dev/javadoc/org-openide-loaders/org/openide/text/DataEditorSupport.html#create(org.openide.loaders.DataObject,%20org.openide.loaders.MultiDataObject.Entry,%20org.openide.nodes.CookieSet)[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 link:DevFaqNonSingletonTopComponents.asciidoc[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 `link:http://bits.netbeans.org/dev/javadoc/org-openide-loaders/org/openide/loaders/DataObject.html[DataObject]` (the file) and one which has no arguments:
[source,java]
----
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:
[source,java]
----
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:
[source,java]
----
@Override
public String getHtmlDisplayName() {
DataObject dob = getLookup().lookup(DataObject.class);
if (dob != null && dob.isModified()) {
return &quot;<html>*&quot; + dob.getName();
}
return super.getHtmlDisplayName();
}
----
The persistence code (described link:DevFaqNonSingletonTopComponents.asciidoc[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:
[source,java]
----
init (DataObject.find(FileUtil.toFileObject(FileUtil.normalizeFile(new File(properties.getProperty("path"))));
----
and saving
[source,java]
----
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 link:DevFaqFileObject.asciidoc[FileObject], and initializing with the link:DevFaqDataObject.asciidoc[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:
[source,java]
----
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);
}
}
----
[source,java]
----
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);
}
}
}
----
==== Implementing A Very Simple DataObject For Our Very Simple Editor
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 `link:http://bits.netbeans.org/dev/javadoc/org-openide-nodes/org/openide/cookies/OpenCookie.html[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 `link:http://bits.netbeans.org/dev/javadoc/org-openide-nodes/org/openide/cookies/SaveCookie.html[SaveCookie]` available, which is what the various built-in Save actions operate on
* Call `DataObject.setModified()`&mdash;this guarantees that the user will be given a chance to save the file if they shut down the application before saving.
[source,java]
----
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();
}
}
}
}
----
==== Caveats For Production Use
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 to `OpenCookie`; a similar `Savable` interface is probably on the horizon to replace `SaveCookie`
* The DataObject's lookup could alternately be implemented link:DevFaqNodesCustomLookup.asciidoc[using ProxyLookup and AbstractLookup] and this will probably be the preferred way in the future
=== Apache Migration Information
The content in this page was kindly donated by Oracle Corp. to the
Apache Software Foundation.
This page was exported from link:http://wiki.netbeans.org/DevFaqEditorTopComponent[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-02-07, and needs to be reviewed.