BookNBPlatformCookbookCH0207

Contents

NetBeans Platform Cookbook Chapter 02 07

Using Palette

You know Pallete from editors - you can drag an object from the palette to text or nodes tree or visual panel to add the object into it. Palette items are organized in categories. Most known is GUI creator for application - visual designer Mattise in NetBeans.

You can create Palette for text editor of any language but there is a better approach for it - editor hints. We will show how to create the palette for your purposes.


Preparation

The NetBeans Platform opens corresponding palette when the TopComponent that uses it becomes activated. If you want open Palette panel by TopComonent together store the PaletteController implementation. You can bind some palette with MIME-type, too.

The PaletteController is created via PaletteFactory.createPalette() method. Palette can be defined in layer file or by providing structure of Nodes. You can use the Palette to drag objects into source code or into any object that accepts drag and drop operation.

An example illustrating usage of Palette API we use a base of simple game framework.

Create suite project and new “model” module project (see project Palette in example sources). We define a World - rectangle area of squares. Each square can contain some object, e. g. Meadow, Forest, House. We define Entity interface for it. Next define EntityFactory creating Entities and Worlds. We will use the Entity as a prototype so it must be cloneable. The World implementation must fire PropertyChangeEvents.

public interface Entity extends Cloneable {
    public String getName();
    public Image getImage();
    public Object clone();     
}

public interface World {
    public static final String PROP_ENTITY = "entity";
    public int getRowsCount();
    public int getColumnsCount();
    public Entity getEntity(int row, int col);
    public void setEntity(int row, int col
                        , Entity newEntity);
    public void addPropertyChangeListener(PropertyChangeListener l);
    public void removePropertyChangeListener(PropertyChangeListener l);
}

In other “Entities” module provides your model implementation as a service.

@ServiceProvider(service=EntityFactory.class)
public class WorldEntityFactory implements EntityFactory {

    private static List<Entity> entities = null;

    @Override
    public World createWorld(int rows, int cols) {
        return new Country(rows, cols);
    }

    @Override
    public List<Entity> createEntities() {
        if (entities == null)
            makeEntities();
        return entities;
    }
 
    private synchronized void makeEntities() {
        if (entities != null) return;
        List<Entity> lst = new ArrayList<Entity>();

        lst.add( new Meadow() );
        lst.add( new Forest() );
        lst.add( new Path() );
        lst.add( new Water() );
        lst.add( new House() ); 

        entities = Collections.unmodifiableList(lst);
    }

}

where the Country is World implementation and Meadow, Forest etc. are real kinds of the Entity.

In the “View” module we create WorldTopComponent containing visual representation of the World - WorldScene. We define our palette here but it should be better to create new module for it.

The WorldScene extends the ObjectScene from NetBeans Visual Library - see Visual Library Chapter. In brief: the ObjectScene is visualization of general Scene. It can contain objects to display - widgets - and can register pairs object-widget by calling addObject(obj, widget). It enables to find joined widget for object and object for widget. It contains one instance of the World and displays ImageWidget for each Entity. How to add widget for entity is shown here:

    private void setEntityWidget(World w, int r, int c) {
        ImageWidget iw;
        Entity e;
        int x = c * getEntitySize();
        int y = r * getEntitySize();
        e = w.getEntity(r, c);
        iw = new ImageWidget(this, e.getImage());
        iw.setPreferredLocation(new Point(x, y));
        addObject(e, iw);
        worldLayer.addChild(iw);
   }

The WorldScene listens to property changes of its World. If some square was changed retrieve old entity from the event, find joined widget, remove it from scene, remove object registration and add new widget instead of old one.

      public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getPropertyName().equals(World.PROP_ENTITY)) {
                Object [] v = (Object []) evt.getNewValue();
                Entity en = (Entity) v[1] ;
                Widget oldWidget = findWidget( en );

                worldLayer.removeChild( oldWidget );
                removeObject(en);

                Dimension d = (Dimension) v[0];
                setEntityWidget(world, d.width, d.height);
            }
      }  

How to

Define Palette by the layer file

