Developer of the Wildfire Management Tool software featuring the Campbell Prediction System (CPS) for prediction wildland fire behavior. Bruce has posted 3 posts at DZone. You can read more from them at their website. View Full User Profile

Adding Content to the Global Selection

11.20.2012
| 3867 views |
  • submit to reddit

How to Get the Current Project from any Action

How many times have you wished you could identify the currently selected project from within an Action? Or, have you ever wished you could create context-sensitive actions that continue to work after you change the focus to some other window? This article demonstrates how to make a context-sensitive action based on the currently selected project that works from any window, not just the Projects window.

The NetBeans Platform provides the ability to enable and disable Actions based upon what is selected in the current logical window--this is known as the "global selection". In this article I show you how to extend this selection concept to include objects that are global to the entire application. Using this technique, you'll be able to leverage existing idioms for creating context-sensitive actions and for querying the global selection, but the scope of your operations will be expanded from the context of the current window to the context of the entire application. That's a pretty powerful extension point and it demonstrates the strength and versatility of the NetBeans Platform APIs.

Concepts Used

  • Proxy Lookups
  • Service Providers
  • Context Sensitive Actions
  • NetBeans Modules

For some basic and useful information about Lookups, I suggest reading Toni Epple's article: NetBeans Lookups Explained

Use Cases

Consider the following use cases.  Each one interacts with the global selection in some way.  Each one would benefit by giving the currently selected project an application-wide scope.

Use Case #1

You want to create a context sensitive action that is enabled when a project is selected in the Projects window, and that remains enabled when the focus changes to another window.  For example:

@ActionID(...)
@ActionRegistration(...)
@ActionReference(...)
public final class CloseProjectAction implements ActionListener
{
    private final Project project;

    public CloseProjectAction(Project context)
    {
        this.project = context;
    }

    @Override
    public void actionPerformed(ActionEvent e)
    {
        OpenProjects.getDefault().close(new Project[] { project });
    }
}

Use Case #2

You want an Action to interact with a capability found in the current project. Your Action is in a loosely coupled module that knows nothing about projects other than the NetBeans Project API and your capability's API. For example:

@ActionID(...)
@ActionRegistration(...)
@ActionReference(...)
public final class SomeAction implements ActionListener
{
    @Override
    public void actionPerformed(ActionEvent event)
    {
        Project project = Utilities.actionsGlobalContext().lookup(Project.class);
        if (project != null)
        {            
            SomeCapability capability = project.getLookup().lookup(SomeCapability.class);
            if (capability != null)
            {
                 ...
            }
            ...
        }
    }
}

Use Case #3

You want to update the main window title based on an attribute in the current project, for instance, the name.  You want to use a simple LookupListener that listens for the selected projects in the global selection.  For example:

/**
 * This class provides the application's window title with the selected project's name.
 */
public class MainWindowTitleManager
{
    private static Lookup.Result<Project> lookupResults;
    private static LookupListener lookupListener;

    /**
     * Creates a LookupListener on the Project.class that handles changes in the project selection.
     */
    public static void activate()
    {
        if (lookupResults == null)
        {
            // Monitor the existance of Projects in the global context lookup
            lookupResults = Utilities.actionsGlobalContext().lookupResult(Project.class);
            // Create the listener on the lookupResults
            lookupListener = new LookupListener()
            {
                // Update window title when the Project changes
                @Override
                public void resultChanged(LookupEvent ignored)
                {
                    String projectName;
                    Collection<? extends Project> projects = lookupResults.allInstances();
                    if (projects.isEmpty())
                    {
                        projectName = "<No Project>";
                    }
                    else if (projects.size() == 1)
                    {
                        Project project = projects.iterator().next();
                        projectName = ProjectUtils.getInformation(project).getDisplayName();
                    }
                    else
                    {
                        projectName = "Multiple Projects";
                    }
                    //ProjectAssistant.getDefault().updateWindowTitle(projectName);
                    demonstrateUpdateWindowTitle(projectName);
                }
            };
            // Activate the listener
            lookupResults.addLookupListener(lookupListener);
            lookupListener.resultChanged(null);
        }
    }

    static void demonstrateUpdateWindowTitle(final String projectName)
    {
        // We have to do this on the AWT thread, so we use the invokeWhenUIReady
        // method which can be called from any thread.
        {
            WindowManager.getDefault().invokeWhenUIReady(new Runnable()
            {
                @Override
                public void run()
                {
                    Frame mainWindow = WindowManager.getDefault().getMainWindow();
                    mainWindow.setTitle(projectName);
                }
            });
        }
    }
}

