Over a million developers have joined DZone.

Quick and Dirty ContextChoiceview

· Java Zone

Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code! Brought to you in partnership with ZeroTurnaround.

A few days ago I was porting a Swing application to the NetBeans Platform. (Actually, I've been porting it for a long time already).

Then I realized that the NetBeans Platform lacks a very useful Nodes view: a ContextChoiceView. There is BeanTreeView, ChoiceView, ContextTreeView... but not a single ContextChoiceview.

So, it should not be a very difficult View to make. Almost everything is already done. Take the ChoiceView, use the ContextTreeView model, add some piping to put it all together and voilá, a ContextChoiceView:

ContextChoiceView with root node:

ContextTreeView expanded and shows the context. In addition, a ListView shows the context's contents:

ContextTreeView expanded and showing the context.

 

Unfortunately, there were some private and package visible classes I couldn't just extend. I had to refer to the source code and use the copy/paste magic. First, the model:

static final class NodeContextModel extends NodeTreeModel
                                        implements MutableComboBoxModel,
                                                   PropertyChangeListener{
        private Node selectedItem = null;
        //
        // Event filtering
        //
        private int[] newIndices;
        private Object[] newChildren;
        public NodeContextModel(Node root) {
            super(root);
        }
        public NodeContextModel() {
        }
        @Override
        public Object getChild(Object parent, int index) {
            int childCount = super.getChildCount(parent);
            int contextIndex = -1;
            for(int i = 0; ((contextIndex < index) || (i < childCount)); i++){
                Object child = super.getChild(parent, i);
                if(!isLeaf(child)){
                    contextIndex++;
                    if(contextIndex == index)
                        return child;
                }
            }
            return null;
        }
        @Override
        public int getChildCount(Object parent) {
            int childCount = super.getChildCount(parent);
            int contextIndex = 0;
            for(int i = 0; i < childCount; i++){
                Object child = super.getChild(parent, i);
                if(!isLeaf(child)){
                    contextIndex++;
                }
            }
            return contextIndex;
        }
        
        @Override
        public Object getSelectedItem() {
            return selectedItem == null?
                root:
                Visualizer.findVisualizer(selectedItem);
        }
        @Override
        public void setSelectedItem(Object anItem) {
            selectedItem = Visualizer.findNode(anItem);
        }
        @Override
        public void addListDataListener(ListDataListener l) {
            listenerList.add(ListDataListener.class, l);
        }
        @Override
        public Object getElementAt(int index) {
            TreeNode[] nodesPath = getPathToRoot((TreeNode)getSelectedItem());
            try {
                if (index > (nodesPath.length - 1)) {
                    return getChild(getSelectedItem(), index-nodesPath.length);
                }
                else {
                    return nodesPath[index];
                }
            } catch (Exception e) {
                return null;
            }
        }
        @Override
        public int getSize() {
            TreeNode[] nodesPath = getPathToRoot((TreeNode)getSelectedItem());
            return nodesPath.length + getChildCount(getSelectedItem());
        }
        @Override
        public void removeListDataListener(ListDataListener l) {
            listenerList.remove(ListDataListener.class, l);
        }
        @Override
        public void addElement(Object obj) {
            throw new UnsupportedOperationException("Not allowed.");
        }
        @Override
        public void insertElementAt(Object obj, int index) {
            throw new UnsupportedOperationException("Not allowed.");
        }
        @Override
        public void removeElement(Object obj) {
            throw new UnsupportedOperationException("Not allowed.");
        }
        @Override
        public void removeElementAt(int index) {
            throw new UnsupportedOperationException("Not allowed.");
        }
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (ExplorerManager.PROP_ROOT_CONTEXT.equals(
                    evt.getPropertyName()
                ) ||
                ExplorerManager.PROP_EXPLORED_CONTEXT.equals(
                    evt.getPropertyName()
                )) {
                for (ListDataListener listener : listenerList.getListeners(
                                                    ListDataListener.class)) {
                    listener.contentsChanged(
                        new ListDataEvent(
                            this, ListDataEvent.CONTENTS_CHANGED, 0, getSize()
                        )
                    );
                }
                return;
            }
        }
    }
     // end of NodeContextModel

