Extreme Java When vanilla Java is not enough

27Sep/100

From Swing to NetBeans Platform (part 2)

In my previous post, I put an old Swing application to run as a NetBeans Platform Application. After some refactoring, I got it working, nicely integrated to NB's file editing support. Now, I'm converting their Tileset Manager from a JDialog to a TopComponent sensitive to global selection.

I started with a new "TileSet Manager" module, then moved the TSM dialog to that module and changed it to a JPanel. After adding a TC and putting the TSM Panel on it, half-work was done. Thanks to the Swing skills of Tiled team, the next step was a piece of cake. After tracking the global selection, I only made the right "setEditor" on their manager and voilá! The tileset table changes according to the selected file! God bless the NetBeans Team!

It's already very cool, but I want more. Their version uses JTables and lots of buttons on this dialog. What about using NBP's ExplorerManager? Easy as 1-2-3. I replaced the TSM Panel with a BeanTreeView plus some Nodes and the ExplorerManager (just like FeedReader tutorial). I could use OutlineView to keep the table look, but I plan to add some depth on this tree later. And, instead of a table with two columns, I use a nice tree view with HTML display name:

Migration of actions started by the most complex one: Delete. Adding the action to the ExplorerManager and their JButton's actionPerformed to Node.canDestroy and Node.destroy did the trick.

There were other actions, like "Save As" and "Embed". All of them were easy to convert. I cut the code on "actionPerformed", pasting it on new actions. I put them on NB layer and used this snippet to convert them to Node.getActions:

@Override
public Action[] getActions(boolean context) {
  List<Action> list = new ArrayList<Action>();
  for (Object obj : Lookups.forPath("...")
      .lookupAll(Object.class)) {
    if (obj instanceof JSeparator) {
      list.add(null);
    }
    if (obj instanceof Action) {
      list.add((Action) obj);
    }
  }
  return list.toArray(new Action[list.size()]);
}
12May/080

Custom project file structure in NetBeans – new menu item in projects

In a previous post, I’ve told that I want to use NetBeans and share projects with developers that still use Eclipse. In another post, I added a custom folder to the project view.

Now, I will add a custom menu item to run an ant task outside NB's build scripts:

  1. Assuming you have a NB module project ready, you just need to add a new entry in your layer.xml:
    <folder name="Projects">
      <folder name="Actions">
        <file name="org-mycompany-MyMenuAction.instance"/>
      </folder>
    </folder>
  2. Create a new class with the same name (org.mycompany.MyMenuAction):
    public final class TransitMenuAction
      extends AbstractAction
      implements Presenter.Popup, ContextAwareAction {
     
      public final static String DISPLAY_NAME =
        NbBundle.getMessage(TransitMenuAction.class,
          "CTL_TransitMenuAction");
     
      private Lookup context;
     
      public TransitMenuAction() {
        this(null);
      }
      public TransitMenuAction(Lookup context) {
        super(DISPLAY_NAME);
        this.context = context;
      }
     
      public void actionPerformed(ActionEvent e) {
        // I'm a menu container... I do nothing
      }
     
      public Action createContextAwareInstance(
        Lookup context) {
        return new MyMenuAction(context);
      }
      public JMenuItem getPopupPresenter() {
        return new PopupPresenter();
      }
     
      /** Dynamic menu factory */
      class PopupPresenter extends JMenuItem
        implements DynamicMenuContent {
     
        public JComponent[] getMenuPresenters() {
          Project proj = context.lookup(Project.class);
          // MyUtils comes from last post
          if (!MyUtils.isMyProject(proj)) {
            // hides this menu
            return new JComponent[0];
          }
     
          JMenu mnu = new JMenu(DISPLAY_NAME);
          mnu.add(new PackageAction(context));
          return new JComponent[] { mnu };
        }
        public JComponent[] synchMenuPresenters(
          JComponent[] items) {
          return items;
        }
      }
    }
  3. And, the PackageAction is:
    public class PackageAction extends AbstractAction {
      private Lookup context;
      public PackageAction(Lookup context) {
        super(NbBundle.getMessage(PackageAction.class,
          "CTL_PackageAction"));
        this.context = context;
      }
     
      /* This action will copy a file and run an Ant task */
      public void actionPerformed(ActionEvent e) {
        try {
          /* Both the file and the build scripts are under
           * the "ant" folder in project directory
           */
          Project proj = context.lookup(Project.class);
          FileObject antFolder = proj.getProjectDirectory().
            getFileObject("ant");
     
          // Read the original file
          FileObject def = antFolder.
            getFileObject("compile-default.properties");
          Properties prop = new Properties();
          prop.load(def.getInputStream());
     
          // Change a little bit
          // TODO: Add this to a property window
          prop.setProperty("compile.lib.ext",
            "C:\\etc\\apache-tomcat-6.0.14\\lib");
     
          // Write to the copy file
          FileObject cpy = antFolder
            .getFileObject("compile.properties");
          if (cpy == null) {
            cpy = antFolder.createData("compile.properties");
          }
          FileLock lock = cpy.lock();
          try {
            prop.store(cpy.getOutputStream(lock),
              "any cool comment");
          } finally {
            lock.releaseLock();
          }
     
          // Now, we will run Ant - grab the script...
          FileObject buildXML = antFolder
            .getFileObject("build.xml");
     
          // ... and run the "package" target
          ActionUtils.runTarget(buildXML,
             new String[]{ "package" }, null);
        } catch (IOException ex) {
          Exceptions.printStackTrace(ex);
        } catch (IllegalArgumentException ex) {
          Exceptions.printStackTrace(ex);
        }
      }
    }
  4. Last, but not least, the project dependencies:
    1. Ant (ActionUtils)
    2. Filesystem API (FileObject and FileLock);
    3. Project API (Project);
    4. UI Utilities API (DynamicMenuContent); and
    5. Utilities API (Lookup)

