Sunday Mar 31, 2013

Eight Years Of Blogging on NetBeans

It is now 8 years since I started this blog, i.e., it was 31 March 2005 when, inspired by my colleague at the time Roumen Strobl, I hesitantly began my existence as a blogger: https://blogs.oracle.com/geertjan/date/200503

What pearls of wisdom do I have to show for these 8 years of blogging? Not only have I been blogging, more or less daily, for 8 years, but I have done so almost exclusively about one very specific software product, i.e., of course, NetBeans IDE. Come to think of it, I kind of suspect that this may be the only blog written, pretty much daily, by a single person for 8 years on one software product. Other blogs have been written for as long and probably longer, but are there any out there that have been written by one person (i.e., not a team, but a single individual) on one software product? I've done zero research into this question at all, not even bothered to Google for 'competitors', so I'm likely to be inundated with links to other similar longliving blogs written by other individuals out there.

What can be said about individuals that blog consecutively on one specific thing for 8 years? Clearly, on the face of it, one suspects a certain level of fanaticism and possible mental aberrations of various kinds. Would I have been blogging this long and this monomanically about NetBeans IDE for 8 years had I not been paid to write this blog? But I am not paid to write this blog. If I didn't blog at all, Oracle would continue to pay me, as Sun Microsystems would also have done during the time that I was employed by Sun. Blogging is not my function for the organizations where I have been blogging; under Sun I was a technical writer and under Oracle I am a product manager. Though blogging certainly helps a lot with those functions, they're not a requirement and many technical writers and product managers don't blog at all and certainly not as frequently as me. Of course, on the other hand, I haven't been blogging about lawnmowers or garden gnomes, because neither Oracle (and, before that, Sun) are in the lawnmower nor garden gnome business.

More closer to home, I haven't been blogging about other software technologies, only NetBeans IDE. But is that really true? I've blogged a considerable amount about technologies which don't relate to NetBeans IDE at all. Or at least not directly, since every software technology relates to an IDE in the sense that the IDE potentially provides tools for it. For example, I've blogged (and openly promoted) technologies such as Wicket, Gradle, and Groovy. And this blog has not always followed the "party line" of the organization for which I work. It hasn't followed whatever trend is currently interesting. Instead, I have consistently argued a number of contentious positions, some of which have not been welcomed amongst those with whom I work. Not frequently, of course, but where it mattered, to me. For example, in several instances I have argued against curent trends and in favor of well established approaches to solving software problems. And on a different note, I've also, via this blog, facilitated a lot of interaction between developers working on disparate solutions and technologies, some of which were completely unrelated to NetBeans IDE.

In short, and personally, I don't feel that I have been promoting anything with which I myself don't feel comfortable. I've been completely (and in some cases, on later reading, maybe too) frank and honest and never hyped anything for the sake of it or for the sake of anyone other than my own understanding of the value of a particular technology or solution. And that also brings me back round to the questions with which I started this blog entry. I think the single pearl of wisdom that I carry away (and onwards to the next 8 years) with me is that a blog such as this, i.e., one focused on a product, by an employee of the company producing the product, can only "work" in the sense of it being edifying for both its writer and reader, if it is completely true to itself. So long as one is willing to take a few risks, occasionally skirting the lines of the agreeable, as far as possible with an undertone of self reflection and humor, a product-centered blog has a valid place in the larger ecosystem of which it is a part.

What also helps a lot is if the product in question is interesting and open to close and varied scrutiny. That certainly is the case for NetBeans. Wow, what an interesting and multilayered product! On its surface, sure, one can blog on how to use various features. But much more interesting is blogging on how to MAKE those features. That's where creativity and endless explorations can lead to an array of insights useful to software developers in any and every domain imaginable. It's been great, meeting and sharing and learning with so many people over the past years. Here's to the next 8! 

Saturday Mar 30, 2013

Adding & Removing Connection Widgets

Let's extend the example referred to yesterday. We'll let the user connect widgets with each other, after the Ctrl key is pressed and the mouse is moved from one widget to another.

Add the below to the AccountBuchWidget class:

private class AccountBuchConnectProvider implements ConnectProvider {
    @Override
    public boolean isSourceWidget(Widget source) {
        return source instanceof AccountBuchWidget
                && source != null ? true : false;
    }
    @Override
    public ConnectorState isTargetWidget(Widget src, Widget trg) {
        return src != trg && trg instanceof AccountBuchWidget
                ? ConnectorState.ACCEPT : ConnectorState.REJECT;
    }
    @Override
    public boolean hasCustomTargetWidgetResolver(Scene arg0) {
        return false;
    }
    @Override
    public Widget resolveTargetWidget(Scene arg0, Point arg1) {
        return null;
    }
    @Override
    public void createConnection(Widget source, Widget target) {
        ConnectionWidget conn = new ConnectionWidget(scene);
        conn.setTargetAnchorShape(AnchorShape.TRIANGLE_FILLED);
        conn.setTargetAnchor(AnchorFactory.createRectangularAnchor(target));
        conn.setSourceAnchor(AnchorFactory.createRectangularAnchor(source));
        connectionLayerWidget.addChild(conn);
    }
}

