Abstract: The document describes typical usecases of looks in form of
How Tos. You may either choose the topic you interested in or the document
can be read sequentialy as a tutorial. This document is non-normative.
Contents:
Simple look implementation
1] How to implement simple node using looks
2] How to add children (subnodes)
3] How to make coding easier with DefaultLook
4] How to reflect changes in represented object
5] How to transform "standard objects" (Beans & Nodes)
Operations on Looks
6] How to reuse part of an existing Look - Filtering
7] How to build a look from more looks - Composition
8] How to add features to other looks - Composition II
Declarative registration and exposing APIs
9] How to declare looks using the system filesystem
10] How to allow other modules to influence your view
See also:
The example shows simple look for NetBeans FileObject which provides icon and name:
import java.awt.Image;
import org.openide.filesystems.FileObject;
import org.netbeans.spi.looks.Look;
import org.openide.util.Lookup;
import org.openide.util.Utilities;
public class FileObjectStyle extends Look {
private static Look INSTANCE = new FileObjectStyle( "FILE_OBJECT_STYLE" );
private static Image FILE_IMAGE = Utilities.loadImage( "file.gif" );
private static Image FOLDER_IMAGE = Utilities.loadImage( "folder.gif" );
private FileObjectStyle(String name) {
super( name );
}
public static Look getInstance() {
return INSTANCE;
}
public String getName(Object representedObject, Lookup env) {
return ((FileObject)representedObject).getNameExt();
}
public String getDisplayName(Object representedObject, Lookup env) {
return getName( representedObject, env );
}
public Image getIcon(Object representedObject, int type, Lookup env) {
FileObject fo = (FileObject)representedObject;
return fo.isFolder() ? FOLDER_IMAGE : FILE_IMAGE;
}
}
Notice that we've made the Look rather a factory than a class with
contructor. It is usually enough to have one instance of the Look
in the system so there should be no need for other instances.
Of course there are exceptions from the rule, but you should usualy
rather create static factories than create allways new Looks.
Notice that the following example uses different class for adding children (this is an artifical example just to be used in later examples) it would be absolutely correct to add metods in the above Look subclass.
There are two methods in the look class responsible for managing
node's children. The method isLeaf says whether the node should have any
children at all. If this method returns false. User won't be able to open the
node. (There will be no thumb for opening shown in the tree.) Returning true
from this method will enable opening the node. Still the node may have no children
(A good example is an empty folder, user can open the node but it shows no
childern underneath).
The method responsible for real creation of sub nodes is getChildObjects(...)
has to return the List of represented objects.
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.openide.filesystems.FileObject;
import org.netbeans.spi.looks.Look;
import org.openide.util.Lookup;
public class FileObjectChildren extends Look {
private static final Look INSTANCE = new FileObjectChildren( "FILE_OBJECT_CHILDREN" );
private FileObjectChildren(String name) {
super( name );
}
public static Look getInstance() {
return INSTANCE;
}
public List getChildObjects(Object representedObject, Lookup env) {
FileObject fo = (FileObject)representedObject;
if ( fo.isFolder() ) {
FileObject[] fos = fo.getChildren();
return Arrays.asList( fos );
}
else {
// This should never happen
return Collections.EMPTY_LIST;
}
}
public Boolean isLeaf(Object representedObject, Lookup env) {
return ((FileObject)representedObject).isFolder() ? Boolean.TRUE : Boolean.FALSE;
}
}
Notice that there is usually no need to create new Lister for each represented object. It is usually enough to register just one. You can even decide that the Look itself will implement the Listener interface. However if necessary e.g. for some event source translation you may still decide to create one Listener per represented object.
Also notice that when your Look is used more than once for given represented object (E.g. there are mor views or your look is used in more Filter/Composite Looks) then the attachTo(...)/detachFrom(...) methods will only be called once. So you don't have to worry about having atached more Listeners than necessary to your represented object.
Following exaple shows how to react on change of name of a FileObject. Of
course the FileObjectChangeListener is richer than that but the example is
restriced to name in order to keep it short and understandable. The implemetation
of other methods would be analogous.
Again we introduce new class in the example but it would be perfectly OK to
have all the methods in one class.
import org.openide.filesystems.*;
import org.netbeans.spi.looks.Look;
import org.openide.util.Lookup;
import org.openide.util.Utilities;
public class FileObjectEvents extends Look implements FileChangeListener {
private static final Look INSTANCE = new FileObjectEvents( "FILE_OBJECT_EVENTS" );
private FileObjectEvents(String name) {
super( name );
}
public static Look getInstance() {
return INSTANCE;
}
public void attachTo( Object representedObject ) {
((FileObject)representedObject).addFileChangeListener( this );
}
public void detachFrom( Object representedObject ) {
((FileObject)representedObject).removeFileChangeListener( this );
}
// Implementation of FileChangeListener ------------------------------------
public void fileRenamed( FileRenameEvent fe ) {
fireChange( fe.getSource(), Look.GET_NAME | Look.GET_DISPLAY_NAME );
}
public void fileAttributeChanged( FileAttributeEvent fe ) { /*IGNORE */ }
public void fileChanged( FileEvent fe ) { /*IGNORE */ }
public void fileDataCreated( FileEvent fe ) { /*IGNORE */ }
public void fileDeleted( FileEvent fe ) { /*IGNORE */ }
public void fileFolderCreated( FileEvent fe ) { /*IGNORE */ }
}
In case of Nodes the right thing to do would of course be to figure out what the real represented objects are and rewrite your the code to use the Looks more "native" way. This is nice but sometimes happens that developers are rather looking for the quick and dirty solution.
How to get Look for representing JavaBeans:
Look look4Beans = org.netbeans.spi.looks.Looks.bean();
How to get Look for reresenting Nodes:
Look look4Nodes = org.netbeans.api.nodes2looks.Nodes.nodeLook();
Notice that the FilterLook not only takes care of filtering the methods but it also restricts the event firing. I.e. if you filter all methods except getName and getDisplayName you at the same time flter all change events except those for changes in getName() and getDisplayName(). So even if the delegate Look fires a change in children such a FilterLook will not refire this event.
Following example shows how to create a look which behaves as a standard Look for beans but only for properties and customizer (and corresponding events of course)
Look beans = Looks.bean();
Look filteredBeans = Looks.filter( "MY_FILTER_BEANS", beans,
Look.GET_NAME | Look.GET_DISPLAY_NAME );
Following example shows merging the three aspects into one example.
Look composite = Looks.composite( "MY_FO_COMPOSITE", new Look[] {
FileObjectStyle.getInstance(),
FileObjectChildren.getInstance(),
FileObjectEvents.getInstance() } );
Look composite = Looks.composite( "MY_FO_COMPOSITE", new Look[] {
Look_A, Look_B // Ordering is important
} );
Well the resulting look will merge the values and the result will be
{ CHa1, CHa2, CHb1 }. This is easy and can be applyied to all methods which
return Collections of objects e.g. childObjects, Actions, Properties etc.
But what to do with single vaued aspects of the object e.g. name or
Customizer? In this case the composite will apply the first (who does
return something else than null) wins. I.e. if the look A would return
NAME_A from getDisplayName() method and the Look B would return NAME_B then
result given by getDisplayName() on the composit look would be NAME_A, However
if the look A would not implement the getDisplayName() method (i.e. would
return null then the result would be NAME_BThis kind of composition allows for additions/competition (depends on whther the aspect is single or multivalued). When combined with the registration of Looks in the module layer (discussed later) it gives the possibility of cooperation between modules.