Describe your structure in the layer file in structure palette-name/category*/item* of files:

   <folder name="EntityPalette">
      <folder name="Basic">
         <attr name="SystemFileSystem.localizingBundle"
               stringvalue="nbpcook.palette.viewdecl.palette.Bundle"/>
         <attr name="isReadonly" stringvalue="true"/>
         <attr name="isExpanded" boolvalue="true"/>
         <file name="EntityMeadow.xml"
               url="palette/EntityMeadow.xml">
             <attr name="SystemFileSystem.localizingBundle"
                   stringvalue="nbpcook.palette.viewdecl.palette.Bundle"/>
         </file>
         <file name="EntityForest.xml" url="palette/EntityForest.xml">
             <attr name="SystemFileSystem.localizingBundle"
                   stringvalue="nbpcook.palette.viewdecl.palette.Bundle"/>
         </file>
      </folder>
      <folder name="Objects">
         <attr name="SystemFileSystem.localizingBundle"
               stringvalue="nbpcook.palette.viewdecl.palette.Bundle"/>
         <attr name="isReadonly" stringvalue="true"/>
         <attr name="isExpanded" boolvalue="true"/>
         … declare house, brick, tree...
      </folder>
   </folder>

For each item define the item-file.xml, e. g. EntityForest.xml. Do not get scared you can't find this file in the Project tree – it is displayed as “Forest” node only because this XML file is registered as NetBeans file-type and localized name is displayed.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE editor_palette_item PUBLIC
      "-//NetBeans//Editor Palette Item 1.1//EN"
      "http://www.netbeans.org/dtds/editor-palette-item-1_1.dtd">

<editor_palette_item version="1.1">
  <class name="nbpcook.palette.viewdecl.palette.EntityForestItem"/>
  <icon16 urlvalue="nbpcook/palette/viewdecl/palette/ForestItem16.png"/>
  <icon32 urlvalue="nbpcook/palette/viewdecl/palette/ForestItem32.png"/>
  <inline-description>
     <display-name>Forest</display-name>
     <tooltip>Add Forest</tooltip>
  </inline-description>
</editor_palette_item>

Do not forget add used images and localized text into Bundle.properties file:

EntityPalette/Basic=Entities
EntityPalette/Objects=Objects

EntityPalette/Basic/EntityMeadow.xml=Meadow
EntityPalette/Basic/EntityForest.xml=Forest

For each item create XxxItem.java (see example with EntityForestItem.java) and implement Transferable interface. Items will be similar except containing domain object (Entity in our example). So create base abstract class EntityItem:

public abstract class EntityItem implements Transferable {

    public static final DataFlavor DATA_FLAVOR = 
               new DataFlavor("text/someplay-world", 
                       NbBundle.getMessage(EntityItem.class,
                                     "Entity.flavor.name"));

    private static DataFlavor [] FLAVORS = 
               new DataFlavor [] { EntityItem.DATA_FLAVOR };

    private Entity data;

    public EntityItem(Entity data) {
        this.data = data;
    }

    public Entity getEntity() {
        return data;
    }

    public Object getTransferData(DataFlavor flavor) 
                  throws  UnsupportedFlavorException
                        , IOException {
        return data;
    }
    
    public DataFlavor[] getTransferDataFlavors() {
        return FLAVORS;
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {
        if (flavor == DATA_FLAVOR)  return true;
        else      return false;
    }    
}

Each item is then simple – EntityForestItem.java:

public class EntityForestItem extends EntityItem {
    public EntityForestItem() {
        super( new Forest() );
    }
}

Explore the source: Each item contains domain object (some data) to inform the Palette API what object to add. The Transferable object provides list of DataFlavor instances what it contains, checks if it supports given flavor and returns object of required flavor.

To inform the Platform about palette existence at run-time store PaletteController instance into TopComponent's lookup:

    PaletteController palette = EntityPaletteSuport.createPalette();
    associateLookup( Lookups.fixed( getActionMap(), palette ) );

You can create the the EntityPalette directly here but you must provide some support for creating it. So we delegate this work to EntityPaletteSupport class:

    public static final String ENTITY_PALETTE_FOLDER = "EntityPalette";
    
    private static PaletteController paletteController = null;
    