Next, in the TopComponent constructor, create a new LayerWidget, add it to the Scene, and change the initialization of the AccountBuchWidget so that the new LayerWidget is passed to the AccountBuchWidget constructor:

        final LayerWidget connectionLayerWidget = new LayerWidget(scene);

        layerWidget.addChild(new AccountBuchWidget(scene, ab, point, connectionLayerWidget));

        scene.addChild(connectionLayerWidget);

Then you can add the AccountBuchConnectionProvider to the list of Actions in the constructor of the AccountBuchWidget:

        getActions().addAction(ActionFactory.createExtendedConnectAction(
           connectionLayerWidget,
           new AccountBuchConnectProvider())
        );

Finally, if you incorporated the Delete key, mentioned by Mike in the comments yesterday, make sure to also remove the related ConnectionWidgets when deleting an AccountBuchWidget:

} else if (event.getKeyCode() == KeyEvent.VK_DELETE) {
    widget.removeFromParent();
    List<Widget> connectionsToRemove = new ArrayList<Widget>();
    for (Widget clwKid : connectionLayerWidget.getChildren()) {
        ConnectionWidget connectionWidget = (ConnectionWidget) clwKid;
        if (connectionWidget.getSourceAnchor().getRelatedWidget().equals(widget)) {
            connectionsToRemove.add(connectionWidget);
        }
        if (connectionWidget.getTargetAnchor().getRelatedWidget().equals(widget)) {
            connectionsToRemove.add(connectionWidget);
        }
    }
    connectionLayerWidget.removeChildren(connectionsToRemove);
}

Tip: Make sure to hold down the Ctrl key when using the mouse to connect two widgets.

Friday Mar 29, 2013

Keyboard Move Action for NetBeans Visual Library

By default, the MoveAction on Widgets in the NetBeans Visual Library lets the user move widgets via the mouse. But what about the keyboard? Using the code below, whenever the user presses Up/Down/Left/Right on a widget, it will move 20 pixels in that direction.

Note: Some little gotchas to be aware of are also solved below, e.g., include a SelectProvider so that when a widget is selected, it receives the focus and, more importantly, so that key event processing is correct.

public class AccountBuchWidget extends VMDNodeWidget {

    public AccountBuchWidget(final Scene scene, AccountBuch ab, Point point) {
        super(scene);
        setNodeName(ab.getDescription());
        setPreferredLocation(point);
        getActions().addAction(new KeyboardMoveAction());
        getActions().addAction(ActionFactory.createSelectAction(new SelectProvider() {
            @Override
            public boolean isAimingAllowed(Widget widget, Point localLocation, boolean invertSelection) {
                return true;
            }
            @Override
            public boolean isSelectionAllowed(Widget widget, Point localLocation, boolean invertSelection) {
                return true;
            }
            @Override
            public void select(Widget widget, Point localLocation, boolean invertSelection) {
                scene.setFocusedWidget(widget);
            }
        }));
    }

    private final class KeyboardMoveAction extends WidgetAction.Adapter {
        private MoveProvider provider;
        private KeyboardMoveAction() {
            this.provider = ActionFactory.createDefaultMoveProvider();
        }
        @Override
        public WidgetAction.State keyPressed(Widget widget, WidgetAction.WidgetKeyEvent event) {
            Point originalSceneLocation = provider.getOriginalLocation(widget);
            int newY = originalSceneLocation.y;
            int newX = originalSceneLocation.x;
            if (event.getKeyCode() == KeyEvent.VK_UP) {
                newY =
newY - 20;
            } else if (event.getKeyCode() == KeyEvent.VK_DOWN) {
                newY =
newY + 20;
            } else if (event.getKeyCode() == KeyEvent.VK_RIGHT) {
                newX =
newX + 20;
            } else if (event.getKeyCode() == KeyEvent.VK_LEFT) {
                newX =
newX - 20;
            }
            provider.movementStarted(widget);
            provider.setNewLocation(widget, new Point(newX, newY));
            return State.CONSUMED;
        }
        @Override
        public WidgetAction.State keyReleased(Widget widget, WidgetAction.WidgetKeyEvent event) {
            provider.movementFinished(widget);
            return State.REJECTED;
        }
    }

}

Here's the constructor of the related TopComponent, note especially the call to "setKeyEventProcessingType":

public EditorTopComponent() {

    initComponents();
    setName(Bundle.CTL_EditorTopComponent());
    setToolTipText(Bundle.HINT_EditorTopComponent());

    setLayout(new BorderLayout());

    JScrollPane pane = new JScrollPane();

    final LayerWidget layerWidget = new LayerWidget(scene);

    scene.getActions().addAction(ActionFactory.createAcceptAction(new AcceptProvider() {
        @Override
        public ConnectorState isAcceptable(Widget widget, Point point, Transferable t) {
            return ConnectorState.ACCEPT;
        }
        @Override
        public void accept(Widget widget, Point point, Transferable t) {
            Node node = NodeTransfer.node(t, NodeTransfer.DND_COPY_OR_MOVE);
            final AccountBuch ab = node.getLookup().lookup(AccountBuch.class);
            layerWidget.addChild(new AccountBuchWidget(scene, ab, point));
        }
    }));

    scene.setKeyEventProcessingType(EventProcessingType.FOCUSED_WIDGET_AND_ITS_CHILDREN);

    scene.addChild(layerWidget);

    pane.setViewportView(scene.createView());

    add(pane, BorderLayout.CENTER);

}

Also, make sure the TopComponent above includes this so that focus is on the Scene when the TopComponent is activated:

    @Override
    public void componentActivated() {
        scene.getView().requestFocusInWindow();
    }

For extra usability, you could include the above Action together with the default MoveAction:

    getActions().addAction(ActionFactory.createMoveAction());
    getActions().addAction(new KeyboardMoveAction());

Now your user can move widgets via the mouse as well as the keyboard. Maybe the keyboard would be used for finetuning the larger movements made by the mouse.

Monday Mar 25, 2013

Malkovich Moment: Gradle, Gradle, Gradle

What happens when you open Gradle, which is built on Gradle, via the NetBeans Gradle plugin? Well, the sky inverts itself and cats start falling from the clouds. Shortly thereafter, though, things start looking pretty sweet (click to enlarge the pic below):

Note: I didn't use the Gradle plugin from the NetBeans Plugin Portal, but directly from the Git repo, where the latest changes are found:

https://github.com/kelemen/netbeans-gradle-project

I also needed to make sure that the NetBeans Groovy plugins are installed from the Plugin Manager. Then I opened the sources above into the IDE, right-clicked the project node, and installed the sources directly into the running IDE.

Also, I downloaded the very latest Gradle distro, which right now is 1.5 RC 3, from here:

http://www.gradle.org/release-candidate

In the Options window of NetBeans IDE 7.3, I registered the Gradle distro like this:


Congratulations to Attila Kelemen for the brilliant work he's been doing on this plugin.

Friday Mar 22, 2013

YouTube: Displaying Realtime Data on the Java Desktop

In response to a recent request in this blog, here's a quick (slightly over 10 minutes) YouTube movie (without sound) showing how to display real-time data in a Java desktop application on the NetBeans Platform:

Thursday Mar 21, 2013

Consolas, Ubuntu, NetBeans IDE 7.3

I followed these instructions, and then found that I had Consolas available in NetBeans IDE 7.3, on Ubuntu:


Initially, the fonts in the editor looked as shown here.

Then, after increasing the font size and type to 14 and Bold, in the NetBeans Options window, the fonts were like this:


I'm not a hardcore Consolas user, so wondering if this is what Consolas users would expect?

Wednesday Mar 20, 2013

Set Up Couchbase Java Client Sources in NetBeans IDE

The Couchebase Java Client is a library for other clients to implement. The Couchbase team uses Ant to test and create the JAR, while using both Ivy and Maven for dependency related tasks. In other words, a highly customized project structure. How to set it up in NetBeans IDE? The end goal is this:

I.e., no red error marks, and [which you can't see above], code completion working, as well as compilation of the application. Also, the ability to run single test classes is a requirement.

Therefore, use the Java Free-Form Project Type and point to the root folder of the download, once you've got it from Git:

https://github.com/couchbase/couchbase-java-client 

Spend some time configuring the project, with this end result:

Below, for the Source Packages, select all the JARs from build/ivy/lib/couchbase-client/common. Then, for the Test Packages, select all JARs from build/ivy/lib/couchbase-client/common AND from build/jars/*.jar:

Everything below is defined as you see it by default, except for the mapping of "jar" to Build Project and "mvn-install" to Run Project:

What's great is that all these configurations have no impact on the original build.xml file. Nevertheless, one small, but very important, tweak should be done to the 'clean' target in the build.xml file, so that the "nbproject" file is not deleted when clean target is run. 

Next, let's make it possible to, as requested, provide the ability to run individual test classes. In the project.xml file, add this to the ide-actions section:

<action name="test.single">
    <script>nbproject/ide-file-targets.xml</script>
    <target>run-selected-files-in-test</target>
    <context>
        <property>classname</property>
        <folder>src/test/java</folder>
        <pattern>\.java$</pattern>
        <format>java-name</format>
        <arity>
            <one-file-only/>
        </arity>
    </context>
</action>

Then create a new file named ide-file-targets.xml in the same folder as where project.xml is found and define it as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project basedir=".." name="couchbase-client-IDE">
    <target name='run-selected-files-in-test'>
        <fail unless='classname'>Must set property 'classname'</fail>
        <ant antfile="build.xml" inheritall="false">
            <target name="jar"/>
            <target name="srcjar"/>
            <target name="package"/>
            <target name="mvn-prep"/>
        </ant>
        <junit 
            dir='build/test/classes' 
            printsummary='true' 
            showoutput='true'
            fork='true'>
            <classpath>
                <pathelement path="build/ivy/lib/couchbase-client/common/asm-3.3.1.jar:
build/ivy/lib/couchbase-client/common/cglib-2.2.2.jar:
build/ivy/lib/couchbase-client/common/commons-codec-1.5.jar:
build/ivy/lib/couchbase-client/common/easymock-2.4.jar:
build/ivy/lib/couchbase-client/common/easymockclassextension-2.4.jar:
build/ivy/lib/couchbase-client/common/httpcore-4.1.1.jar:
build/ivy/lib/couchbase-client/common/httpcore-nio-4.1.1.jar:
build/ivy/lib/couchbase-client/common/jettison-1.1.jar:
build/ivy/lib/couchbase-client/common/jmock-1.2.0.jar:
build/ivy/lib/couchbase-client/common/junit-4.7.jar:
build/ivy/lib/couchbase-client/common/junit-addons-1.4.jar:
build/ivy/lib/couchbase-client/common/mockito-all-1.9.5.jar:
build/ivy/lib/couchbase-client/common/netty-3.5.5.Final.jar:
build/ivy/lib/couchbase-client/common/spymemcached-2.8.12.jar:
build/ivy/lib/couchbase-client/common/spymemcached-test-2.8.12.jar:
build/jars/couchbase-client-no-version.jar:
build/jars/couchbase-client-test-no-version.jar"/>
            </classpath>            
            <formatter type='brief' usefile='false'></formatter>
            <formatter type='xml'></formatter>
            <test name='${classname}'></test>
        </junit>
    </target>
</project>

Better than the above is to use "Automatic Projects", which works fine in 7.3:

http://wiki.netbeans.org/AutomaticProjects

http://plugins.netbeans.org/plugin/37522/automatic-projects

I will blog about this soon, works perfectly with Couchbase Java Client Library.

Tuesday Mar 19, 2013

Drag Nodes Into Empty NetBeans ListViews

I've blogged quite a lot (especially, in the context below, here, back in 2009) about dragging and dropping Nodes into various places. One place I hadn't looked at yet is inspired by the question of the day, provided by Geoffrey Waardenburg in a comment in this blog today: how to drag a Node into an empty ListView. So, rather than dragging a Node onto another Node, the scenario is that you have an empty ListView, i.e., containing no Nodes at all, like this:

Now you want to drag one or more Nodes into that ListView and then, on the drop, display related data there.

The first thing that you need is to have a reference to the JList which is contained within the ListView. Once you have that, you can set it as a drop target.

  1. Create a Custom ListView. Here's the code of my ListView, which exists for no other reason than to expose the underlying and protected JList:
    import javax.swing.JList;
    import org.openide.explorer.view.ListView;
    
    public class MyListView extends ListView {
    
        public JList jList;
    
        @Override
        protected JList createList() {
            jList = super.createList();
            return jList;
        }
    
        public JList getjList() {
            return jList;
        }
        
    }
  2. Listen for Drops on the JList in the ChildFactory. An unorthodox thing to do, yet I challenge anyone to solve this in a different/better way:
    import java.awt.datatransfer.UnsupportedFlavorException;
    import java.awt.dnd.DnDConstants;
    import java.awt.dnd.DropTarget;
    import java.awt.dnd.DropTargetDragEvent;
    import java.awt.dnd.DropTargetDropEvent;
    import java.awt.dnd.DropTargetEvent;
    import java.awt.dnd.DropTargetListener;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    import javax.swing.JList;
    import org.my.domain.Customer;
    import org.openide.awt.StatusDisplayer;
    import org.openide.nodes.AbstractNode;
    import org.openide.nodes.ChildFactory;
    import org.openide.nodes.Children;
    import org.openide.nodes.Node;
    
    class NameChildFactory extends ChildFactory<Customer> {
    
        private ArrayList<Customer> names = new ArrayList<Customer>();
        private final JList list;
    
        public NameChildFactory(JList list) {
            this.list = list;
            MyDropTargetListener dtl = new MyDropTargetListener();
            DropTarget dt = new DropTarget(list, dtl);
            dt.setDefaultActions(DnDConstants.ACTION_COPY_OR_MOVE);
            dt.setActive(true);
        }
    
        @Override
        protected boolean createKeys(List<Customer> list) {
            list.addAll(names);
            return true;
        }
    
        @Override
        protected Node createNodeForKey(final Customer name) {
            Node node = new AbstractNode(Children.LEAF);
            node.setDisplayName(name.getName());
            return node;
        }
    
        public class MyDropTargetListener implements DropTargetListener {
    
            @Override
            public void drop(DropTargetDropEvent dtde) {
                if (dtde.isDataFlavorSupported(Customer.DATA_FLAVOR)) {
                    try {
                        Object transData = dtde.getTransferable().
                                getTransferData(Customer.DATA_FLAVOR);
                        if (transData instanceof Customer) {
                            dtde.acceptDrop(DnDConstants.ACTION_COPY);
                            Customer c = (Customer) dtde.getTransferable().
                                getTransferData(Customer.DATA_FLAVOR);
                            StatusDisplayer.getDefault().setStatusText(c.getName());
                            names.add(c);
                            refresh(true);
                        }
                    } catch (UnsupportedFlavorException ufe) {
                        dtde.rejectDrop();
                        dtde.dropComplete(true);
                    } catch (IOException ioe) {
                        dtde.rejectDrop();
                        dtde.dropComplete(false);
                    }
                } else {
                    dtde.rejectDrop();
                    dtde.dropComplete(false);
                }
            }
    
            @Override
            public void dragEnter(DropTargetDragEvent dtde) {}
            @Override
            public void dragExit(DropTargetEvent dtde) {}
            @Override
            public void dragOver(DropTargetDragEvent dtde) {}
            @Override
            public void dropActionChanged(DropTargetDragEvent dtde) {}
    
        }
    
    }

And that's all you need to do. When a drop takes place on the JList, update the list held by the ChildFactory, and refresh the node hierarchy. (Go here for all the other code in this sample so that you're able to recreate it.)

Sunday Mar 17, 2013

Copy Nodes in Live DOM View to Clipboard

The new HTML5 capabilities in NetBeans IDE, a.k.a. Project Easel, can be extended in many different ways. For example, you can add new Actions to the HTML Editor, JavaScript Editor, or CSS Editor.

But that's always been possible. Less well known is that you can also extend the live DOM view with new Actions, via registering Actions in the new Navigation/DOM/Actions folder:

That's a new Action I added for copying the selected DOM Nodes to the clipboard from where, as can be seen in the new Clipboard History viewer, they can be retrieved:

If a single Node is selected, the Action is shown together with the default Actions shown in the DOM Viewer:

The code:

package org.demo.feature;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collections;
import java.util.List;
import org.netbeans.modules.html.editor.lib.api.SourceElementHandle;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionRegistration;
import org.openide.filesystems.FileObject;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import org.openide.util.datatransfer.ExClipboard;

@ActionID(
        category = "Edit",
        id = "org.demo.feature.CopyDOMToClipboardAction")
@ActionRegistration(
        lazy = true,
        displayName = "#CTL_COPYNODES")
@ActionReference(
        path = "Navigation/DOM/Actions", 
        position = 0)
@Messages("CTL_COPYNODES=Copy DOM to Clipboard")
public final class CopyDOMToClipboardAction implements ActionListener {

    private Clipboard clipboard;
    private final List<Node> context;

    public CopyDOMToClipboardAction(List<Node> nodes) {
        this.context = nodes;
        clipboard = Lookup.getDefault().lookup(ExClipboard.class);
        if (clipboard == null) {
            clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < context.size(); i++) {
            Node node = context.get(i);
            final SourceElementHandle sourceElementHandle = 
                    node.getLookup().lookup(SourceElementHandle.class);
            final FileObject file = sourceElementHandle.getFileObject();
            Source source = Source.create(file);
            try {
                ParserManager.parse(Collections.singleton(source), new UserTask() {
                    @Override
                    public void run(ResultIterator resultIterator) throws Exception {
                        Parser.Result result = resultIterator.getParserResult();
                        org.netbeans.modules.html.editor.lib.api.elements.Node resolved =
                                sourceElementHandle.resolve(result);
                        String text = resolved.image().toString();
                        sb.append(text).append("\n");
                    }
                });
            } catch (ParseException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        setClipboardContents(sb.toString());
    }
    
    private void setClipboardContents(String content) {
        if (clipboard != null) {
            if (content == null) {
                clipboard.setContents(null, null);
            } else {
                clipboard.setContents(new StringSelection(content), null);
            }
        }
    }

}

Saturday Mar 16, 2013

Extending the NetBeans Favorites Window

URLs registered in the Favorites folder are rendered in the Favorites window. Rendering is done by the FavoritesNode, which is an internal class in the Favorites module, for each URL registered in the Favorites folder.

For example, here I've created a URL:

package org.my.fav;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import org.openide.filesystems.FileUtil;
import org.openide.util.Utilities;

public class ConfigRootNode {

     public static URL getNetBeansUser() throws MalformedURLException {
        String s = System.getProperty("netbeans.user"); // NOI18N
        File userDir = new File(s);
        userDir = FileUtil.normalizeFile(userDir);
        return Utilities.toURI(userDir).toURL();
    }
     
}

And here the URL above is registered:

<folder name="Favorites">
    <file name="UserDir.shadow">
        <attr name="originalFile" 
                 methodvalue="org.my.fav.ConfigRootNode.getNetBeansUser"/>
        <attr name="position" intvalue="200"/>
    </file>
</folder>

Therefore, note that the only thing you can pass into the Favorites folder is a URL, nothing else, i.e., you have no control over the rendered Node, e.g., over its children. If that's what you want, either create a Node and register it in the Services window as shown here (assuming you're extending NetBeans IDE, since that's where that window comes from, not the NetBeans Platform) or simply create your own TopComponent and render your Nodes there.

Saturday Mar 09, 2013

Play in NetBeans IDE 7.3

The NetBeans plugin for Play, by Yann D'Isanto and myself, is available for the first time in the NetBeans Plugin Portal, ready to use in NetBeans IDE 7.3:

http://plugins.netbeans.org/plugin/47637/?show=true

Install the plugin into 7.3 and then you're able to create new Play projects via the New Project dialog or open any existing Play project, such as the sample applications in the Play distro:

This is how opened Play projects look like in the IDE:


Related reading:

The sources of the plugin (in the form of a Maven module) are available here:

http://java.net/projects/nbplay/sources/nbplay

Do you want to contribute to this plugin? You are MORE than welcome to do so, please leave a message here either with your ideas or with your username on java.net so that you can be added as a committer to the project.

Issue tracker:

http://java.net/jira/browse/NBPLAY

Friday Mar 08, 2013

Python in NetBeans IDE 7.3

Via this update center (which you can register in Tools | Plugins | Settings), you'll find you have the Python plugin available in NetBeans IDE 7.3:

http://deadlock.netbeans.org/hudson/job/nbms-and-javadoc/lastStableBuild/artifact/nbbuild/nbms/updates.xml.gz

Lots of features are provided (http://wiki.netbeans.org/Python) but I especially like the ability of opening existing Python projects, such as the open source Cheetah templating engine, which I succeeded to build, as can be seen here:

Thursday Mar 07, 2013

"Look, Oprah. Take steroids and let's see you win."

I've been blogging for many years on work related issues in this work-sponsored blog and so feel that now, when things have been clearly really getting out of hand, I should be able to take a breather from all that and use this platform to shake some sense into this insane world.

"What's been clearly really getting out of hand in this insane world?" you ask. Well, these sad looking Armstrongites with their long faces and furrowed brows. "Oh Oprah, I repent, I repent." And so on.

Well, one of these doped up cyclists needs to straighten their back and say: "Look, Oprah, right here, right now, I'm going to inject EPO and 27 other steroids directly into your veins. Since that means you will then obviously be invincible, you'll be winning the next three grueling cycling tours, starting with the Tour de France or whatever ludicrous cycling race is next on the madly horrible cycling calendar! Right? Isn't that all that it takes? Isn't that what you've been implying? That it's all really easy? Simply a question of getting a shot in the arm? Well, then, bring on your arm and I'll inject you with steroids. Make some space in your book case because that's where you'll be putting your trophies."

My hero Bill Burr has similar, though different, relativizing comments:

http://www.youtube.com/watch?v=O9YL04v-J5U

Wednesday Mar 06, 2013

Why Privatizing Public Transport Does Not Work

I spent some time today with the nice instructors and students at the Fontys University of Applied Sciences in Venlo, in the south of the Netherlands. For several years already, the university has standardized all their Java courses, assignments, and exams to be done through the free and open source NetBeans IDE, thanks to its out of the box support for, in particular, Ant based projects. I went through the freely available "What's New in NetBeans IDE 7.3" slide deck while focusing, of course, on the many new HTML5 capabilities in the IDE.

On the train journey back, I was reminded of how privatizing public transport does not work:

  • In the morning, in Amsterdam, I had bought a return ticket from Amsterdam to Venlo. On the return journey, at the station in Venlo, I found that there were several blockages and delays in the train route I would normally have taken from Venlo back to Amsterdam. But, guess what? The alternative route I had to take included a section (from Venlo to Roermond) operated by Veolia Transport. "So what?", you might be thinking. Well think about this... I was not able to use the return ticket I had bought that morning in Amsterdam. It was not a valid ticket for Veolia. Instead, I had to buy a new ticket from Venlo to Roermond, operated by Veolia, after which I could use my original ticket from Roermond to Amsterdam.

  • This reminded me of a very similar occurrence at Schiphol airport in Amsterdam. Imagine you're a tourist and you arrive at Schiphol and you want to catch the train to Amsterdam central station. There are several automated sale points all over Schiphol, where you can buy your ticket. The first question you get when you press a button, or even before that, on the automated sale point is: "Do you want to travel with NS or with Fyra?" The latter is Fyra, yet another transportation company (famous for mess ups with the new connection between Holland and Belgium), while the former is the Dutch national train service, i.e., the government controled train service, the one that brought me from Amsterdam to Venlo and then from Roermond to Amsterdam. But, if you're a tourist freshly arrived at Schiphol, how on earth are you supposed to make a decision about whether to get to Amsterdam central station via the NS or Fyra? All you want to do is get there, you don't want to choose which of the two services to use, despite the fact that exactly that is supposed to be wonderful "because now we're giving freedom and choice to train travelers everywhere", especially since the whole trip takes about 10 minutes, so how much value can one service have over another? And even more especially since you're given no clue whatsoever about how you're meant to make that choice.

But, worse than the two examples above is the fact that it doesn't make any difference to any of these services that these ludicrous situations take place. It makes no difference to them. Why? Because the cost of the above problems is low to passengers, including me. E.g., I only had to pay 2.70 EUR for my trip between Venlo and Roermond, while the discomfort of tourists at Schiphol is also not very high. Neither the tourists nor me myself are going to get extremely upset, just slightly annoyed, in the greater scheme of things.

So, the cost of fixing the problem (i.e., coming up with shared protocols, for example) is higher than the cost of the inconveniences described above. And that's the REAL reason why privatizing public transport does not work. 

Tuesday Mar 05, 2013

Synchronizing an Editor Window with a Viewer Window

Now that we're able to open a TopComponent per Node, let's... synchronize back to the viewer window from the editor window.

Below you can see that right now there's a TopComponent open for each of the Nodes. However, wouldn't it be nice if selecting one of the open TopComponents would cause the related Node to automatically be selected? In other words, below I have clicked on the "professor" window, which automatically results in the "professor" node in the viewer window being highlighted:

How to achieve this? Via a very elegant NetBeans Platform mechanism.

  1. Create a "Synchronizable" Capability. Here it is, in the domain module:
    package org.person.domain.capabilities;
    
    import org.person.domain.Person;
    
    public interface Synchronizable {
        Person getPerson();
    }
  2. Add an Implementation of the Capability to the Node Lookup. You can see that the Synchronizable capability has been added to the Lookup of the Node, in the Node constructor, as shown in the highlighted bit below:
    private PersonNode(final Person person, InstanceContent ic) throws IntrospectionException {
        super(person, Children.LEAF, new AbstractLookup(ic));
        ic.add(new Openable() {
            @Override
            public void open() {
                TopComponent tc = findTopComponent(person);
                if (tc == null) {
                    tc = new PersonEditorTopComponent(person);
                    tc.open();
                }
                tc.requestActive();
            }
        });
        ic.add(new Synchronizable() {
            @Override
            public Person getPerson() {
                return person;
            }
        });
        setDisplayName(person.getType());
    }

    Note: There's no 'naked' Person object in the Node Lookup, only a Person object wrapped in a capability. This way, you have more control over the Object; since various other parts of the application may (and currently are already) be listening/responding to Person objects, using a capability gives you a fresh access point to the Object.

  3. Retrieve the Capability from the Lookup in the Viewer. In the Viewer window, implement LookupListener, listen for the current Person object, and set the Node that has the same Person object in the Synchronizable in its Lookup to be the selected Node in the ExplorerManager:
    @Override
    public void resultChanged(LookupEvent le) {
        Collection<? extends Person> p = personResult.allInstances();
        if (p.size() == 1) {
            Person currentPerson = p.iterator().next();
            for (Node node : em.getRootContext().getChildren().getNodes()) {
                if (node.getLookup().lookup(Synchronizable.class).getPerson() == currentPerson) {
                    try {
                        em.setSelectedNodes(new Node[]{node});
                    } catch (PropertyVetoException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
            }
        }
    }

And these are the three files created/changed during the above instructions:

Source code of the current state of the sample above:

http://java.net/projects/nb-api-samples/sources/api-samples/show/versions/7.3/misc/SimpleApp

Monday Mar 04, 2013

Modularity Is What You Make Of It

Right now, the application we're looking at consists of a domain module, containing the Person object, and a functionality module, containing everything else.

Let's refactor the application so that it looks like this:

Here we have a highly modulerized application. Normally, you wouldn't have the PersonOpenAction in its own module, but here I'm doing it that way just to prove that it's possible. Since the PersonOpenAction listens for the current PersonNode and then gets the Openable from the PersonNode, and calls "open" on it, which in this case opens the PersonEditorTopComponent, it could be argued that the Node, EditorTopComponent, and Action should all be found in the same module.

On the other hand, separating them in this way means that it's easier to find where each is defined. And one set of engineers could be working on the editor window, while others work on the node hierarchy, in their own workspaces, i.e., in separated modules. Also, and even more usefully, the viewer and editor windows are now found in separate modules, without any dependencies on each other. Both depend on the domain module, while the viewer also depends on the model module, since that's where the node hierarchy is found, and the model module depends on the editor module, since the Openable implementation on the Node references the editor window. 

In short, modularity is what you make of it. Further considerations can be read below:

http://java.dzone.com/news/how-to-split-into-modules

Sunday Mar 03, 2013

Creating a Customized OpenAction

The question of the day comes from Michael Bishop who, in a follow up to yesterday's blog entry, doesn't simply want the display text of the OpenAction to be "Open". He wants it to show something about the context, e.g., "Open Joe", where Joe is the name of the underlying object.

The key to solving this problem is described in Dynamically Changing the Display Names of Menus and Popups.

And so here's the code, i.e., remove the NetBeans Platform OpenAction and create your own instead:

import java.awt.event.ActionEvent;
import java.util.Collection;
import javax.swing.AbstractAction;
import org.netbeans.api.actions.Openable;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionRegistration;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;

@ActionID(
        category = "PersonActions",
        id = "org.person.viewer.PersonOpenAction")
@ActionRegistration(
        lazy = false,
        displayName = "NOT-USED")
@ActionReference(path = "Menu/File", position = 1300)
public final class PersonOpenAction extends AbstractAction implements LookupListener {

    private Lookup.Result<PersonNode> personNodeResult;
    private Openable context;

    public PersonOpenAction() {
        super("Open Person");
        personNodeResult = Utilities.actionsGlobalContext().lookupResult(PersonNode.class);
        personNodeResult.addLookupListener(
                WeakListeners.create(LookupListener.class, this, personNodeResult));
        resultChanged(new LookupEvent(personNodeResult));
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        context.open();
    }

    @Override
    public void resultChanged(LookupEvent le) {
        Collection<? extends PersonNode> p = personNodeResult.allInstances();
        if (p.size() == 1) {
            PersonNode currentPersonNode = p.iterator().next();
            context = currentPersonNode.getLookup().lookup(Openable.class);
            String displayText = "Open Person: " + currentPersonNode.getDisplayName();
            putValue("popupText", displayText);
            putValue("menuText", displayText);
        }
    }

    @Override
    public boolean isEnabled() {
        return context!=null ? true : false;
    }

}

Above, the assumption is that there's an Openable in the Node Lookup. If you're using OpenCookie or something different instead, change the code above accordingly. Secondly, above the display name of the Node is used to set the display text of the popup and menu. Of course, instead of that, you could look in the Lookup of the Node for your Object, e.g., Person, and then get some value from that Object.

Display the Action above on the Node as follows (and it is automatically available in the menu bar thanks to the @ActionReference annotation above):

@Override
public Action[] getActions(boolean context) {
    List<? extends Action> personActions = Utilities.actionsForPath("Actions/PersonActions");
    return personActions.toArray(new Action[personActions.size()]);
//        return new Action[]{SystemAction.get(OpenAction.class)};
}

Saturday Mar 02, 2013

Opening a TopComponent per Node

In the Node, displayed in a viewer TopComponent, add an OpenCookie to the Lookup, and define it such that a new editor TopComponent is created only if no currently open editor TopComponent contains the Object in its Lookup:
public class PersonNode extends BeanNode {

    public PersonNode(Person person) throws IntrospectionException {
        this(person, new InstanceContent());
    }

    private PersonNode(final Person person, InstanceContent ic) throws IntrospectionException {
        super(person, Children.LEAF, new AbstractLookup(ic));
        ic.add(new OpenCookie() {
            @Override
            public void open() {
                TopComponent tc = findTopComponent(person);
                if (tc == null) {
                    tc = new PersonEditorTopComponent(person);
                    tc.open()
                }
                tc.requestActive();
            }
        });
        setDisplayName(person.getType());
    }

    private TopComponent findTopComponent(Person person) {
        Set openTopComponents = WindowManager.getDefault().getRegistry().getOpened();
        for (TopComponent tc : openTopComponents) {
            if (tc.getLookup().lookup(Person.class) == person) {
                return tc;
            }
        }
        return null;
    }

    @Override
    public Action[] getActions(boolean context) {
        return new Action[]{SystemAction.get(OpenAction.class)};
    }
    
}

Note that we also have an OpenAction from the Actions API in the context menu of the Node. When you right-click the Node, the Open action will be enabled if there's an OpenCookie in the Lookup. As you can see in the constructor above, there is an OpenCookie in the Lookup, with the definition of what should happen when the action is invoked.

When a new TopComponent is created, immediately put the Object into the TopComponent Lookup so that the same TopComponent will be found next time Open is clicked on the Node:

public final class PersonEditorTopComponent extends TopComponent {
    
    public PersonEditorTopComponent(Person person) {
        initComponents();
        setName(person.getType());
        setToolTipText(person.getType());
        associateLookup(Lookups.singleton(person));
    }

    ...
    ...
    ...

Note: In the above TopComponent, I have removed all the annotations that are generated into the top of the TopComponent by the New Window wizard.

Complete source code for the above: http://java.net/projects/nb-api-samples/sources/api-samples/show/versions/7.3/misc/SimpleApp

PS: I tried using Openable instead of OpenCookie, but the OpenAction doesn't respond to it. 

Friday Mar 01, 2013

No Expansion Icon When No Children (Part 1)

Sometimes you have Nodes with an expansion icon, i.e., the "plus" sign, even though the Object doesn't have Children. Once the user tries to expand the Node, the "plus" sign disappears, and no Children are shown. Kind of misleading because the user thought that there would be Children because the expansion icon was shown. Would be nicer if there were to be no expansion icon if the Object has no Children.

Here's how to solve that, via Children.createLazy:

public class MyNode extends AbstractNode {

    public MyNode(NodeKey key) {
        super(Children.createLazy(new MyCallable(key)), Lookups.singleton(key));
        setDisplayName(key.toString());
    }

    private static class MyCallable implements Callable<Children> {

        private final NodeKey key;

        private MyCallable(NodeKey key) {
            this.key = key;
        }

        @Override
        public Children call() throws Exception {
            //Check, somehow, that your key has children,
            //e.g., create "hasChildren" on the object
            //to look in the database to see whether
            //the object has children;
            //if it doesn't have children, return a leaf:
            if (!key.hasChildren()) {
                return Children.LEAF;
            } else {
                return Children.create(new MyChildFactory(key), true);
            }
        }

    }

}

Now see part 2.

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
« March 2013 »
SunMonTueWedThuFriSat
     
10
11
12
13
14
15
18
23
24
27
28
      
Today