All of these examples work fine when a project is selected in the Projects window, but not when the focus is switched to another window, nor when a child node of a project is selected. What we want is for the selected project to be universally available throughout the scope of the entire application. What better way than to simply expand the scope of the Lookup contents provided by Utilities.actionsGlobalContext(). We can do this by creating a ProxyLookup that merges the default "global selection" with our own content that we control.

Basic Implementation

The first step is to create a service provider that implements the ContextGlobalProvider interface. Our service provider will supersede the default NetBeans implementation: GlobalActionContextImpl. When Utilities.actionsGlobalContext() is called, our class will return a ProxyLookup that includes the default implementation for the logical window-scope context, plus our own application-wide content.  What you place in the application content is up to you, but some examples that I've used are Projects and NavigatorHints.

We'll start by creating a GlobalActionContextProxy class. Note the class comment about the Window System API dependency. Without it, we won't have access to the GlobalActionContextImpl class. To change the dependency, right-click your class' module and select: Properties > Libraries > Module Dependencies > Window System API > Edit… > Implementation Version.

/**
 * This class proxies the original ContextGlobalProvider.  It provides the ability to add and remove objects
 * from the application-wide global selection.
 *
 * To use this class you must edit the Windows System API module dependency: change the dependency
 * to an implementation version so that the org.netbeans.modules.openide.windows package is on the
 * classpath.
 */