    public static PaletteController createPalette() {

        if (paletteController == null)
            try {
               paletteController = PaletteFactory.createPalette(
                         ENTITY_PALETTE_FOLDER   // folder
                       , new EmptyPaletteActions()
                       , null   // PaletteFilter
                       , new DnDHandler() );
           } catch (IOException ex) {
               Logger.getLogger(EntityPaletteSuport.class.getName()).log(Level.SEVERE
                       , "Palette " + ENTITY_PALETTE_FOLDER + " is not initialized", ex);
               Exceptions.printStackTrace(ex);
           }
        return paletteController;
    }

The palette is created by NetBeans PaletteFactory by createPalette() method. Pass next parameters:

  • Folder name in the layer file where palette structure is defined.
  • PaletteActions instance – it is used by Palette API by palette panel customization and maintenance (e. g. add new item).
  • PaletteFilter instance which enables dynamically filter shown items.and categories.
  • DragAndDrop instance - Handle drop of new items into palette window and add custom DataFlavors to the Transferable of items being dragged from the palette to editor window. Can be null. (from documentation)

If you can't provide palette customization the PaletteActions can be trivial empty implementation:

 private static final Action [] ACT_ARR_0 = new Action [] {} ;
 private static class EmptyPaletteActions extends PaletteActions {
     public EmptyPaletteActions() {
     }
     public Action[] getImportActions()  { return ACT_ARR_0; }
     public Action[] getCustomPaletteActions()  { return ACT_ARR_0; }
     public Action[] getCustomCategoryActions(Lookup category)  
                                                  { return ACT_ARR_0; }
     public Action[] getCustomItemActions(Lookup item)  
                                                  { return ACT_ARR_0; }
     public Action getPreferredAction(Lookup item) {
         return new NotSupportedAction();
     }
 } // EmptyPaletteActions

 private static class NotSupportedAction 
                          extends AbstractAction {
        public NotSupportedAction() {
        }
        public void actionPerformed(ActionEvent e) {
            throw new UnsupportedOperationException(
                       "Not supported yet.");
        }
    } // NotSupportedAction

The last rest is DragAndDropHandler which enables your palette import other items by dragging into palette.

 private static class DnDHandler 
                extends DragAndDropHandler {

    /** Add your own custom Transferable and DataFlavor
     * as need to support drag-drop into editor.
     *    
     * @param exTransferable Item's default Transferable
     * @param lookup Palette item's Lookup
     */
    public void customize(ExTransferable exTransferable
                        , Lookup lookup) {
        EntityItem ent = lookup.lookup(EntityItem.class);
        final Entity entity = ent.getEntity();
        exTransferable.put(
            new ExTransferable.Single(EntityItem.DATA_FLAVOR)
            {
                protected Object getData() 
                          throws IOException
                               , UnsupportedFlavorException 
                {
                    return entity;
                }
            });
        }
        
    } // DnDHandler

Define Palette by Nodes structure

Now we try create our palette by nodes structure. The PaletteController is created by PaletteFactory.createPalette(). Pass the root node as a parameter. Its children are categories nodes and under them are item nodes.

We begin creating palette in EntityPaletteSuportByNode class:

    public static PaletteController createPalette() {
        if (paletteController == null) {
               AbstractNode paletteRoot = new AbstractNode(new RootChildren());
               paletteRoot.setName("Entity Palette Root");
               paletteController = PaletteFactory.createPalette(
                         paletteRoot // root node
                       , new EmptyPaletteActions()
                       , null   // PaletteFilter
                       , new DnDHandler() );               
        }
        return paletteController;
    }

Actions, filter, DataFlavor and DragAndDropHandler are the same as in previous case.

The RootChildren creates only category nodes by AbstractNode:

 private static class RootChildren 
                extends Children.Keys<String>
 {
     private static final String [] CATEGORIES =  {
            "Basic", "Objects", "Continuous"
     };

     public RootChildren() {
         setKeys(CATEGORIES);
     }

     protected Node[] createNodes(String key) {
         // key - category name
         Node no = new AbstractNode(
                            new  CategoryChildren(key) );
         no.setDisplayName(key);
         return new Node [] { no };
     }
 } // RootChildren

CategoryChildren class creates item nodes containing its Entity as the key:

