blob: b85e0b6b8301786e820068477ad2a0b8cf21e33d [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.
//
= NetBeans Selection Management Tutorial I—Using a TopComponent's Lookup
:jbake-type: platform_tutorial
:jbake-tags: tutorials
:jbake-status: published
:syntax: true
:source-highlighter: pygments
:toc: left
:toc-title:
:icons: font
:experimental:
:description: NetBeans Selection Management Tutorial I—Using a TopComponent's Lookup - Apache NetBeans
:keywords: Apache NetBeans Platform, Platform Tutorials, NetBeans Selection Management Tutorial I—Using a TopComponent's Lookup
This tutorial covers how to write components that provide a "selected object" and how to write components that update themselves as the global selection changes.
Optionally, for troubleshooting purposes, you can link:http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=3146[download the completed sample].
== Introduction to Selection
"Selection" is an important concept for any non-trivial application. NetBeans has two basic concepts of selection—the contents of the focused ` link:https://netbeans.apache.org/wiki/devfaqwindowstopcomponent[TopComponent]`'s ` link:https://netbeans.apache.org/wiki/devfaqlookup[Lookup]`, and the focused `TopComponent`'s link:https://netbeans.apache.org/wiki/devfaqwhatisanode[ activated `Node`(s)]. Here you will deal only with the Lookup portion of selection—doing more advanced things will be covered in a later tutorial.
Selection is used to make possible such things as context sensitive actions (actions that are enabled or disabled depending on what is displayed), and palette windows such as the Property Sheet or Navigator components in the IDE, which each display some aspect of whatever is selected.
Basically, each `TopComponent` has a bag of objects that it can put things into, and which other code is able to query. That bag of objects is its _Lookup_—essentially a Map where the keys are class objects and the values are objects that extend or implement the key-class. The thing that makes this approach tremendously useful is the ability to use this mechanism to decouple the components that provide some object and the components that consume those objects—so they can be implemented in separate modules, or new editors for old objects can be provided and the rest of the system will continue to work transparently.
== Creating the Module Suite and Projects
The example for this tutorial will contain three modules, contained within a module suite, as illustrated below:
image::images/selection-1_project-setup-60.png[]
Start by creating the module suite to contain all three modules:
[start=1]
1. Choose File > New Project (Ctrl-Shift-N). Under Categories, select NetBeans Modules. Under Projects, select Module Suite Project and click Next.
[start=2]
1. In the Name and Location panel, type ``SelectionSuite`` in Project Name. Change the Project Location to any directory on your computer. Click Finish.
[start=3]
1. Choose File > New Project (Ctrl-Shift-N) again. Under Categories, select NetBeans Modules. Under Projects, select Module Project and click Next.
[start=4]
1. In the Name and Location panel, type ``MyAPI`` in Project Name. The default in the wizard should be to create the module underneath the directory where you just created the suite, which is fine. Click Next.
[start=5]
1. In the Basic Module Configuration panel, replace ``yourorg`` in Code Name Base with ``myorg`` , so that the whole code name base is ``org.myorg.myapi`` . Add spaces to the default Module Display Name, so that it is changed to ``My API`` . Leave the location of the localizing bundle, so that they will be stored in a package with the name ``org/myorg/myapi`` . Do not let the IDE create an XML layer file, because you will not need it in this tutorial. Click Finish.
[start=6]
1. You're going to create two more modules now—follow the same steps above, but using the names "MyEditor" and "MyViewer". The reason you are creating three modules will be clear as you continue.
== Creating an API and Setting Up Dependencies
What you're going to do here is create a trivial API class. In the real world, such an API might represent files or some other kind of data that is being modelled programmatically. For the purposes of this tutorial it will suffice to have simple objects with a couple of properties.
[start=1]
1. Right click the `org.myorg.myapi` package and choose New > Java Class, as shown below:
image::images/selection-1_create-java-class-60.png[]
[start=2]
1. Name the class `APIObject`.
[start=3]
1. Replace the default code with the following:
[source,java]
----
public final class APIObject {
private final Date date = new Date();
private static int count = 0;
private final int index;
public APIObject() {
index = count++;
}
public Date getDate() {
return date;
}
public int getIndex() {
return index;
}
public String toString() {
return index + " - " + date;
}
}
----
This will be all of the code that this module contains. As you can see, each time a new instance of `APIObject` is created, a counter is incremented—so there will be some unique attribute to each instance of `APIObject`.
[start=4]
1. The next step is to have your API module export the `org.myorg.myapi` package so other modules can see classes in it. Right click the My API project and choose Properties.
[start=5]
1.
In the API Versioning page in the Project Properties dialog box, check the checkbox for `org.myorg.api` in the Public Packages list, shown below:
image::images/selection-1_export-package-60.png[]
[start=6]
1. Now you need to set up some dependencies between your modules. The other two modules, My Editor and My Viewer, will use the `APIObject` class, so each of them needs to say that they _depend on_ the API module. For each of the other module projects in turn, right click the project node and choose Properties.
[start=7]
1.
In the Libraries page of each Project Properties dialog box, click the Add Dependency button. In the dialog box that pops up, type `APIObject`—there should be only one match, which is your API module. Select it and click OK to add the dependency.
image::images/selection-1_add-deps-60.png[]
== Creating the Viewer Component
Now you will create a singleton component that will track if there is an `APIObject` available in the global selection (i.e., if the focused `TopComponent` has one in its Lookup). If there is one, it will display some data about it. One common use case for this sort of thing is creating master/detail views.
A "singleton component" is a component like the Projects window in the NetBeans IDE, or the Property Sheet or the Navigator—a component that there is only ever one of in the system. The Window Component wizard will automatically generate all of the code needed to create such a singleton component—you just have to use the form designer or write code to provide the contents of your singleton component.
[start=1]
1. Right click the `org.myorg.myviewer` package and choose New > Other.
[start=2]
1. In the resulting dialog, select Module Development > Window Component and click Next (or press Enter).
[start=3]
1.
On the "Basic Settings" page of the wizard, select `navigator` as the location in which to place your viewer component, and check the checkbox to open the component on startup, as shown below:
image::images/selection-1_new-window-60.png[]
[start=4]
1. Click Next to continue to the "Name, Icon and Location" page of the wizard.
[start=5]
1. On the following page, name the class `MyViewer` and click Finish (or press Enter).
You now have a skeleton `TopComponent`—a singleton component called `MyViewerTopComponent`. Click the editor tab for `MyViewerTopComponent`—the form editor should be visible. You will add two labels to the component, which will display some information about the selected `APIObject` if there is one.
[start=1]
1. Drag two JLabels to the form from the Palette, one below the other.
image::images/selection-1_viewer-form-editor.png[]
Change the text of the first as shown above, so that by default it displays "[nothing selected]".
[start=2]
1. Click the Source button in the editor toolbar to switch to the code editor
[start=3]
1. Modify the signature of the class, so that `MyViewerTopComponent` implements `LookupListener`:
[source,java]
----
public class MyViewerTopComponent extends TopComponent implements LookupListener {
----
[start=4]
1. Right-click in the editor and choose Fix Imports, so that `LookupListener` is imported.
[start=5]
1.
Put the caret in the signature line as shown below. A lightbulb glyph should appear in the editor margin. Press Alt-Enter, and then Enter again when the popup appears with the text "Implement All Abstract Methods". This will add the LookupListener method to your class.
image::images/selection-1_implement-methods.png[]
[start=6]
1. You now have a class that implements `LookupListener`. Now it needs something to listen to. In your case, there is a convenient global Lookup object, which simply proxies the Lookup of whatever component has focus—it can be obtained from the call `Utilities.actionsGlobalContext()`. So rather than tracking what component has focus yourself, you can simply listen to this one global selection lookup, which will fire appropriate changes whenever focus changes. Edit the source code so that it contains the following methods as shown here:
[source,java]
----
private Lookup.Result result = null;
public void componentOpened() {
Lookup.Template tpl = new Lookup.Template (APIObject.class);
result = Utilities.actionsGlobalContext().lookup(tpl);
result.addLookupListener (this);
}
public void componentClosed() {
result.removeLookupListener (this);
result = null;
}
public void resultChanged(LookupEvent lookupEvent) {
Lookup.Result r = (Lookup.Result) lookupEvent.getSource();
Collection c = r.allInstances();
if (!c.isEmpty()) {
APIObject o = (APIObject) c.iterator().next();
jLabel1.setText (Integer.toString(o.getIndex()));
jLabel2.setText (o.getDate().toString());
} else {
jLabel1.setText("[no selection]");
jLabel2.setText ("");
}
}
----
`componentOpened()` is called whenever the component is made visible by the window system; `componentClosed()` is called whenever the user clicks the X button on its tab to close it. So whenever the component is showing, you want it to be tracking the selection—which is what the above code does.
The `resultChanged()` method is your implementation of `LookupListener`. Whenever the selected `APIObject` changes, it will update the two `JLabel`s you put on the form.
== Creating the Editor Component
Now you need something to actually provide instances of `APIObject`, for this code to be of any use. Fortunately this is quite simple.
You will create another `TopComponent`, this time, one that opens in the editor area and offers an instance of `APIObject` from its `Lookup`. You _could_ use the Window Component template again, but that template is designed for creating singleton components, rather than components there can be many of. So you will simply create a `TopComponent` subclass without the template, and an action which will open additional ones.
[start=1]
1. You will need to add three dependencies to the My Editor module for it to be able to find the classes you will be using. Right click the My Editor project and choose Properties. On the Library page of the Project Properties dialog box, click the Add Dependency button, and type `TopComponent`. The dialog should automatically suggest setting a dependency on the Window System API. Do the same thing for `Lookups` (Utilities API).
[start=2]
1. Right-click the `org.myorg.myeditor` package in the My Editor project, and choose New > JPanel Form.
[start=3]
1. Name it "MyEditor", and finish the wizard.
[start=4]
1. When the form editor opens, drop two JTextFields on the form, one above the other. On the property sheet, set the Editable property (checkbox) to `false` for each one.
[start=5]
1. Click the Source button in the editor toolbar to switch to the code editor.
[start=6]
1. Change the signature of `MyEditor` to extends `TopComponent` instead of `javax.swing.JPanel`:
[source,java]
----
public class MyEditor extends TopComponent {
----
[start=7]
1. Add the following code to the constructor of `MyEditor`:
[source,java]
----
APIObject obj = new APIObject();
associateLookup (Lookups.singleton (obj));
jTextField1.setText ("APIObject #" + obj.getIndex());
jTextField2.setText ("Created: " + obj.getDate());
setDisplayName ("MyEditor " + obj.getIndex());
----
Right-click in the editor and choose Fix Imports.
The line `associateLookup (Lookups.singleton (obj));` will create a Lookup that contains only one object—the new instance of `APIObject`—and assign that `Lookup` to be what is returned by `MyEditor.getLookup()`. While this is an artificial example, you can imagine how `APIObject` might represent a file, an entity in a database or anything else you might want to edit or view. Probably you can also imagine one component that allowed you to select or edit multiple unique instances of `APIObject`—that will be the subject of the next tutorial.
To make your editor component at least somewhat interesting (though it doesn't actually edit anything), you set the text fields' values to values from the `APIObject`, so you have something to display.
== Opening Editor Components
Now you need a way to open `MyEditor` components in the editor area, so that there will be something to show. To do anything meaningful with selection, you will need more than one editor so that there is more than one `APIObject` to track. Since you will want multiple editors, you need a simple action on the main menu which will create and open another instance of `MyEditor` in the window system (as opposed to what the Window Component template would create for us, which is an action that always looks up a singleton component such as the Navigator or Property Sheet components in the IDE).
[start=1]
1. Right click the `org.myorg.myeditor` package and choose New > Other.
[start=2]
1. In the dialog, choose Module Development > Action and click Next.
[start=3]
1. Accept the defaults ("always enabled") and press Next again.
[start=4]
1. On the GUI Registration page, accept the defaults by pressing Next again (this will cause your action to appear at the top of the File menu).
[start=5]
1. On the final page of the wizard, name the action `OpenEditorAction` and set its display name to say "Open Editor".
[start=6]
1. Press Finish to generate the action class.
[start=7]
1. The code editor should now be open over a class called `OpenEditorAction`, which implements `ActionListener`. Add the following code to its `actionPerformed()` method:
[source,java]
----
MyEditor editor = new MyEditor();
editor.open();
editor.requestActive();
----
The above code will simply create a new instance of `MyEditor` (which in turn will create a new instance of `APIObject` and put it in its `Lookup`) and open it in the window system.
== Running the Code
Now you're ready to run the tutorial. Simply right click `SelectionSuite`, the module suite which owns your three modules, and choose Run from the popup menu. When the IDE opens, simply choose File > Open Editor—invoke your action. Do this a couple of times, so that there are several of your editor components open. Your singleton `MyViewer` window should also be open. Notice how the `MyViewer` window's contents change as you click different tabs, as shown here:
image::images/selection-1_result-1-60.png[]
If you click in the Projects window, notice that the text changes to "[No Selection]", as shown below:
image::images/selection-1_result-2-60.png[]
If you do not see the `MyViewer` window, you probably did not check the checkbox in the wizard to open it on system start—simply go to the Window menu and choose Open MyViewer Window to display it.
== So, What's the Point?
You might be wondering what the point of this exercise is—you've just shown that you can handle selection—big deal! The key to the importance of this is the way the code is split into three modules: The My Viewer module knows nothing about the My Editor module—either one can run by itself. They only share a common dependency on My API. That's important—it means two things: 1. My Viewer and My Editor can be developed and shipped independently, and 2. Any module that wants to provide a different sort of editor than My Editor can do so, and the viewer component will work perfectly with it, as long as the replacement editor offers an instance of `APIObject` from its Lookup.
To really picture the value of this, imagine `APIObject` were something much more complex; imagine that `MyEditor` is an image editor, and ` APIObject` represents an image being edited. The thing that's powerful here is that you could replace `MyEditor` with, say, an SVG vector-based editor, and the viewer component (presumably showing attributes of the currently edited image) will work transparently with that new editor. It is this model of doing things that is the reason you can add new tools into the NetBeans IDE that work against Java files, and they will work in different versions of NetBeans, and that you can have an alternate editor (such as the form editor) for Java files and all the components and actions that work against Java files still work when the form editor is used.
This is very much the way NetBeans works with Java and other source files— in their case, the thing that is available from the editor's Lookup is a ` link:https://netbeans.apache.org/wiki/devfaqdataobject[DataObject]`, and components like Navigator and the Property Sheet are simply watching what object is being made available by the focused `TopComponent`.
Another valuable thing about this approach is that often people are migrating existing applications to the NetBeans platform. The object that is part of the data model, in that case, is probably existing, working code that should not be changed in order to integrate it into NetBeans. By keeping the data model's API in a separate module, the NetBeans integration can be kept separate from the core business logic.
== Changing Selected Objects on the Fly
To make it really evident how powerful this approach can be, you'll take one more step, and add a button to your editor component that lets it replace the `APIObject` it has with a new one on the fly.
[start=1]
1. Open `MyEditor` in the form editor (click the Design toolbar button in the editor toolbar), and drag a `JButton` to it.
[start=2]
1. Set the `text` property of the JButton to "Replace".
[start=3]
1. Right click the `JButton` and choose Events > Action > actionPerformed. This will cause the code editor to open with the caret in an event handler method.
[start=4]
1. At the head of the class definition, you will add one final field:
[source,java]
----
public class MyEditor extends TopComponent {
private final InstanceContent content = new InstanceContent();
----
InstanceContent is a class which allows us to modify the content of a Lookup (specifically an instance of `AbstractLookup`) on the fly.
[start=5]
1. Copy all of the lines you added earlier to the constructor to the clipboard, and delete them from the constructor, except for the line beginning "associateLookup...". That line of the constructor should be changed as follows:
[source,java]
----
associateLookup (new AbstractLookup (content));
----
[start=6]
1. You will be using the lines that you put on the clipboard in the action handler for the JButton—so you should run this code once when you first initialize the component. Add the following line to the constructor, after the line above:
[source,java]
----
jButton1ActionPerformed (null);
----
[start=7]
1. Modify the event handler method so it appears as follows, pasting from the clipboard and adding the line at the end:
[source,java]
----
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
APIObject obj = new APIObject();
jTextField1.setText ("APIObject #" + obj.getIndex());
jTextField2.setText ("Created: " + obj.getDate());
setDisplayName ("MyEditor " + obj.getIndex());
content.set(Collections.singleton (obj), null);
}
----
[start=8]
1. Right-click in the editor and choose Fix Imports.
You're now ready to run the suite again. Right click SelectionSuite again and choose Run. Notice how, now, when you click the Replace button, all of the components update, including the instance of `MyViewer`—everything.
image::images/selection-1_first-run-60.png[]
== Providing More Than One Object
This is all well and good for decoupling, but isn't providing this one object from your component a bit like having a `Map` that only contains one key and one value? The answer is, yes, it is like that. Where this technique becomes even more powerful is when you provide multiple objects from multiple APIs.
As an example, it is very common in NetBeans to provide context sensitive actions. A case in point is the built-in `SaveAction` that is part of NetBeans' Actions API. What this action actually does is, it simply listens for the presence of something called `SaveCookie` on the global context— the same way your viewer window listens for `APIObject`. If a `SaveCookie` appears (editors typically add one to their lookup when the content of the file is modified but not yet saved), the action becomes enabled, so the Save toolbar button and menu items become enabled. When the Save action is invoked, it calls `SaveCookie.save()`, which in turn causes the `SaveCookie` to disappear, so the Save action then becomes disabled until a new one appears.
As you may have noticed, context sensitivity was one option in the New Action wizard. The actions currently generated by the wizard actually use a way of doing this that pre-dates `Lookup`; the Lookup-based way of doing such context sensitive actions is described link:https://netbeans.apache.org/wiki/devfaqactioncontextsensitive[in the developer FAQ].
So the pattern in practice is to provide more than just a single object from your component's `Lookup`—different auxilliary components and different actions will be interested in different aspects of the object being edited. These aspects can be cleanly separated into interfaces which those auxilliary components and actions can depend on and listen for.
== Miscellaneous Things Worth Noticing
While not directly related to the topic of this tutorial, it's worth noticing that if you open three `MyEditor` instances, and shut down and restart NetBeans, you end up with three `MyEditor` instances magically appearing on restart. By default, your editors are serialized to disk on shutdown and restored on restart.
If you do not want this behavior, there are two other choices. Override the following method on `MyEditor` to cause editors _never_ to be reopened on restart:
[source,java]
----
public int getPersistenceType() {
return PERSISTENCE_NEVER;
}
----
If you want to persist the components that are open but discard those that have been closed, return `PERSISTENCE_ONLY_OPENED` instead. The default (for backward compatibility reasons) is `PERSISTENCE_ALWAYS`, which is not appropriate for editor-style componenents—it means that even editors that have been closed are kept forever and reloaded on restart.
Note, though, that part of what is serialized to disk is the _location_ of your component in the main window. So singleton `TopComponents`s such as the property sheet, or our viewer component, should use `PERSISTENCE_ALWAYS` - otherwise if they are closed once by the user, the next time they are opened they will appear in the editor area instead of where they are supposed to be.
link:http://netbeans.apache.org/community/mailing-lists.html[Send Us Your Feedback]
== Next Steps
By now you may have noticed that some components have more granular selection logic, and even involve multiple selection. In the link:nbm-selection-2.html[ next tutorial] you will cover how to use the link:https://bits.netbeans.org/dev/javadoc/org-openide-nodes/overview-summary.html[Nodes API] to handle that.