Saturday Jun 30, 2012

HTML, JavaScript, and CSS in a NetBeans Platform Application

I broke down the code I used yesterday, to its absolute bare minimum, and then realized I'm not using HTML 5 at all:

<html>
    <head>
        <link rel="stylesheet" href="style.css" type="text/css" media="all" />
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.3/jquery.min.js"></script>
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
        <script type="text/javascript" src="script.js"></script>
    </head>
    <body>
        <div id="logo">
        </div>
        <div id="infobox">
            <h2 id="statustext"/>
        </div>
    </body>
</html>

Here's the script.js file referred to above:

$(function(){
    
    var banana = $("#logo");
    
    var statustext = $("#statustext");
    
    var defaulttxt = "Drag the banana!";
    var dragtxt = "Dragging the banana!";
	
    statustext.text(defaulttxt);
	
    banana.draggable({
        drag: function(event, ui){
            statustext.text(dragtxt);
        },
        stop: function(event, ui){
            statustext.text(defaulttxt);
        }
    });
	
});

And here's the stylesheet:

body { 
    background:#3B4D61 repeat 0 0; 
    margin:0; 
    padding:0; 
}
h2 { 
    color:#D1D8DF; 
    display:block; 
    font:bold 15px/10px Tahoma, Helvetica, Arial, Sans-Serif; 
    text-align:center; 
}
#infobox { 
    position:absolute; 
    width:300px; 
    bottom:20px; 
    left:50%; 
    margin-left:-150px; 
    padding:0 20px; 
    background:rgba(0,0,0,0.5); 
    -webkit-border-radius:15px; 
    -moz-border-radius:15px; 
    border-radius:15px; 
    z-index:999; 
}
#logo { 
    position:absolute; 
    width:450px; 
    height:150px; 
    top:40%; 
    left: 30%; 
    background:url(bananas.png) no-repeat 0 0; 
    cursor:move; 
    z-index:700; 
}

However, I've replaced the content of the HTML file with a few of the samples from here, without any problem; in other words, if the HTML 5 canvas were to be needed, it could seamlessly be incorporated into my NetBeans Platform application:

https://developer.mozilla.org/en/Canvas_tutorial/Basic_usage

Friday Jun 29, 2012

Mixing JavaFX, HTML 5, and Bananas with the NetBeans Platform

The banana in the image below can be dragged. Whenever the banana is dropped, the current date is added to the viewer:

What's interesting is that the banana, and the viewer that contains it, is defined in HTML 5, with the help of a JavaScript and CSS file. The HTML 5 file is embedded within the JavaFX browser, while the JavaFX browser is embedded within a NetBeans TopComponent class.

The only really interesting thing is how drop events of the banana, which is defined within JavaScript, are communicated back into the Java class.

Here's how, i.e., in the Java class, parse the HTML's DOM tree to locate the node of interest and then set a listener on it. (In this particular case, the event listener adds the current date to the InstanceContent which is in the Lookup.)

Here's the crucial bit of code:

WebView view = new WebView();
view.setMinSize(widthDouble, heightDouble);
view.setPrefSize(widthDouble, heightDouble);
final WebEngine webengine = view.getEngine();
URL url = getClass().getResource("home.html");
webengine.load(url.toExternalForm());
webengine.getLoadWorker().stateProperty().addListener(
        new ChangeListener<State>() {
            @Override
            public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
                if (newState == State.SUCCEEDED) {
                    Document document = (Document) webengine.executeScript("document");
                    EventTarget banana = (EventTarget) document.getElementById("banana");
                    banana.addEventListener("click", new MyEventListener(), true);
                }
            }
        });

It seems very weird to me that I need to specify "click" as a string. I actually wanted the drop event, but couldn't figure out what the arbitrary string was for that. Which is exactly why strings suck in this context.

Many thanks to Martin Kavuma from the Technical University of Eindhoven, who I met today and who inspired me to go down this interesting trail.

Thursday Jun 28, 2012

Mercur Business Control

A professional looking corporate dashboard from Sweden:

Mercur Business Control provides a company’s management and all authorized employees with a unitary, functionally rich, complete system for budgeting, forecasting, management by objectives, reporting and analysis.

Wednesday Jun 27, 2012

Annotation Processor for Superclass Sensitive Actions

Someone creating superclass sensitive actions should need to specify only the following things:

  • The condition under which the popup menu item should be available, i.e., the condition under which the action is relevant. And, for superclass sensitive actions, the condition is the name of a superclass. I.e., if I'm creating an action that should only be invokable if the class implements "org.openide.windows.TopComponent",  then that fully qualified name is the condition.
  • The position in the list of Java class popup menus where the new menu item should be found, relative to the existing menu items.
  • The display name.
  • The path to the action folder where the new action is registered in the Central Registry.
  • The code that should be executed when the action is invoked.

In other words, the code for the enablement (which, in this case, means the visibility of the popup menu item when you right-click on the Java class) should be handled generically, under the hood, and not every time all over again in each action that needs this special kind of enablement.

So, here's the usage of my newly created @SuperclassBasedActionAnnotation, where you should note that the DataObject must be in the Lookup, since the action will only be available to be invoked when you right-click on a Java source file (i.e., text/x-java) in an explorer view:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import org.netbeans.sbas.annotations.SuperclassBasedActionAnnotation;
import org.openide.awt.StatusDisplayer;
import org.openide.loaders.DataObject;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;

@SuperclassBasedActionAnnotation(
        position=30, 
        displayName="#CTL_BrandTopComponentAction",
        path="File",
        type="org.openide.windows.TopComponent")
@NbBundle.Messages("CTL_BrandTopComponentAction=Brand")
public class BrandTopComponentAction implements ActionListener {
    
    private final DataObject context;

    public BrandTopComponentAction() {
        context = Utilities.actionsGlobalContext().lookup(DataObject.class);
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        String message = context.getPrimaryFile().getPath();
        StatusDisplayer.getDefault().setStatusText(message);
    }
    
}