    private static class CategoryChildren 
                        extends Children.Keys<Entity>
    {
        private String category;

        private static final String [] CATEGORIES =  {
            "Basic", "Objects", "Continuous"
            };
        private static final Entity [] [] ENTITIES = {
                { new Meadow(), new Forest() }
              , { new House() }
              , { new Path(), new Water() }
            };
        public CategoryChildren(String category) {
            super();
            this.category = category;
            int ind = -1;
            for (int i = 0; ind < 0 && i < CATEGORIES.length; i++) {
                if (CATEGORIES[i].equals(this.category)) {
                    ind = i;
                }
            } // for i CATEGORIES
            if (ind >= 0) {
                System.out.println("set entities " + ind);
                setKeys( ENTITIES [ind] );
            }
        }

        protected Node[] createNodes(Entity key) {
            // key  - Entity            
            return new Node [] { new EntityNode(key) };
        }

    } // CategoryChildren

The EntityNode keeps its Entity which the target component clones and uses.

    public static class EntityNode extends AbstractNode {

        private Entity entity;

        public EntityNode(Entity ent) {
            super( Children.LEAF
                 , Lookups.fixed(ent, ent.getImage()) );
            this.entity = ent;
        }

        public Image getIcon(int type) {
            return entity.getImage();
        }

        public String getName() {
            return entity.getName();
        }        

    } // EntityNode

To enable WorldScene panel accept drag operation add an AcceptAction. The isAcceptable() method of the AcceptProvider is called to check if the component can accept given transferable. You can paint the image by dragging it from the palette onto the component. Retrieve Entity object from given Transferable and draw its icon at cursor location. Then return ConnectorState.ACCEPT to agree. After dropping is the method accept() called.

getActions().addAction(ActionFactory.createAcceptAction( 
  new AcceptProvider() {

    public ConnectorState isAcceptable(Widget widget
                               , Point point
                               , Transferable transferable) {
        Entity e = getEntityFromTransferable(transferable);
        Image dragImage = e.getImage() == null 
                           ? AbstractEntity.STD_IMAGE  
                           :  e.getImage();
        // repaint to restore old image position
        JComponent view = getView();
        Graphics2D g2 = (Graphics2D) view.getGraphics();
        Rectangle visRect = view.getVisibleRect();
        view.paintImmediately(visRect.x, visRect.y
                            , visRect.width, visRect.height);
        // draw entity icon to cursor location
        g2.drawImage(dragImage,
                AffineTransform.getTranslateInstance(
                   point.getLocation().getX(),
                   point.getLocation().getY())
                 , null);
        return ConnectorState.ACCEPT;
    }

    public void accept(Widget widget, Point point
                       , Transferable transferable) {
        Entity e = getEntityFromTransferable(transferable);
        // for addObject(e, widget) other instance is needed
        e = (Entity) e.clone();  
        Point p = convertLocationToIndexes(point);    
        world.setEntity(p.x, p.y, e);
    }

}));

To get Entity from transferable create geTransferData() method:

    private Entity getEntityFromTransferable(Transferable treansferable) {
        Object o = null;
        Entity e ;
        try {
            e = (Entity) transferable.getTransferData(EntityPaletteUtil.DATA_FLAVOR);
        } catch (UnsupportedFlavorException ex) {
            ex.printStackTrace();
            e = null;
        } catch (IOException ex) {
            ex.printStackTrace();
            e = null;
        }
        return e;
    } 

Let's Explain!

Notes/Tips

To add your palette to editor register the palette for MIME-type. When this MIME-type source is opened the associated Palette opens.

<!-- Register for editor and MIME type if 
     you will use the palette for editor 
  -->
<folder name="Editors">
    <folder name="text">
        <folder name="someplay-world">
            <file name="PaletteFactory.instance">
                <attr name="instanceOf"
                      stringvalue="org.netbeans.spi.palette.PaletteController"/>
                <attr name="instanceCreate"
                      methodvalue="com.packtpub.nbpcook.palette.view.EntityPaletteSupport.createPalette"/>
            </file>
        </folder>
    </folder>
</folder>

Now you can drag objects from the palette onto country map and add them into the Country instance by dropping.

image:nbpcook_02_13_palette.png

Figure 2.13 Using Palette

Sources

Text and sources were created under NB 6.8 6.9, 7.0, 7.1, 7.2.

The declarative (?) palette fails. The by-class is OK.

Navigation

Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo