| // |
| // 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 "<html>*" + 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()`—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. |