Looks API - HowTo

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:

See also:

1] How to implement simple node using looks

This is the basic problem. There is some object which has to be represented using nodes. When using the NodesAPI only one would subclass the Node class (or better the AbstractNode class), override some methods (e.g getDisplayName() ). After that you would call the constructor of the new class. When using looks the procedure is very similar but there are some little diffrences. You will subclass the Look class, which gives you a "null" or "meutral" visual representation, such a Look does not produce any data. This is probably not intended behavior so you have to decide which data you want to produce and override corresponding methods in your subclass.

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.

2] How to add children (subnodes)

The Look in the example in above would only show one directory it would not be possible to descend deeper in the Files tree. One would probably want to be able to expand the folders. So we need to implement the Children aspect of the nodes.

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;
    }    
    
}

3]How to make coding easier with DefaultLook

TBD

4] How to reflect changes in represented object

Taking the values from the represented object and translating them was easy. But what to do when the represented object changes one or more of it's properties while it is shown in the application. In this case it is desirable that given object changes it's apperance. How to do it?
First of all you definitely will have to register listener on your represented object to get notified about the changes in the object. This has to be done in the attachTo method of Look. In order to free memory correctly you also should remove the Listeners. The right place to do so is in detachFrom method.
Then you only need to translate/refire the events usink the fireChange method in Look.

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 */ }
    
}

5] How to transform "standard objects" (Beans & Nodes)

Sometimes your object is a standard object. (Currently plain beans and the NetBeans nodes) are supported. If so you don't need to write your own look. You can use the class Looks which is a static factory and can provide implentations of the Looks for this kind of objects.

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();

7] How to reuse part of an existing Look - Filtering

Let's assume you have an existing looks which implements almost all the methods from look. You want to create a different look which is exactly the same as this look but does provide just a small part of the functionality. E.g. you want to reuse only the name and icon and to ignore all other values from the original look. You might try to write new Look which will delegate on the orignal in the getName(...) and getIcon(...) methods. This is OK but you will create one class more in your VM. There exists an generic solution in the Looks API. It is the FilterLook. FilterLook can be created using the filter() method the parameters of the method require you to provide name of the FilterLook, Look to which this look wants to delegate and bit mask which specifyies which methods should be delegated.

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 );

8] How to build a look from more looks - Composition

Restricting some look's functionality might be nice but sometimes one needs exactly the opposite i.e. merging functionality. In the above examples we've created Looks for style, children and events. Now we would like to make it one doing everything. This can be done using composite Look. Which is crated from a set of "sub-looks" which are merged together.
Generally speaking such usecase is about merging different aspects of the object visualisation together.

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() } );

9] How to add features to other looks - Composition II

In the previous example methods implemented in the sub-looks were not overlaping. But what if they would overlap?
This gives us different way of looking at the composition. Let's have two looks ( A and B ) both have the getChildObjects method implemented. Look A returns { CHa1, CHa2 } and B returns { CHb1 }. What will happen when we create a composite Look exactly the way we did in the example above.
    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_B

This 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.

WARNING: Here ends the revised version of the document

Following paragraphs are oudated nad reflect older APIs.

9] How to declare looks using the system filesystem

Until now we've created all our looks programmaticaly from code. But this is not the only way how to instantiate objects in NetBeans. The other way is to crate objects from system filesystem. This is done by putting special files on the filesystem. Adding files is in turn done usually using XML layers. For more info see LINK. So if you for example plan to install the MethodLook using XML layer you would do something like EXAMPLE If you plan to use some Look or LookSelector from the Looks class. The format is very similar. Some of the Looks in the factory (e.g. FilterLook) take some additional parameters in form of file attributes, please consult the LooksAPI Javadoc for the exact syntax.

10] How to allow other modules to influence your view

TBD