It's basically a ContextTreeView's model that implements MutableComboBoxModel, so a ComboBox can show its data ans change it on run-time. I learned the hard way that you can't change a ComboBox data unless you use a MutableComboBoxModel. Now, let's initialize the view. As usual, wait untill the view is added, then get the ExplorerManager so we can know what are we showing and add some listeners (don't forget to release whatever we've got):

@Override
    public void addNotify() {
        manager = ExplorerManager.find(this);
        manager.addVetoableChangeListener(iListener);
        manager.addPropertyChangeListener(iListener);
        manager.addPropertyChangeListener((NodeContextModel)model);
        updateChoice();
        addActionListener(iListener);
        super.addNotify();
    }
 
    @Override
    public void removeNotify() {
        super.removeNotify();
        removeActionListener(iListener);
        if (manager != null) {
          manager.removeVetoableChangeListener(iListener);
          manager.removePropertyChangeListener(iListener);
          manager.removePropertyChangeListener((NodeContextModel)model);
       }
    }

 Then, make sure to keep in-sync the Explorer and the ComboBox:

    private void updateSelection() {
        for(ListDataListener listener : 
            listenerList.getListeners(ListDataListener.class)){
            listener.contentsChanged(
                new ListDataEvent(
                    this,
                    ListDataEvent.CONTENTS_CHANGED,
                    0, 
                    model.getSize()
                )
            );
        }
        setSelectedItem(manager.getExploredContext());
    }
    private void updateChoice() {
        model.setSelectedItem(manager.getExploredContext());
        updateSelection();
    }

 By listening to changes in both Explorer and ComboBox selection:

/*
 * The inner adaptor class for listening to the ExplorerManager's property
 * and vetoable changes.
 */
    final class PropertyIL implements PropertyChangeListener,
                                      VetoableChangeListener,
                                      ActionListener {
        @Override
        public void vetoableChange(PropertyChangeEvent evt)
        throws PropertyVetoException {
            if (ExplorerManager.PROP_SELECTED_NODES.equals(
                    evt.getPropertyName()
                )) {
                Node[] nodes = (Node[]) evt.getNewValue();
                if (nodes.length > 1) {
                    // we do not allow multiple selection // NOI18N
                    throw new PropertyVetoException("", evt); 
                }
            }
        }
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            ContextChoiceView.this.removeActionListener(this);
            try {
                if (ExplorerManager.PROP_ROOT_CONTEXT.equals(
                    evt.getPropertyName())) {
                    updateChoice();
                    return;
                }
                if (ExplorerManager.PROP_EXPLORED_CONTEXT.equals(
                    evt.getPropertyName())) {
                    updateChoice();
                    return;
                }
            } finally {
                ContextChoiceView.this.addActionListener(this);
            }
        }
        @Override
        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
            int s = getSelectedIndex();
            if ((s < 0) || (s >= model.getSize())) {
                return;
            }
            manager.removeVetoableChangeListener(this);
            manager.removePropertyChangeListener(this);
            manager.setExploredContext(
                Visualizer.findNode(getSelectedItem()));
            manager.addVetoableChangeListener(this);
            manager.addPropertyChangeListener(this);
        }
    }