That implies I've created (in a separate module to where it is used) a new annotation. Here's the definition:

package org.netbeans.sbas.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface SuperclassBasedActionAnnotation {

    String type();
    
    String path();

    int position();
    
    String displayName();
    
}

And here's the processor:

package org.netbeans.sbas.annotations;

import java.util.Set;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.openide.filesystems.annotations.LayerBuilder.File;
import org.openide.filesystems.annotations.LayerGeneratingProcessor;
import org.openide.filesystems.annotations.LayerGenerationException;
import org.openide.util.lookup.ServiceProvider;

@ServiceProvider(service = Processor.class)
@SupportedAnnotationTypes("org.netbeans.sbas.annotations.SuperclassBasedActionAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_6)    
public class SuperclassBasedActionProcessor extends LayerGeneratingProcessor {

    @Override
    protected boolean handleProcess(Set annotations, RoundEnvironment roundEnv) throws LayerGenerationException {
         Elements elements = processingEnv.getElementUtils();
        for (Element e : roundEnv.getElementsAnnotatedWith(SuperclassBasedActionAnnotation.class)) {
            TypeElement clazz = (TypeElement) e;
            SuperclassBasedActionAnnotation mpm = clazz.getAnnotation(SuperclassBasedActionAnnotation.class);
            String teName = elements.getBinaryName(clazz).toString();
            String originalFile = "Actions/" + mpm.path() + "/" + teName.replace('.', '-') + ".instance";
            File actionFile = layer(e).file(
                    originalFile).
                    bundlevalue("displayName", mpm.displayName()).
                    methodvalue("instanceCreate", "org.netbeans.sbas.annotations.SuperclassSensitiveAction", "create").
                    stringvalue("type", mpm.type()).
                    newvalue("delegate", teName);
            actionFile.write();
            File javaPopupFile = layer(e).file(
                    "Loaders/text/x-java/Actions/" + teName.replace('.', '-') + ".shadow").
                    stringvalue("originalFile", originalFile).
                    intvalue("position", mpm.position());
            javaPopupFile.write();
        }
        return true;
    }
    
}

The "SuperclassSensitiveAction" referred to in the code above is unchanged from how I had it in yesterday's blog entry.

When I build the module containing two action listeners that use my new annotation, the generated layer file looks as follows, which is identical to the layer file entries I hard coded yesterday:

<folder name="Actions">
    <folder name="File">
        <file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance">
            <attr name="displayName" stringvalue="Process Action Listener"/>
            <attr methodvalue="org.netbeans.sbas.annotations.SuperclassSensitiveAction.create" name="instanceCreate"/>
            <attr name="type" stringvalue="java.awt.event.ActionListener"/>
            <attr name="delegate" newvalue="org.netbeans.sbas.impl.ActionListenerSensitiveAction"/>
        </file>
        <file name="org-netbeans-sbas-impl-BrandTopComponentAction.instance">
            <attr bundlevalue="org.netbeans.sbas.impl.Bundle#CTL_BrandTopComponentAction" name="displayName"/>
            <attr methodvalue="org.netbeans.sbas.annotations.SuperclassSensitiveAction.create" name="instanceCreate"/>
            <attr name="type" stringvalue="org.openide.windows.TopComponent"/>
            <attr name="delegate" newvalue="org.netbeans.sbas.impl.BrandTopComponentAction"/>
        </file>
    </folder>
</folder>
<folder name="Loaders">
    <folder name="text">
        <folder name="x-java">
            <folder name="Actions">
                <file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.shadow">
                    <attr name="originalFile" stringvalue="Actions/File/org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance"/>
                    <attr intvalue="10" name="position"/>
                </file>
                <file name="org-netbeans-sbas-impl-BrandTopComponentAction.shadow">
                    <attr name="originalFile" stringvalue="Actions/File/org-netbeans-sbas-impl-BrandTopComponentAction.instance"/>
                    <attr intvalue="30" name="position"/>
                </file>
            </folder>
        </folder>
    </folder>
</folder>

Tuesday Jun 26, 2012

Superclass Sensitive Actions

I've created a small piece of functionality that enables you to create actions for Java classes in the IDE. When the user right-clicks on a Java class, they will see one or more actions depending on the superclass of the selected class.

To explain this visually, here I have "BlaTopComponent.java". I right-click on its node in the Projects window and I see "This is a TopComponent":

Indeed, when you look at the source code of "BlaTopComponent.java", you'll see that it implements the TopComponent class. Next, in the screenshot below, you see that I have right-click a different class. In this case, there's an action available because the selected class implements the ActionListener class.

Then, take a look at this one. Here both TopComponent and ActionListener are superclasses of the current class, hence both the actions are available to be invoked:

Finally, here's a class that subclasses neither TopComponent nor ActionListener, hence neither of the actions that I created for doing something that relates to TopComponents or ActionListeners is available, since those actions are irrelevant in this context:

How does this work? Well, it's a combination of my blog entries "Generic Node Popup Registration Solution" and "Showing an Action on a TopComponent Node".

The cool part is that the definition of the two actions that you see above is remarkably trivial:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import org.openide.loaders.DataObject;
import org.openide.util.Utilities;

public class TopComponentSensitiveAction implements ActionListener {
    
    private final DataObject context;

    public TopComponentSensitiveAction() {
        context = Utilities.actionsGlobalContext().lookup(DataObject.class);
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        //Do something with the context:
        JOptionPane.showMessageDialog(null, "TopComponent: " + 
                context.getNodeDelegate().getDisplayName());
    }
    
}

The above is the action that will be available if you right-click a Java class that extends TopComponent. This, in turn, is the action that will be available if you right-click a Java class that implements ActionListener:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import org.openide.loaders.DataObject;
import org.openide.util.Utilities;

public class ActionListenerSensitiveAction implements ActionListener {
    
    private final DataObject context;

    public ActionListenerSensitiveAction() {
        context = Utilities.actionsGlobalContext().lookup(DataObject.class);
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        //Do something with the context:
        JOptionPane.showMessageDialog(null, "ActionListener: " + 
                context.getNodeDelegate().getDisplayName());
    }
    
}

Indeed, the classes, at this stage are the same. But, depending on what I want to do with TopComponents or ActionListeners, I now have a starting point, which includes access to the DataObject, from where I can get down into the source code, as shown here.

This is how the two ActionListeners that you see defined above are registered in the layer, which could ultimately be done via annotations on the ActionListeners, of course:

<folder name="Actions">
    <folder name="Tools">
        <file name="org-netbeans-sbas-impl-TopComponentSensitiveAction.instance">
            <attr stringvalue="This is a TopComponent" name="displayName"/>
            <attr name="instanceCreate" 
    methodvalue="org.netbeans.sbas.SuperclassSensitiveAction.create"/>
            <attr name="type" stringvalue="org.openide.windows.TopComponent"/>
            <attr name="delegate" 
    newvalue="org.netbeans.sbas.impl.TopComponentSensitiveAction"/>
        </file>
        <file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance">
            <attr stringvalue="This is an ActionListener" name="displayName"/>
            <attr name="instanceCreate" 
    methodvalue="org.netbeans.sbas.SuperclassSensitiveAction.create"/>
            <attr name="type" stringvalue="java.awt.event.ActionListener"/>
            <attr name="delegate" 
    newvalue="org.netbeans.sbas.impl.ActionListenerSensitiveAction"/>
        </file>
    </folder>
</folder>
<folder name="Loaders">
    <folder name="text">
        <folder name="x-java">
            <folder name="Actions">
                <file name="org-netbeans-sbas-impl-TopComponentSensitiveAction.shadow">
                    <attr name="originalFile" 
    stringvalue="Actions/Tools/org-netbeans-sbas-impl-TopComponentSensitiveAction.instance"/>
                    <attr intvalue="150" name="position"/>
                </file>
                <file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.shadow">
                    <attr name="originalFile" 
   stringvalue="Actions/Tools/org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance"/>
                    <attr intvalue="160" name="position"/>
                </file>
            </folder>
        </folder>
    </folder>
</folder>

The most important parts of the layer registration are the lines that are highlighted above. Those lines connect the layer to the generic action that delegates back to the action listeners defined above, as follows:

public final class SuperclassSensitiveAction extends AbstractAction implements ContextAwareAction {

  private final Map map;

  //This method is called from the layer, via "instanceCreate",
  //magically receiving a map, which contains all the attributes
  //that are defined in the layer for the file:
  static SuperclassSensitiveAction create(Map map) {
      return new SuperclassSensitiveAction(Utilities.actionsGlobalContext(), map);
  }

  public SuperclassSensitiveAction(Lookup context, Map m) {
      super(m.get("displayName").toString());
      this.map = m;
      String superclass = m.get("type").toString();
      //Enable the menu item only if 
      //we're dealing with a class of type superclass:
      JavaSource javaSource = JavaSource.forFileObject(
              context.lookup(DataObject.class).getPrimaryFile());
      try {
          javaSource.runUserActionTask(new ScanTask(this, superclass), true);
      } catch (IOException ex) {
          Exceptions.printStackTrace(ex);
      }
      //Hide the menu item if it isn't enabled:
      putValue(DynamicMenuContent.HIDE_WHEN_DISABLED, true);
  }

  @Override
  public void actionPerformed(ActionEvent ev) {
      ActionListener delegatedAction = (ActionListener)map.get("delegate");
      delegatedAction.actionPerformed(ev);
  }

  @Override
  public Action createContextAwareInstance(Lookup actionContext) {
      return new SuperclassSensitiveAction(actionContext, map);
  }

  private class ScanTask implements Task<CompilationController> {
      private SuperclassSensitiveAction action = null;
      private String superclass;
      private ScanTask(SuperclassSensitiveAction action, String superclass) {
          this.action = action;
          this.superclass = superclass;
      }
      @Override
      public void run(final CompilationController info) throws Exception {
          info.toPhase(Phase.ELEMENTS_RESOLVED);
          new EnableIfGivenSuperclassMatches(info, action, superclass).scan(
                  info.getCompilationUnit(), null);
      }
  }

  private static class EnableIfGivenSuperclassMatches extends TreePathScanner<Void, Void> {
      private CompilationInfo info;
      private final AbstractAction action;
      private final String superclassName;
      public EnableIfGivenSuperclassMatches(CompilationInfo info, 
                                              AbstractAction action, 
                                              String superclassName) {
          this.info = info;
          this.action = action;
          this.superclassName = superclassName;
      }
      @Override
      public Void visitClass(ClassTree t, Void v) {
          Element el = info.getTrees().getElement(getCurrentPath());
          if (el != null) {
              TypeElement te = (TypeElement) el;
              List<? extends TypeMirror> interfaces = te.getInterfaces();
              if (te.getSuperclass().toString().equals(superclassName)) {
                  action.setEnabled(true);
              } else {
                  action.setEnabled(false);
              }
              for (TypeMirror typeMirror : interfaces) {
                  if (typeMirror.toString().equals(superclassName)){
                      action.setEnabled(true);
                  }
              }
          }
          return null;
      }
  }

}

This is a pretty cool solution and, as you can see, very generic. Create a new ActionListener, register it in the layer so that it maps to the generic class above, and make sure to set the type attribute, which defines the superclass to which the action should be sensitive.

Monday Jun 25, 2012

BeansBinding Across Modules in a NetBeans Platform Application

Here's two TopComponents, each in a different NetBeans module. Let's use BeansBinding to synchronize the JTextField in TC2TopComponent with the data published by TC1TopComponent and received in TC2TopComponent by listening to the Lookup.

The key to getting to the solution is to have the following in TC2TopComponent, which implements LookupListener:

private BindingGroup bindingGroup = null;
private AutoBinding binding = null;

@Override
public void resultChanged(LookupEvent le) {
    if (bindingGroup != null && binding != null) {
        bindingGroup.getBinding("customerNameBinding").unbind();
    }
    if (!result.allInstances().isEmpty()){
        Customer c = result.allInstances().iterator().next();
        // put the customer into the lookup of this topcomponent,
        // so that it will remain in the lookup when focus changes
        // to this topcomponent:
        ic.set(Collections.singleton(c), null);
        bindingGroup = new BindingGroup();
        binding = Bindings.createAutoBinding(
                // a two-way binding, i.e., a change in
                // one will cause a change in the other:
                AutoBinding.UpdateStrategy.READ_WRITE,
                // source:
                c, BeanProperty.create("name"),
                // target:
                jTextField1, BeanProperty.create("text"),
                // binding name:
                "customerNameBinding");
        bindingGroup.addBinding(binding);
        bindingGroup.bind();
    }
}

I must say that this solution is preferable over what I've been doing prior to getting to this solution: I would get the customer from the resultChanged, set a class-level field to that customer, add a document listener (or action listener, which is invoked when Enter is pressed) on the text field and, when a change is detected, set the new value on the customer. All that is not needed with the above bit of code.

Then, in the node, make sure to use canRename, setName, and getDisplayName, so that when the user presses F2 on a node, the display name can be changed. In other words, when the user types something different in the node display name after pressing F2, the underlying customer name is changed, which happens, in the first place, because the customer name is bound to the text field's value, so that the text field's value will also change once enter is pressed on the changed node display name.

Also set a PropertyChangeListener on the node (which implies you need to add property change support to the customer object), so that when the customer object changes (which happens, in the second place, via a change in the value of the text field, as defined in the binding defined above), the node display name is updated.

In other words, there's still a bit of plumbing you need to include. But less than before and the nasty class-level field for storing the customer in the TC2TopComponent is no longer needed. And a listener on the text field, with a property change listener implented on the TC2TopComponent, isn't needed either. On the other hand, it's more code than I was using before and I've had to include the BeansBinding JAR, which adds a bit of overhead to my application, without much additional functionality over what I was doing originally. I'd lean towards not doing things this way. Seems quite expensive for essentially replacing a listener on a text field and a property change listener implemented on the TC2TopComponent for being notified of changes to the customer so that the text field can be updated. On the other other hand, it's kind of nice that all this listening-related code is centralized in one place now.

So, here's a nice improvement over the above. Instead of listening for a customer, listen for a node, from which the customer can be obtained. Then, bind the node display name to the text field's value, so that when the user types in the text field, the node display name is updated. That saves you from having to listen in the node for changes to the customer's name. In addition to that binding, keep the previous binding, because the previous binding connects the customer name to the text field, so that when the customer display name is changed via F2 on the node, the text field will be updated.

private BindingGroup bindingGroup = null;
private AutoBinding nodeUpdateBinding;
private AutoBinding textFieldUpdateBinding;

@Override
public void resultChanged(LookupEvent le) {
    if (bindingGroup != null && textFieldUpdateBinding != null) {
        bindingGroup.getBinding("textFieldUpdateBinding").unbind();
    }
    if (bindingGroup != null && nodeUpdateBinding != null) {
        bindingGroup.getBinding("nodeUpdateBinding").unbind();
    }
    if (!result.allInstances().isEmpty()) {
        Node n = result.allInstances().iterator().next();
        Customer c = n.getLookup().lookup(Customer.class);
        ic.set(Collections.singleton(n), null);
        bindingGroup = new BindingGroup();
        nodeUpdateBinding = Bindings.createAutoBinding(
                AutoBinding.UpdateStrategy.READ_WRITE,
                n, BeanProperty.create("name"),
                jTextField1, BeanProperty.create("text"),
                "nodeUpdateBinding");
        bindingGroup.addBinding(nodeUpdateBinding);
        textFieldUpdateBinding = Bindings.createAutoBinding(
                AutoBinding.UpdateStrategy.READ_WRITE,
                c, BeanProperty.create("name"),
                jTextField1, BeanProperty.create("text"),
                "textFieldUpdateBinding");
        bindingGroup.addBinding(textFieldUpdateBinding);
        bindingGroup.bind();
    }
}

Now my node has no property change listener, while the customer has no property change support. As in the first bit of code, the text field doesn't have a listener either. All that listening is taken care of by the BeansBinding code. 

Thanks to Toni for help with this, though he can't be blamed for anything that is wrong with it, only thanked for anything that is right with it. 

Sunday Jun 24, 2012

Viewing the NetBeans Central Registry

For some fun, create a TopComponent and then add this bit of code, with thanks to Toni Epple:

add(new BeanTreeView(), BorderLayout.CENTER);
try {
    myExplorerManager.setRootContext(DataObject.find(FileUtil.getConfigRoot()).getNodeDelegate());
} catch (DataObjectNotFoundException ex) {
    Exceptions.printStackTrace(ex);
}

Run the application and you'll see the NetBeans Central Registry (a.k.a. System FileSystem) of the application you're running.

Friday Jun 22, 2012

Branding Support for TopComponents

In yesterday's blog entry, you saw how a menu item can be created, in this case with the label "Brand", especially for Java classes that extend TopComponent:

And, as you can see here, it's not about the name of the class, i.e., not because the class above is named "BlaTopComponent" because below the "Brand" men item is also available for the class named "Bla":

Both the files BlaTopComponent.java and Bla.java have the "Brand" menu item available, because both extend the "org.openide.windows.TopComponent"  class, as shown yesterday.

Now we continue by creating a new JPanel, with checkboxes for each part of a TopComponent that we consider to be brandable. In my case, this is the end result, at deployment, when the Brand menu item is clicked for the Bla class:


When the user (who, in this case, is a developer) clicks OK, a constructor is created and the related client properties are added, depending on which of the checkboxes are clicked:

public Bla() {
    putClientProperty(TopComponent.PROP_SLIDING_DISABLED, false);
    putClientProperty(TopComponent.PROP_UNDOCKING_DISABLED, true);
    putClientProperty(TopComponent.PROP_MAXIMIZATION_DISABLED, false);
    putClientProperty(TopComponent.PROP_CLOSING_DISABLED, true);
    putClientProperty(TopComponent.PROP_DRAGGING_DISABLED, false);
}

At this point, no check is done to see whether a constructor already exists, nor whether the client properties are already available. That's for an upcoming blog entry! Right now, the constructor is always created, regardless of whether it already exists, and the client properties are always added.

The key to all this is the 'actionPeformed' of the TopComponent, which was left empty yesterday. We start by creating a JDialog from the JPanel and we retrieve the selected state of the checkboxes defined in the JPanel:

@Override
public void actionPerformed(ActionEvent ev) {
    String msg = dobj.getName() + " Branding";
    final BrandTopComponentPanel brandTopComponentPanel = new BrandTopComponentPanel();
    dd = new DialogDescriptor(brandTopComponentPanel, msg, true, new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            Object result = dd.getValue();
            if (DialogDescriptor.OK_OPTION == result) {
                isClosing = brandTopComponentPanel.getClosingCheckBox().isSelected();
                isDragging = brandTopComponentPanel.getDraggingCheckBox().isSelected();
                isMaximization = brandTopComponentPanel.getMaximizationCheckBox().isSelected();
                isSliding = brandTopComponentPanel.getSlidingCheckBox().isSelected();
                isUndocking = brandTopComponentPanel.getUndockingCheckBox().isSelected();
                JavaSource javaSource = JavaSource.forFileObject(dobj.getPrimaryFile());
                try {
                    javaSource.runUserActionTask(new ScanTask(javaSource), true);
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }
    });
    DialogDisplayer.getDefault().createDialog(dd).setVisible(true);
} 

Then we start a scan process, which introduces the branding. We're already doing a scan process for identifying whether a class is a TopComponent. So, let's combine those two scans, branching out based on which one we're doing:

private class ScanTask implements Task<CompilationController> {

    private BrandTopComponentAction action = null;
    private JavaSource js = null;

    private ScanTask(JavaSource js) {
        this.js = js;
    }

    private ScanTask(BrandTopComponentAction action) {
        this.action = action;
    }

    @Override
    public void run(final CompilationController info) throws Exception {
        info.toPhase(Phase.ELEMENTS_RESOLVED);
        if (action != null) {
            new EnableIfTopComponentScanner(info, action).scan(
                    info.getCompilationUnit(), null);
        } else {
            introduceBranding();
        }
    }

    private void introduceBranding() throws IOException {
        CancellableTask task = new CancellableTask<WorkingCopy>() {
            @Override
            public void run(WorkingCopy workingCopy) throws IOException {
                workingCopy.toPhase(Phase.RESOLVED);
                CompilationUnitTree cut = workingCopy.getCompilationUnit();
                TreeMaker treeMaker = workingCopy.getTreeMaker();
                for (Tree typeDecl : cut.getTypeDecls()) {
                    if (Tree.Kind.CLASS == typeDecl.getKind()) {
                        ClassTree clazz = (ClassTree) typeDecl;
                        ModifiersTree methodModifiers = treeMaker.Modifiers(Collections.<Modifier>singleton(Modifier.PUBLIC));
                        MethodTree newMethod =
                                treeMaker.Method(methodModifiers,
                                "<init>",
                                treeMaker.PrimitiveType(TypeKind.VOID),
                                Collections.<TypeParameterTree>emptyList(),
                                Collections.EMPTY_LIST,
                                Collections.<ExpressionTree>emptyList(),
                                "{ putClientProperty(TopComponent.PROP_SLIDING_DISABLED, " + isSliding + ");\n"+
                                "  putClientProperty(TopComponent.PROP_UNDOCKING_DISABLED, " + isUndocking + ");\n"+
                                "  putClientProperty(TopComponent.PROP_MAXIMIZATION_DISABLED, " + isMaximization + ");\n"+
                                "  putClientProperty(TopComponent.PROP_CLOSING_DISABLED, " + isClosing + ");\n"+
                                "  putClientProperty(TopComponent.PROP_DRAGGING_DISABLED, " + isDragging + "); }\n",
                                null);
                        ClassTree modifiedClazz = treeMaker.addClassMember(clazz, newMethod);
                        workingCopy.rewrite(clazz, modifiedClazz);
                    }
                }
            }
            @Override
            public void cancel() {
            }
        };
        ModificationResult result = js.runModificationTask(task);
        result.commit();
    }
}

private static class EnableIfTopComponentScanner extends TreePathScanner<Void, Void> {

    private CompilationInfo info;
    private final AbstractAction action;

    public EnableIfTopComponentScanner(CompilationInfo info, AbstractAction action) {
        this.info = info;
        this.action = action;
    }

    @Override
    public Void visitClass(ClassTree t, Void v) {
        Element el = info.getTrees().getElement(getCurrentPath());
        if (el != null) {
            TypeElement te = (TypeElement) el;
            if (te.getSuperclass().toString().equals("org.openide.windows.TopComponent")) {
                action.setEnabled(true);
            } else {
                action.setEnabled(false);
            }
        }
        return null;
    }

}

Thursday Jun 21, 2012

Showing an Action on a TopComponent Node

Let's say you want to extend the tools in NetBeans IDE, specifically for TopComponents. When the user right-clicks in the Projects window (or Files window or Favorites window) on a Java class that extends TopComponent, a menu item should be available for branding the TopComponent. What "branding" entails is, at this stage, a secondary question. The primary question, from an implementation point of view, is "how do I create an action that is only shown when the user right-clicks on a TopComponent?"

Here's the solution, in NetBeans IDE 7.2 (the "lazy" attribute, here set to false, is new in 7.2):

import com.sun.source.tree.ClassTree;
import com.sun.source.util.TreePathScanner;
import java.awt.event.ActionEvent;
import java.io.IOException;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JOptionPane;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.Task;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionRegistration;
import org.openide.awt.DynamicMenuContent;
import org.openide.loaders.DataObject;
import org.openide.util.ContextAwareAction;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import org.openide.util.Utilities;

@ActionID(
    category = "Tools",
id = "org.tc.customizer.BrandTopComponentAction")
@ActionRegistration(
    displayName = "#CTL_BrandTopComponentAction",
    lazy = false)
@ActionReferences({
    @ActionReference(path = "Loaders/text/x-java/Actions", position = 150)
})
@Messages("CTL_BrandTopComponentAction=Brand")
public final class BrandTopComponentAction extends AbstractAction implements ContextAwareAction {

    private final DataObject dobj;

    public BrandTopComponentAction() {
        this(Utilities.actionsGlobalContext());
    }

    public BrandTopComponentAction(Lookup context) {
        super(Bundle.CTL_BrandTopComponentAction());
        this.dobj = context.lookup(DataObject.class);
        //Enable the menu item only if we're dealing with a TopComponent
        JavaSource javaSource = JavaSource.forFileObject(dobj.getPrimaryFile());
        try {
            javaSource.runUserActionTask(new ScanForTopComponentTask(this), true);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        //Hide the menu item if it isn't enabled:
        putValue(DynamicMenuContent.HIDE_WHEN_DISABLED, true);
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        JOptionPane.showMessageDialog(null, "Hurray, I am a TopComponent!");
        //Now add your code for showing a dialog,
        //where the dialog will display UI for branding the TopComponent somehow
        //and retrieve those branding values
        //and then change the TopComponent class accordingly.
    }

    @Override
    public Action createContextAwareInstance(Lookup actionContext) {
        return new BrandTopComponentAction(actionContext);
    }

    private static class ScanForTopComponentTask implements Task<CompilationController> {
        private final BrandTopComponentAction action;
        private ScanForTopComponentTask(BrandTopComponentAction action) {
            this.action = action;
        }
        @Override
        public void run(CompilationController compilationController) throws Exception {
            compilationController.toPhase(Phase.ELEMENTS_RESOLVED);
            new MemberVisitor(compilationController, action).scan(
                    compilationController.getCompilationUnit(), null);
        }
    }
 
    private static class MemberVisitor extends TreePathScanner<Void, Void> {
        private CompilationInfo info;
        private final AbstractAction action;
        public MemberVisitor(CompilationInfo info, AbstractAction action) {
            this.info = info;
            this.action = action;
        }
        @Override
        public Void visitClass(ClassTree t, Void v) {
            Element el = info.getTrees().getElement(getCurrentPath());
            if (el != null) {
                TypeElement te = (TypeElement) el;
                if (te.getSuperclass().toString().equals("org.openide.windows.TopComponent")){
                    action.setEnabled(true);
                } else {
                    action.setEnabled(false);
                }
            }
            return null;
        }
    }

}

The code above is the result of combining various tutorials found on the NetBeans Platform Learning Trail.

Wednesday Jun 20, 2012

Alt Key + Mouse Scroll is the New Text Zoom In/Out in NetBeans

When the text zoom in/out, via "Ctrl Key + Mouse Wheel", was introduced in editors in a recent version of NetBeans IDE, many people cheered. Others booed because the combination "Ctrl Key + Mouse Wheel" is often pressed accidentally, especially when the user scrolls in the editor while intending to use some Ctrl shortcut, such as paste, which is Ctrl-v.

So, in NetBeans IDE 7.2, the text zoom in/out is now "Alt Key + Mouse Wheel":

http://netbeans.org/bugzilla/show_bug.cgi?id=212484

Remember that the text change only persists for as long as the file is open. So, if you've accidentally resized the text (i.e., in the current situation, prior to 7.2, where unintended side effects may happen because of Ctrl key usage), you can just close the file and reopen it to get the text size back to the way it was before.

Tuesday Jun 19, 2012

Frinika on NetBeans Platform

Interesting looking music application on the NetBeans Platform, a port of Frinika:

Related thread, from some years ago:

http://sourceforge.net/projects/frinika/forums/forum/447356/topic/3846547

I'd be happy to help with this effort, if someone picks it up again!

Monday Jun 18, 2012

Smarter Search Results in NetBeans IDE 7.2

After you search your code using NetBeans IDE (using Ctrl-F for "Find" or Ctrl-H for "Replace"), you see the Search Results window, which looks like this:

At least, the above is how it looks in NetBeans IDE 7.2. Before that, you didn't have all those extra columns (which can be displayed in the Search Results window after clicking the small button top right in the view) and you also didn't have the quick search (which is invoked by typing directly into the Search Results window), as can be seen here:

So, the Search Results window now provides a lot more info than before. Being able to know the path to a file I've found, as well as the last modification date, file size, and the number of matches within the file, is useful at the end of a search process.

In the NetBeans IDE 7.2 New & Noteworthy, the above changes are described in the Utilities section, as well as in the Quick Search in OutlineView section, where you can read that these are generic solutions that can be used in your own OutlineViews. Other OutlineViews in NetBeans IDE 7.2, such as the Debugger window, now also have these new features.

A related article worth reading is Beefed Up Code Navigation Tools in NetBeans IDE 7.2

Sunday Jun 17, 2012

Reduced Tree View in NetBeans IDE 7.2

Right-click within the Projects window in NetBeans IDE 7.2 and from the "View Java Packages As" menu, you can now choose "Reduced Tree".

I never really understood the difference between "Reduced Tree" and the already existing "Tree". But it makes sense when you see it. Here's Reduced Tree view:

And here's Tree view. Tree view is just a plain folder tree, like you would see in the Files window; it scales well and has a clear UI, but you have to expand a lot of nodes in a typical project to find anything.

What's cool is that your selected package view is persisted across restarts of the IDE.

To be complete, here's the List view. List view is a flat list of all packages with no nesting displayed. It can make it quicker to jump to a particular package, though as you can see it can take up a lot of horizontal space. List view can also become overwhelmingly long - and slow to compute - in a very large project.

Seems to me like the new Reduced Tree view combines the best of the Tree view with the best of the List view!

Related issue: http://netbeans.org/bugzilla/show_bug.cgi?id=53192

Saturday Jun 16, 2012

NetBeans "Find Usages" Tool Integrates JSF Expression Language

I saw this by Adam on Twitter today:

Interesting. Let's try it. Here's my method "getCustomerId". I select it, right-click, and choose "Find Usages" (or press Alt-F7):

A nice dialog appears:


Then click "Find" and, guess what, this is what I see (click to enlarge it):

Clearly, as you can see, I'm not only finding the Java controller class where the getter is used, but also the Facelets files, and, within those, the exact lines where the JSF expression language makes use of the getter.

This is not a new feature, tried it and got the same result in 7.1.1, but it's really cool to know about nonetheless.

Friday Jun 15, 2012

Code Coverage for Maven Integrated in NetBeans IDE 7.2

In NetBeans IDE 7.2, JaCoCo is supported natively, i.e., out of the box, as a code coverage engine for Maven projects, since Cobertura does not work with JDK 7 language constructs. (Although, note that Cobertura is supported as well in NetBeans IDE 7.2.) It isn't part of NetBeans IDE 7.2 Beta, so don't even try there; you need some development build from after that. I downloaded the latest development build today.

To enable JaCoCo features in NetBeans IDE, you need do no different to what you'd do when enabling JaCoCo in Maven itself, which is rather wonderful. In both cases, all you need to do is add this to the "plugins" section of your POM:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.5.7.201204190339</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Now you're done and ready to examine the code coverage of your tests, whether they are JUnit or TestNG. At this point, i.e., for no other reason than that you added the above snippet into your POM, you will have a new Code Coverage menu when you right-click on the project node:

If you click Show Report above, the Code Coverage Report window opens. Here, once you've run your tests, you can actually see how many classes have been covered by your tests, which is pretty useful since 100% tests passing doesn't mean much when you've only tested one class, as you can see very graphically below:

Then, when you click the bars in the Code Coverage Report window, the class under test is shown, with the methods for which tests exist highlighted in green and those that haven't been covered in red:

(Note: Of course, striving for 100% code coverage is a bit nonsensical. For example, writing tests for your getters and setters may not be the most useful way to spend one's time. But being able to measure, and visualize, code coverage is certainly useful regardless of the percentage you're striving to achieve.)

Best of all about all this is that everything you see above is available out of the box in NetBeans IDE 7.2. Take a look at what else NetBeans IDE 7.2 brings for the first time to the world of Maven:

http://wiki.netbeans.org/NewAndNoteworthyNB72#Maven

Thursday Jun 14, 2012

Rotating a NetBeans Visual Library Widget

Trying to create a widget which, when clicked, rotates slightly further on each subsequent click:

Above, the bird where the mouse is visible has been clicked a few times and so has rotated a bit further on each click.

The code isn't quite right yet and I'm hoping someone will take this code, try it out, and help with a nice solution!

public class BirdScene extends Scene {

    public BirdScene() {
        addChild(new LayerWidget(this));
        getActions().addAction(ActionFactory.createAcceptAction(new AcceptProvider() {
            public ConnectorState isAcceptable(Widget widget, Point point, Transferable transferable) {
                Image dragImage = getImageFromTransferable(transferable);
                if (dragImage != null) {
                    JComponent view = getView();
                    Graphics2D g2 = (Graphics2D) view.getGraphics();
                    Rectangle visRect = view.getVisibleRect();
                    view.paintImmediately(visRect.x, visRect.y, visRect.width, visRect.height);
                    g2.drawImage(dragImage,
                            AffineTransform.getTranslateInstance(point.getLocation().getX(),
                            point.getLocation().getY()),
                            null);
                    return ConnectorState.ACCEPT;
                } else {
                    return ConnectorState.REJECT;
                }
            }
            public void accept(Widget widget, final Point point, Transferable transferable) {
                addChild(new BirdWidget(getScene(), getImageFromTransferable(transferable), point));
            }
        }));
    }

    private Image getImageFromTransferable(Transferable transferable) {
        Object o = null;
        try {
            o = transferable.getTransferData(DataFlavor.imageFlavor);
        } catch (IOException ex) {
        } catch (UnsupportedFlavorException ex) {
        }
        return o instanceof Image ? (Image) o : null;
    }

    private class BirdWidget extends IconNodeWidget {

        private int theta = 0;

        public BirdWidget(Scene scene, Image imageFromTransferable, Point point) {
            super(scene);
            setImage(imageFromTransferable);
            setPreferredLocation(point);
            setCheckClipping(true);
            getActions().addAction(ActionFactory.createMoveAction());
            getActions().addAction(ActionFactory.createSelectAction(new SelectProvider() {
                public boolean isAimingAllowed(Widget widget, Point localLocation, boolean invertSelection) {
                    return true;
                }
                public boolean isSelectionAllowed(Widget widget, Point localLocation, boolean invertSelection) {
                    return true;
                }
                public void select(final Widget widget, Point localLocation, boolean invertSelection) {
                    theta = (theta + 100) % 360;
                    repaint();
                    getScene().validate();
                }
            }));
        }

        @Override
        public void paintWidget() {
            final Image image = getImageWidget().getImage();
            Graphics2D g = getGraphics();
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            Rectangle bounds = getClientArea();
            AffineTransform newXform = g.getTransform();
            int xRot = image.getWidth(null) / 2;
            int yRot = image.getWidth(null) / 2;
            newXform.rotate(theta * Math.PI / 180, xRot, yRot);
            g.setTransform(newXform);
            g.drawImage(image, bounds.x, bounds.y, null);
        }
 
    }
 
}

The problem relates to refreshing the scene after the rotation. But it would help if someone would just take the code above, add it to their own application, try it out, see the problem for yourself, and develop it a bit further!

Wednesday Jun 13, 2012

Enhanced Ctrl-Tab File Switcher in NetBeans IDE 7.2

Press Ctrl-Tab to toggle the file switcher in NetBeans IDE and you'll see something more expressive when you're using NetBeans IDE 7.2:

As you can see, not only open editor documents are found in the file switcher. Also open view documents, e.g., "Properties" in the screenshot above, are shown. Keep pressing Tab while holding down the Ctrl key or hold down the Ctrl key while pressing up and down to move up and down the list.

However, in addition, you can see that you can now also switch to tabs within file documents. Above, you can see I have selected "ShapeTopComponent.java" which, because it is a GUI component, has three different tabs, "Source", "Design", and "History". By pressing the right arrow key and the left arrow key, I can switch between those different tabs in the switcher, if I am still holding down the Ctrl key. Then, when the I release the Ctrl key, whichever currently selected file, at whatever selected tab, is opened.

All this thanks to JDeveloper, where this enhanced switcher was a requirement. (Read here.)

Related to all of this is the new call "TopComponent.getSubComponents", which was needed to achieve part of the above: "The new method can be used to access for example inner tabs in a multiview window." (Read here.)

Note also that there's a new branding token "WinSys.CtrlTabSwitching.In.JTable.Enabled", to disable Ctrl-Tab window switching when a JTable or a JTabbedPane has input focus, because in those contexts Ctrl-Tab consumes key strokes associated with those components. (Read here and read here.)

A happy side effect for anyone creating their generic desktop applications on the NetBeans Platform is that your users can click Ctrl-Tab to switch between open windows (which in some NetBeans Platform applications can be MANY open windows), even when you don't have any editor documents in your application:

So, thanks to JDeveloper, NetBeans Platform applications all over the world now have a handy window switcher, automatically, without you needing to write a single line of code.

Tuesday Jun 12, 2012

YouTube: How to Rewrite the Tabs in NetBeans IDE

Watch the YouTube movie below and the tabs in NetBeans IDE will end up looking totally different:

Here's all the info you'll need to achieve the above effect:

Related blog entries:

Monday Jun 11, 2012

"previousMode": Controling the Pin Action of a TopComponent

An excellent thing I learned today is that you, as a developer of a NetBeans module or NetBeans Platform application, can control the pin button. Up until today, whenever I had a TopComponent defined to appear in "rightSlidingSide" mode and then I clicked the "pin" button, as shown here...

...the TopComponent would then find itself pinned in the "explorer" mode. Would make more sense if it would be pinned in the "properties" mode, which is the docked mode closest to the "rightSlidingSide" mode. Not being able to control the "pin" button has been a recurring question (including in my own head) over several years.

But the NetBeans Team's window system guru Stan Aubrecht informed me today that a "previousMode" attribute exists in the "tc-ref" file of the TopComponent. Since a few releases, that file is generated via the annotations in the TopComponent. However, "previousMode" is currently not one of the attributes exposed by the @TopComponent.Registration annotation.

Therefore, what I did was this:

  1. Set "rightSlidingSide" in the "mode" attribute of the @TopComponent.Registration.

  2. Build the module.

  3. Find the "generated-layer.xml" (in the Files window) and move the layer registration of the TopComponent, including its action and menu item for opening the TopComponent, into my own manual layer within the module. Then remove all the TopComponent annotations from the TopComponent, though you can keep @ConvertAsProperties and @Messages.

  4. Then add the "previousMode" attribute, as highlighted below, into my own layer file, i.e., within the tags copied from the "generated-layer.xml":

    <folder name="Modes">
        <folder name="rightSlidingSide">
            <file name="ComparatorTopComponent.wstcref">
            <![CDATA[<?xml version="1.0" encoding="UTF-8"?>
            <!DOCTYPE tc-ref PUBLIC "-//NetBeans//DTD Top Component in Mode Properties 2.0//EN" 
                "http://www.netbeans.org/dtds/tc-ref2_0.dtd">
            <tc-ref version="2.0">
              <tc-id id="ComparatorTopComponent"/>
              <state opened="false"/>
              <previousMode name="properties" index="0" />
            </tc-ref>
            ]]>
            </file>
        </folder>
    </folder>

Now when you run the application and pin the specific TopComponent defined above, i.e., in the above case, named "ComparatorTopComponent", you will find it is pinned into the "properties" mode! That's pretty cool and if you agree, then you're a pretty cool NetBeans Platform developer, and I'd love to find out more about the application/s you're creating on the NetBeans Platform!

Meanwhile, I'm going to create an issue for exposing the "previousMode" attribute in the @TopComponent.Registration annotation.

Sunday Jun 10, 2012

Two Hidden NetBeans Keyboard Shortcuts for Opening & Toggling between Views

The following are two really basic shortcuts for working with NetBeans editor windows that will be added to the Keyboard Shortcuts card (always accessible under the Help menu) for NetBeans IDE 7.2:
  1. Ctrl-Alt-PgUp/PgDown: Shortcuts for switching between editor types (e.g. Source, Design, History buttons). Switching between the editor types is a frequent operation sometimes, e.g., when using GUI builder, and while it can be done easily via mouse, or from View | Editors menu, it is very handy to know the shortcuts as well.

  2. Ctrl-PgUp/PgDown: Similarly, these are shortcuts for switching to next/previous opened document (tab). Note this is not like Ctrl-Tab that cycles in the last used order, but going through the tabs as they appear in the editor.

Both shortcuts should fit into the "Opening and Toggling between Views" section. These are important to mention on the card because they are not visible anywhere else in the UI (as there are no menu items like "Go to next/previous editor type" or "Go to next/previous document").

Reported by Tomas Pavek from the NetBeans Team, here: http://netbeans.org/bugzilla/show_bug.cgi?id=213815

About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« June 2012 »
SunMonTueWedThuFriSat
     
23
       
Today