@ServiceProvider(service = ContextGlobalProvider.class, 
                 supersedes = org.netbeans.modules.openide.windows.GlobalActionContextImpl")
public class GlobalActionContextProxy implements ContextGlobalProvider
{
    /** The native NetBeans global context Lookup provider  */
    private final GlobalActionContextImpl globalContextProvider;
    /** The primary lookup managed by the platform  */
    private Lookup globalContextLookup;
    /** The project lookup managed by this class  */
    private Lookup projectLookup;
    /** The actual Lookup returned by this class  */
    private Lookup proxyLookup;
    /** The additional content for our proxy lookup  */
    private final InstanceContent content;
 
    public GlobalActionContextProxy()
    {
        this.content = new InstanceContent();
        
        // Create the default GlobalContextProvider
        this.globalContextProvider = new GlobalActionContextImpl();
        this.globalContextLookup = this.globalContextProvider.createGlobalContext();
    }
 
    /**
     * Returns a ProxyLookup that adds the application-wide content to the original lookup
     * returned by Utilities.actionsGlobalContext().
     *
     * @return a ProxyLookup that includes the default global context plus our own content
     */
    @Override
    public Lookup createGlobalContext()
    {
        if (this.proxyLookup == null)
        {
            // Merge the two lookups that make up the proxy
            this.projectLookup = new AbstractLookup(content);
            this.proxyLookup = new ProxyLookup(this.globalContextLookup, this.projectLookup);
        }
        return this.proxyLookup;
    }
    
    /**
     * Adds an Object to the application scope global selection.
     */
    public void add(Object obj)
    {
        this.content.add(obj);
    }

    /**
     * Removes an Object from the application scope global selection.
     */
    public void remove(Object obj)
    {
        this.content.remove(obj);
    }
}

Complete Implementation

Here's a complete GlobalActionContextProxy that satisfies the three use cases descibed above.  This implementation ensures that the currently selected Project remains in the global selection regardless of the current TopComponent. This is accomplished with the following:

  • A PropertyChangeListener is attached to the TopComponent.Registry to track the Project node selection in the Projects window. It stores the last selected Project in the lastProject static member.  Here's the magic: when the lastProject reference not found in the default global selection, it is placed in the InstanceContent that is returned in our ProxyLookup. Wha-la!
  • A Lookup.Result is obtained from the default global selection to track the existance of Projects in the global selection.  A LookupListener is attached to the result that handles changes to the project selection that occur outside of the Projects window, for instance, when projects are closed.
package com.emxsys.projectassistant;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.ui.OpenProjects;
import org.netbeans.modules.openide.windows.GlobalActionContextImpl;
import org.openide.explorer.ExplorerManager;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node;
import org.openide.util.ContextGlobalProvider;
import org.openide.util.Lookup;
import org.openide.util.Lookup.Result;
import org.openide.util.Lookup.Template;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.ProxyLookup;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;


/**
 * This class proxies the original ContextGlobalProvider and ensures the current project remains in
 * the GlobalContext regardless of the TopComponent selection. The class also ensures that when a
 * child node is selected within the in Projects tab, the parent Project will be in the lookup.
 *
 * To use this class you must edit the Windows System API module dependency: change the dependency
 * to an implementation version so that the org.netbeans.modules.openide.windows package is on the
 * classpath.
 *
 * @see ContextGlobalProvider
 * @see GlobalActionContextImpl
 * @author Bruce Schubert <bruce@emxsys.com>
 */
@ServiceProvider(service = ContextGlobalProvider.class,
                 supersedes = "org.netbeans.modules.openide.windows.GlobalActionContextImpl")
public class GlobalActionContextProxy implements ContextGlobalProvider
{

    /**
     * The native NetBeans global context Lookup provider
     */
    private final GlobalActionContextImpl globalContextProvider;
    /**
     * Additional content for our proxy lookup
     */
    private final InstanceContent content;
    /**
     * The primary lookup managed by the platform
     */
    private Lookup globalContextLookup;
    /**
     * The project lookup managed by resultChanged
     */
    private Lookup projectLookup;
    /**
     * The actual proxyLookup returned by this class
     */
    private Lookup proxyLookup;
    /**
     * A lookup result that we listen to for Projects
     */
    private Result<Project> resultProjects;
    /**
     * Listener for changes resultProjects
     */
    private final LookupListener resultListener;
    /**
     * Listener for changes on the TopComponent registry
     */
    private final PropertyChangeListener registryListener;
    /**
     * The last project selected
     */
    private Project lastProject;
    /**
     * Critical section lock
     */
    private final Object lock = new Object();
    private static final Logger logger = Logger.getLogger(GlobalActionContextProxy.class.getName());
    public static final String PROJECT_LOGICAL_TAB_ID = "projectTabLogical_tc";
    public static final String PROJECT_FILE_TAB_ID = "projectTab_tc";


    static
    {
        logger.setLevel(Level.FINE);
    }


    public GlobalActionContextProxy()
    {
        this.content = new InstanceContent();
        
        // The default GlobalContextProvider
        this.globalContextProvider = new GlobalActionContextImpl();
        this.globalContextLookup = this.globalContextProvider.createGlobalContext();

        // Monitor the activation of the Projects Tab TopComponent
        this.registryListener = new RegistryPropertyChangeListener();
        TopComponent.getRegistry().addPropertyChangeListener(this.registryListener);

        // Monitor the existance of a Project in the principle lookup
        this.resultProjects = globalContextLookup.lookupResult(Project.class);
        this.resultListener = new LookupListenerImpl();
        this.resultProjects.addLookupListener(this.resultListener);

        WindowManager.getDefault().invokeWhenUIReady(new Runnable()
        {
            @Override
            public void run()
            {
                // Hack to force the current Project selection
                TopComponent tc = WindowManager.getDefault().findTopComponent(PROJECT_LOGICAL_TAB_ID);
                if (tc != null)
                {
                    tc.requestActive();
                }
            }
        });
    }


    /**
     * Returns a ProxyLookup that adds the current Project instance to the original lookup returned
     * by Utilities.actionsGlobalContext().
     *
     * @return a ProxyLookup that includes the original global context lookup.
     */
    @Override
    public Lookup createGlobalContext()
    {
        if (proxyLookup == null)
        {
            logger.config("Creating a proxy for Utilities.actionsGlobalContext()");
            // Create the two lookups that will make up the proxy
            projectLookup = new AbstractLookup(content);
            proxyLookup = new ProxyLookup(globalContextLookup, projectLookup);
        }
        return proxyLookup;
    }

    /**
     * This class populates the proxy lookup with the currently selected project found in the
     * Projects tab.
     *
     * @author Bruce Schubert
     */
    private class RegistryPropertyChangeListener implements PropertyChangeListener
    {
        private TopComponent projectsTab = null;

        @Override
        public void propertyChange(PropertyChangeEvent event)
        {
            if (event.getPropertyName().equals(TopComponent.Registry.PROP_ACTIVATED_NODES)
                || event.getPropertyName().equals(TopComponent.Registry.PROP_ACTIVATED))
            {
                // Get a reference to the Projects window
                if (projectsTab == null)
                {
                    projectsTab = WindowManager.getDefault().findTopComponent(PROJECT_LOGICAL_TAB_ID);
                    if (projectsTab == null)
                    {
                        logger.severe("propertyChange: cannot find the Projects logical window (" + PROJECT_LOGICAL_TAB_ID + ")");
                        return;
                    }
                }
                // Look for the current project in the Projects window when activated and handle 
                // special case at startup when lastProject hasn't been initialized.            
                Node[] nodes = null;
                TopComponent activated = TopComponent.getRegistry().getActivated();
                if (activated != null && activated.equals(projectsTab))
                {
                    logger.finer("propertyChange: processing activated nodes");
                    nodes = projectsTab.getActivatedNodes();
                }
                else if (lastProject == null)
                {
                    logger.finer("propertyChange: processing selected nodes");
                    ExplorerManager em = ((ExplorerManager.Provider) projectsTab).getExplorerManager();
                    nodes = em.getSelectedNodes();
                }
                // Find and use the first project that owns a node
                if (nodes != null)
                {
                    for (Node node : nodes)
                    {
                        Project project = findProjectThatOwnsNode(node);
                        if (project != null)
                        {
                            synchronized (lock)
                            {
                                // Remember this project for when the Project Tab goes out of focus
                                lastProject = project;

                                // Add this project to the proxy if it's not in the global lookup
                                if (!resultProjects.allInstances().contains(lastProject))
                                {
                                    logger.finer("propertyChange: Found project [" + ProjectUtils.getInformation(lastProject).getDisplayName() + "] that owns current node.");
                                    updateProjectLookup(lastProject);
                                }
                            }
                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * This class listens for changes in the Project results, and ensures a Project remains in the
     * Utilities.actionsGlobalContext() if a project is open.
     *
     * @author Bruce Schubert
     */
    private class LookupListenerImpl implements LookupListener
    {

        @Override
        public void resultChanged(LookupEvent event)
        {
            logger.finer("resultChanged: Entered...");
            synchronized (lock)
            {
                // First, handle projects in the principle lookup
                if (resultProjects.allInstances().size() > 0)
                {
                    // Clear the proxy, and remember this project. Note: not handling muliple selection.
                    clearProjectLookup();
                    lastProject = resultProjects.allInstances().iterator().next();
                    logger.finer("resultChanged: Found project [" + ProjectUtils.getInformation(lastProject).getDisplayName() + "] in the normal lookup.");
                }
                else if (OpenProjects.getDefault().getOpenProjects().length==0) 
                {
                    clearProjectLookup();
                    lastProject = null;
                }
                else
                {
                    if (lastProject == null)
                    {
                        // Find the project that owns the current Node
                        Node currrentNode = globalContextLookup.lookup(Node.class);
                        Project project = findProjectThatOwnsNode(currrentNode);
                        if (project != null)
                        {
                            lastProject = project;
                            logger.finer("resultChanged: Found project [" + ProjectUtils.getInformation(lastProject).getDisplayName() + "] that owns current node.");
                        }
                    }
                    // Add the last used project to our internal lookup
                    if (lastProject != null)
                    {
                        updateProjectLookup(lastProject);
                    }
                }
            }
        }
    }

    /**
     * Unconditionally clears the project lookup.
     */
    private void clearProjectLookup()
    {
        Collection<? extends Project> projects = projectLookup.lookupAll(Project.class);
        for (Project project : projects)
        {
            content.remove(project);
        }
    }

    /**
     * Replaces the project lookup content.
     * @param project to place in the project lookup.
     */
    private void updateProjectLookup(Project project)
    {
        if (project == null)
        {
            throw new IllegalArgumentException("project cannot be null.");
        }
        // Add the project if an instance of it is not already in the lookup
        Template<Project> template = new Template<Project>(Project.class, null, project);
        if (projectLookup.lookupItem(template) == null)
        {
            clearProjectLookup();
            content.add(project);
            logger.fine("updateProjectLookup: added [" + ProjectUtils.getInformation(lastProject).getDisplayName() + "] to the proxy lookup.");
        }
    }
    
    /**
     * Recursively searches the node hierarchy for the project that owns a node.
     *
     * @param node a node to test for a Project in its or its ancestor's lookup.
     * @return the Project that owns the node, or null if not found
     */
    private static Project findProjectThatOwnsNode(Node node)
    {
        if (node != null)
        {
            Project project = node.getLookup().lookup(Project.class);
            if (project == null)
            {
                DataObject dataObject = node.getLookup().lookup(DataObject.class);
                if (dataObject != null)
                {
                    project = FileOwnerQuery.getOwner(dataObject.getPrimaryFile());
                }
            }
            return (project == null) ? findProjectThatOwnsNode(node.getParentNode()) : project;
        }
        else
        {
            return null;
        }
    }
}

Conclusion

The current source for my GlobalContextProviderProxy is here: http://java.net/projects/emxsys/sources/svn/content/trunk/emxsys/emx_project-assistant/src/com/emxsys/projectassistant/GlobalActionContextProxy.java

The complete sources for my Project Assistant module can be found here: http://java.net/projects/emxsys/sources/svn/show/trunk/emxsys/emx_project-assistant

Published at DZone with permission of its author, Bruce Schubert.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)