And after you put it all together, this is what you get:

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */
package org.myorg.nodes.views;
import java.awt.event.ActionListener;
import javax.swing.event.ListDataListener;
import org.openide.explorer.*;
import org.openide.explorer.ExplorerManager.Provider;
import org.openide.nodes.Node;
import org.openide.nodes.Node.Property;
import java.beans.*;
import java.io.*;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.tree.TreeNode;
import org.openide.explorer.view.ChoiceView;
import org.openide.explorer.view.ContextTreeView;
import org.openide.explorer.view.NodeRenderer;
import org.openide.explorer.view.NodeTreeModel;
import org.openide.explorer.view.Visualizer;
/** Explorer view based on a combo box.
 *  Allows to select the selected Context for the Nodes hierarchy.
 *  No leaf nodes are shown.
 * 

* This class is a view * to use it properly you need to add it into a component which implements * {@link Provider}. Good examples of that can be found * in {@link ExplorerUtils}. Then just use * {@link Provider#getExplorerManager} call to get the {@link ExplorerManager} * and control its state. *

*

* There can be multiple views under one container * implementing {@link Provider}. Select from * range of predefined ones or write your own: *

*
    *
  • {@link org.openide.explorer.view.BeanTreeView} - * shows a tree of nodes
  • *
  • {@link org.openide.explorer.view.ContextTreeView} - * shows a tree of nodes without leaf nodes
  • *
  • {@link org.openide.explorer.view.ListView} - * shows a list of nodes
  • *
  • {@link org.openide.explorer.view.IconView} - * shows a rows of nodes with bigger icons
  • *
  • {@link org.openide.explorer.view.ChoiceView} - * creates a combo box based on the explored nodes
  • *
  • {@link org.openide.explorer.view.TreeTableView} - * shows tree of nodes together with a set of their {@link Property}
  • *
  • {@link org.openide.explorer.view.MenuView} - * can create a {@link JMenu} structure based on structure of {@link Node}s
  • *
*

* All of these views use {@link ExplorerManager#find} to walk up the * AWT hierarchy and locate the{@link ExplorerManager} to use as a controler. * They attach as listeners to it and also call its setter methods to update * the shared state based on the user action. Not all views make sence together, * but for example {@link org.openide.explorer.view.ContextTreeView} and * {@link org.openide.explorer.view.ListView} were designed to complement * themselves and behaves like windows explorer. * The {@link org.openide.explorer.propertysheet.PropertySheetView} * for example should be able to work with any other view. *

* @author Jaroslav Tulach */ public class ContextChoiceView extends JComboBox implements Externalizable { /** logger to find out why the ContextTreeView tests fail so randomly */ static final Logger LOG = Logger.getLogger( ContextChoiceView.class.getName()); /** The local reference to the explorerManager. It is transient * because it will be reset in initializeManager() after deserialization.*/ transient private ExplorerManager manager; /** Listens on ExplorerManager. */ transient private PropertyIL iListener; /** model to use */ transient private ComboBoxModel model; /** Value of property showExploredContext. */ private boolean showExploredContext = true; // init ........................................................................ /** Default constructor. */ public ContextChoiceView() { super(); initializeChoice(); } /** Initialize view. */ private void initializeChoice() { setRenderer(new NodeRenderer()); setModel(model = createModel()); iListener = new PropertyIL(); } // XXX [PENDING] setting new model via setModel() is in fact ignored, see model // field -> which 'replaces' normal combo model thus the underlying one making // useless. /** * Write view's state to output stream. */ @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(showExploredContext ? Boolean.TRUE : Boolean.FALSE); } /** * Reads view's state form output stream. */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { showExploredContext = ((Boolean) in.readObject()).booleanValue(); } // // To override // /** Creates the model that this view should show. */ protected ComboBoxModel createModel() { return new NodeContextModel(); } // main methods ................................................................ /** Set showing of explored contexts. * @param b true to show the explored context, * false the root context */ public void setShowExploredContext(boolean b) { showExploredContext = b; updateChoice(); } /** * Get explored context toggle. * @return whether currently showing explored context * (default false) */ public boolean getShowExploredContext() { return showExploredContext; } // main methods ................................................................ /* Initializes view. */ @Override public void addNotify() { manager = ExplorerManager.find(this); manager.addVetoableChangeListener(iListener); manager.addPropertyChangeListener(iListener); manager.addPropertyChangeListener((NodeContextModel)model); updateChoice(); addActionListener(iListener); super.addNotify(); } /* Deinitializes view. */ @Override public void removeNotify() { super.removeNotify(); removeActionListener(iListener); if (manager != null) { manager.removeVetoableChangeListener(iListener); manager.removePropertyChangeListener(iListener); manager.removePropertyChangeListener((NodeContextModel)model); } } private void updateSelection() { for(ListDataListener listener : listenerList.getListeners( ListDataListener.class)){ listener.contentsChanged( new ListDataEvent( this, ListDataEvent.CONTENTS_CHANGED, 0, model.getSize() ) ); } setSelectedItem(manager.getExploredContext()); } private void updateChoice() { model.setSelectedItem(manager.getExploredContext()); updateSelection(); } // innerclasses ................................................................ /* * The inner adaptor class for listening to the ExplorerManager's property * and vetoable changes. */ final class PropertyIL implements PropertyChangeListener, VetoableChangeListener, ActionListener { @Override public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { if (ExplorerManager.PROP_SELECTED_NODES.equals( evt.getPropertyName()) ) { Node[] nodes = (Node[]) evt.getNewValue(); if (nodes.length > 1) { // we do not allow multiple selection // NOI18N throw new PropertyVetoException("", evt); } } } @Override public void propertyChange(PropertyChangeEvent evt) { ContextChoiceView.this.removeActionListener(this); try { if (ExplorerManager.PROP_ROOT_CONTEXT.equals( evt.getPropertyName())) { updateChoice(); return; } if (ExplorerManager.PROP_EXPLORED_CONTEXT.equals( evt.getPropertyName())) { updateChoice(); return; } } finally { ContextChoiceView.this.addActionListener(this); } } @Override public void actionPerformed(java.awt.event.ActionEvent actionEvent) { int s = getSelectedIndex(); if ((s < 0) || (s >= model.getSize())) { return; } manager.removeVetoableChangeListener(this); manager.removePropertyChangeListener(this); manager.setExploredContext(Visualizer.findNode(getSelectedItem())); manager.addVetoableChangeListener(this); manager.addPropertyChangeListener(this); } } /** Excludes leafs from the model. */ static final class NodeContextModel extends NodeTreeModel implements MutableComboBoxModel, PropertyChangeListener{ private Node selectedItem = null; // // Event filtering // private int[] newIndices; private Object[] newChildren; public NodeContextModel(Node root) { super(root); } public NodeContextModel() { } @Override public Object getChild(Object parent, int index) { int childCount = super.getChildCount(parent); int contextIndex = -1; for(int i = 0; ((contextIndex < index) || (i < childCount)); i++){ Object child = super.getChild(parent, i); if(!isLeaf(child)){ contextIndex++; if(contextIndex == index) return child; } } return null; } @Override public int getChildCount(Object parent) { int childCount = super.getChildCount(parent); int contextIndex = 0; for(int i = 0; i < childCount; i++){ Object child = super.getChild(parent, i); if(!isLeaf(child)){ contextIndex++; } } return contextIndex; } @Override public Object getSelectedItem() { return selectedItem == null? root: Visualizer.findVisualizer(selectedItem); } @Override public void setSelectedItem(Object anItem) { selectedItem = Visualizer.findNode(anItem); } @Override public void addListDataListener(ListDataListener l) { listenerList.add(ListDataListener.class, l); } @Override public Object getElementAt(int index) { TreeNode[] nodesPath = getPathToRoot((TreeNode)getSelectedItem()); try { if (index > (nodesPath.length - 1)) { return getChild( getSelectedItem(), index - nodesPath.length); } else { return nodesPath[index]; } } catch (Exception e) { return null; } } @Override public int getSize() { TreeNode[] nodesPath = getPathToRoot((TreeNode)getSelectedItem()); return nodesPath.length + getChildCount(getSelectedItem()); } @Override public void removeListDataListener(ListDataListener l) { listenerList.remove(ListDataListener.class, l); } @Override public void addElement(Object obj) { throw new UnsupportedOperationException("Not allowed."); } @Override public void insertElementAt(Object obj, int index) { throw new UnsupportedOperationException("Not allowed."); } @Override public void removeElement(Object obj) { throw new UnsupportedOperationException("Not allowed."); } @Override public void removeElementAt(int index) { throw new UnsupportedOperationException("Not allowed."); } @Override public void propertyChange(PropertyChangeEvent evt) { if (ExplorerManager.PROP_ROOT_CONTEXT.equals( evt.getPropertyName()) || ExplorerManager.PROP_EXPLORED_CONTEXT.equals( evt.getPropertyName())) { for (ListDataListener listener : listenerList.getListeners( ListDataListener.class)) { listener.contentsChanged( new ListDataEvent( this, ListDataEvent.CONTENTS_CHANGED, 0, getSize() ) ); } return; } } } // end of NodeContextModel }

There are some missing desirable features, such as indenting the nodes by its level, and showing the parent nodes with the "open" icon, while the sibling nodes keep their closed icon; something like the FileChooser.

You can probably name some other missing features, but the essence is here. This has been my first article here. If you are reading this, then it was at least not boring. :-) Please, provide feedback. It will be very much appreciated. Thank you very much. Muchas gracias por su paciencia!

 

The Java Zone is brought to you in partnership with ZeroTurnaround. Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code!

Topics:

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}