Статьи

Добавление контента в глобальный выбор

Как получить текущий проект от любого действия

Сколько раз вы хотели, чтобы вы могли определить текущий выбранный проект в рамках действия? Или вы когда-нибудь хотели создавать контекстно-зависимые действия, которые продолжают работать после изменения фокуса на какое-то другое окно? В этой статье показано, как выполнить контекстно-зависимое действие на основе выбранного в данный момент проекта, который работает из любого окна, а не только из окна «Проекты».

Платформа NetBeans предоставляет возможность включать и отключать Действия на основе того, что выбрано в текущем логическом окне — это называется « глобальным выбором ». В этой статье я покажу, как расширить эту концепцию выбора, чтобы включить объекты, которые являются глобальными для всего приложения. Используя эту технику, вы сможете использовать существующие идиомы для создания контекстно-зависимых действий и для запроса глобального выбора, но объем ваших операций будет расширен от контекста текущего окна до контекста всего приложения. Это довольно мощная точка расширения, которая демонстрирует силу и универсальность API-интерфейсов платформы NetBeans.

Используемые понятия

  • Поиск прокси
  • Поставщики услуг
  • Контекстно-зависимые действия
  • Модули NetBeans

Для получения некоторой базовой и полезной информации о поисках я предлагаю прочитать статью Тони Эппла: Объяснения поисков NetBeans

Случаи использования

Рассмотрим следующие варианты использования. Каждый из них так или иначе взаимодействует с глобальным выбором. Каждый из них выиграет, предоставив выбранному в настоящее время проекту область применения.

Вариант использования № 1

Вы хотите создать контекстно-зависимое действие, которое включается при выборе проекта в окне «Проекты» и остается активным, когда фокус переключается на другое окно. Например:

@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 });
    }
}

Вариант использования № 2

Вы хотите, чтобы действие взаимодействовало с возможностью, найденной в текущем проекте. Ваше действие находится в слабо связанном модуле, который ничего не знает о проектах, кроме API-интерфейса NetBeans и API вашей возможности. Например:

@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)
            {
                 ...
            }
            ...
        }
    }
}

Вариант использования № 3

Вы хотите обновить заголовок главного окна на основе атрибута в текущем проекте, например, имени. Вы хотите использовать простой LookupListener, который прослушивает выбранные проекты в глобальном выборе. Например:

/**
 * 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);
                }
            });
        }
    }
}

Все эти примеры работают нормально, когда проект выбран в окне «Проекты», но не когда фокус переключен на другое окно, или когда выбран дочерний узел проекта. Мы хотим, чтобы выбранный проект был универсально доступен во всей области применения. Что может быть лучше, чем просто расширить область просмотра содержимого Lookup, предоставляемого Utilities.actionsGlobalContext () . Мы можем сделать это, создав ProxyLookup, который объединяет «глобальный выбор» по умолчанию с нашим собственным контентом, который мы контролируем.

Основная реализация

Первым шагом является создание поставщика услуг, который реализует интерфейс ContextGlobalProvider . Наш поставщик услуг заменит стандартную реализацию NetBeans: GlobalActionContextImpl . Когда вызывается Utilities.actionsGlobalContext () , наш класс возвращает ProxyLookup, который включает реализацию по умолчанию для логического контекста области окна, а также наш собственный контент для всего приложения. То, что вы помещаете в контент приложения, зависит от вас, но некоторые примеры, которые я использовал, это Projects и NavigatorHints.

Мы начнем с создания класса GlobalActionContextProxy . Обратите внимание на комментарий класса о зависимости API оконной системы . Без этого у нас не будет доступа к классу GlobalActionContextImpl . Чтобы изменить зависимость, щелкните правой кнопкой мыши модуль вашего класса и выберите: Свойства> Библиотеки> Зависимости модуля> API оконной системы> Изменить …> Версия реализации .

/**
 * 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);
    }
}

Полная реализация

Вот полный GlobalActionContextProxy, который удовлетворяет трем вариантам использования, описанным выше. Эта реализация гарантирует, что текущий выбранный проект останется в глобальном выборе независимо от текущего TopComponent. Это достигается с помощью следующего:

  • PropertyChangeListener прикрепляется к TopComponent.Registry для отслеживания выбора узла проекта в окне проектов. Он хранит последний выбранный проект в статическом члене lastProject . Вот волшебство: когда ссылка lastProject не найдена в глобальном выборе по умолчанию, она помещается в InstanceContent, который возвращается в нашем ProxyLookup . Wha-ла!
  • Lookup.Result , получается из глобального выбора по умолчанию для отслеживания существования проектов в глобальной селекции. LookupListener прикрепляется к результату , который обрабатывает изменения в отборе проектов , которые происходят за пределами окна проектов, например, когда проекты будут закрыты.
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;
        }
    }
}

Вывод

Текущий источник для моего GlobalContextProviderProxy находится здесь: http://java.net/projects/emxsys/sources/svn/content/trunk/emxsys/emx_project-assistant/src/com/emxsys/projectassistant/GlobalActionContextProxy.java

Полные источники для моего модуля Project Assistant можно найти здесь: http://java.net/projects/emxsys/sources/svn/show/trunk/emxsys/emx_project-assistant