And, your new project menu item (that calls an Ant task) is ready.

12May/080

Custom project file structure in NetBeans – adding folder

In a previous post, I've told that I want to use NetBeans and share projects with developers that still use Eclipse. Since we have a custom requirements, I plan to customize the way NB handle these projects.

As you will notice, every NB project customization will go into the layer file, under "Projects" folder. You will be surprised on how easy it goes.

In this post, my task is to make a "config" folder visible under "Projects" tab of NetBeans. Assuming you already have a NetBeans module project ready, steps are:

  1. Create an utility class with this method:
    public static boolean isMyProject(Project proj) {
      // check if it is a nb project
      if (proj.getProjectDirectory().getFileObject("nbproject") ==
        null) {
        return false;
      }
      // check if our custom folder exists
      return (proj.getProjectDirectory().getFileObject("config") !=
        null);
    }
  2. In your layer.xml, create an entry named "Projects / type-of-your-desired-project / Nodes". You can look into "Important Files / XML Layer / Projects / org-netbeans-modules-xxx-project / Nodes" and right click on it to add a new file - NB will add the "folder" tags automatically;
  3. Inside "Nodes", add a file named "org-mycompany-MyCompanyNodeFactory.instance" (or rename the file if you created it on step 1);
  4. Now, create a class named "org.mycompany.MyCompanyNodeFactory" that implements NodeFactory. This is a pretty small one:
    public class MyNodeFactoryImpl implements NodeFactory {
      public NodeList createNodes(Project proj) {
        if (MyUtils.isMyProject(project) {
          try {
            MyFilesNode nd = new MyFilesNode(proj);
            return NodeFactorySupport.fixedNodeList(nd);
          } catch (DataObjectNotFoundException ex) {
            Exceptions.printStackTrace(ex);
          }
        }
        // if we can't use it, return an empty list
        return NodeFactorySupport.fixedNodeList();
      }
    }
  5. Create the MyFilesNode. It must be a child of Node. You can do anything you want here (just Google it and you will see what I mean). Mine is:
    public class MyFilesNode extends AbstractNode {
      private static Image smallImage =
              Utilities.loadImage("/path-to/icon-8x8.gif"); // NOI18N
     
      public MyFilesNode(Project proj) {
        super(Children.create(new MyChildFactory(proj), true);
      }
      public String getDisplayName() {
        return "My Files";
      }
      // A bonus: this snippet will merge NB folder icon and
      // your 8x8 icon
      @Override
      public Image getIcon(int type) {
        DataFolder root = DataFolder.findFolder(
          Repository.getDefault().getDefaultFileSystem().getRoot());
        Image original = root.getNodeDelegate().getIcon(type);
        return Utilities.mergeImages(original, smallImage, 7, 7);
      }
      @Override
      public Image getOpenedIcon(int type) {
        DataFolder root = DataFolder.findFolder(
          Repository.getDefault().getDefaultFileSystem().getRoot());
        Image original = root.getNodeDelegate().getIcon(type);
        return Utilities.mergeImages(original, smallImage, 7, 7);
      }
    }

    And, of course, the ChildFactory is:

    public class MyChildFactory extends ChildFactory<String> {
      private Project proj;
     
      public TransitFilesChildren(Project proj) {
        this.proj = proj;
      }
     
      /* Since a node is the visual representation of
       * something else, this method builds the list
       * of "something else"s we want to show. If this
       * was a business application, the "key" could be
       * a value object. In this module, the key will
       * be the name of the custom folder.
       */
      @Override
      protected boolean createKeys(List<String> keys) {
        keys.add("ant");    // another folder - why not?
        keys.add("config");
        return true;
      }
     
      @Override
      protected Node createNodeForKey(String key) {
        // 1. Find the FileObject for the folder
        FileObject fo = proj.getProjectDirectory().getFileObject(key);
        // 2. Find the DataFolder for the FileObject
        DataFolder df = DataFolder.findFolder(fo);
        // 3. Create a custom filter
        DataFilter filter = new MyDataFilter();
        // 4. Get DataFolder children (with filter)
        Children children = df.createNodeChildren(filter);
        // 5. Create a proxy lookup
        Lookup lookup = new ProxyLookup(new Lookup[]{
          df.getNodeDelegate().getLookup(), proj.getLookup()
        });
        // 6. Create a filter node
        return new FilterNode(df.getNodeDelegate(), children, lookup);
      }
     
      /**
       * Filter DataObjects that must not be shown (like .svn folders)
       */
      static final class MyDataFilter implements ChangeListener,
        ChangeableDataFilter {
     
        private final ChangeSupport changeSupport =
          new ChangeSupport(this);
     
        public MyDataFilter() {
          VisibilityQuery.getDefault().addChangeListener(this);
        }
        public boolean acceptDataObject(DataObject obj) {
          FileObject fo = obj.getPrimaryFile();
          return VisibilityQuery.getDefault().isVisible(fo);
        }
        public void stateChanged(ChangeEvent e) {
          changeSupport.fireChange();
        }
        public void addChangeListener(ChangeListener listener) {
          changeSupport.addChangeListener(listener);
        }
        public void removeChangeListener(ChangeListener listener) {
          changeSupport.removeChangeListener(listener);
        }
      }
    }
  6. Now, go toproject properties and add the following modules (you can also search for the needed classes):
    1. Datasystems API (ChangeableDataFilter, DataFilter, DataFolder and DataObject);
    2. Filesystem API (FileObject);
    3. General Queries API (VisibilityQuery);
    4. Nodes API (ChildFactory, Children, FilterNode and Node);
    5. Project API (Project);
    6. Utilities API (ChangeSupport, Lookup and ProxyLookup);

Now, if you run the project, you will notice the new folder under Projects view.

12May/080

Custom project file structure in NetBeans

In my job, they officially use Eclipse Europa to develop. I really dislike Eclipse - it is a matter of taste. Because of this, I'm using NetBeans ability to share projects (like in this demo) - and it works like a charm.

But, since even rose has its thorn, they have some custom tasks here:

  1. Inside every project, there's a "config" folder with some files that must be copied to a "/var/xxx" folder before running the application;
  2. A custom ant build script to create a ZIP file that will go to test and production environments; and
  3. The file from step 2 must be transfered via SFTP to specific servers, on a folder named like "/var/deploy/xxx".

These are manually-repeated tasks that must be done for every project. Eclipse users (here) must do it step-by-step. NetBeans users will use a (custom) brand-new module that I developed to do these tasks.

Just to enumerate, we need to solve these problems:

  1. The "config" folder must be visible in "Projects" tab (it is under "Files" tab, but it is ugly);
  2. An ant task in a custom build script must be called whenever I want;
  3. The "config" folder must be copied to "/var/xxx" when the project is compiled;
  4. An ZIP file must be sent via SFTP to a "/var/deploy/xxx" folder;
  5. I don't want to hardcode the "xxx" - what about a new tab on project properties?

OK. We have a lot of things to cover here. But, they are very simple. Since it could become a large post, I'll cover them in other posts (that will be have a pingback here, on comments section).