Archive

Posts Tagged ‘navigatorContent’

Writing an Eclipse Plug-in (Part 21): Return of the Popup Menu (Displaying Resources)

[In case anyone cares: I have upgraded to Eclipse 3.6 RC1]

Welcome to the second of what will probably be 4 posts on creating popup menus using the Common Navigator Framework.

In the last post we created a two item popup that appears when there are no resources displayed or selected. In this post we will have the popup menu appear even when we right click on a resource.

How (are we doing it?)

Perform the following on the customnavigator plug-in project.

  1. Delete the navigatorContent enablement adapt entries for both CustomNewActionProvider and CustomRefreshActionProvider.
    • plugin.xml –> Extensions
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete
  2. Add two enablement instanceof entries for both CustomNewActionProvider and CustomRefreshActionProvider.
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> New –> instanceof
      • value: customnavigator.navigator.ICustomProjectElement
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> New –> instanceof
      • value: customnavigator.navigator.ICustomProjectElement
  3. Start the runtime workbench, create a Custom Project, go to the Custom Perspective and right click on the project. You should see the popup menu.

Why (did we do it that way?)

First, let’s do that simplest thing I can think of: have a popup menu appear with one menu item. Once that is in place the rest are mechanical steps.

In order to have a new popup menu appear Eclipse needs to recognize the resource so that it will show just the menus you want and no others. How do we do that? By setting up an enablement with our custom project type.

Perform the following on the customnavigator plug-in project.

Go to plugin.xml –> Extensions.

Remove the entry for IResource (adapt):

  • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete

That entry told Eclipse to open the popup when the selected resource was of type IResource. Well the custom project does not include the IResource. We don’t need it for now. What we do need is for the popup to open when any of our custom types is selected. Since all of the custom nodes implement ICustomProjectElement we use that instead.

  • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> New –> instanceof
    • value: customnavigator.navigator.ICustomProjectElement

Why use instanceof instead of adapt? An adapt entry means that the selected object, in this case CustomProjectParent, can be adapted (converted) into the listed type, originally IResource, Since a CustomProjectParent does not implement IResource anywhere in its inheritance hierarchy the adapt will never work. Adapt means that we would have to change the custom node types while instanceof puts the onus on Eclipse. You know where my vote goes.

Perform the same steps for CustomRefreshActionProvider:

  1. org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete
  2. org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> New –> instanceof
    • value: customnavigator.navigator.ICustomProjectElement
  3. Start the runtime workbench, create a Custom Project, go to the Custom Perspective and right click on the project. You should see the popup menu.

While I was doing this I found that the above did not work. It turned out that my Launch configuration thought I only needed 73 of the existing plug-ins while I needed a few more. It is possible that your Launch Configuration might not have enough dependencies selected. In Eclipse 3.6 RC1 this needed 80 out of 375 plug-ins. The only other plug-in I have installed is EGit.

What Just Happened?

Not a lot just happened. Changing the adapt entry to instanceof specific to our custom project type was enough to get the popup to behave the way we needed. Not a lot of work.

What we need to do next is create New behavior for:

  • New Schema Table
  • New Schema View
  • New Schema Filter
  • New Stored Procedure

That will take commands, handlers and command images.

Our current setup creates new projects, schema files and deployment files. Oops. We’ll have to change that. Next time. Maybe.

After that we’ll add properties pages to each node type fix the New menus in the toolbar and main menu.

The cat is tired. It may be time to do some genetic programming posts.

Code

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.ui.views">
      <category
            id="customnavigator.category"
            name="%category.name">
      </category>
      <view
            allowMultiple="false"
            category="customnavigator.category"
            class="org.eclipse.ui.navigator.CommonNavigator"
            icon="icons/navigator.png"
            id="customnavigator.navigator"
            name="%view.name">
      </view>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.viewer">
      <viewer
            viewerId="customnavigator.navigator">
         <popupMenu
               id="customnavigator.navigator#PopupMenu">
            <insertionPoint
                  name="group.new">
            </insertionPoint>
            <insertionPoint
                  name="group.build"
                  separator="true">
            </insertionPoint>
         </popupMenu>
      </viewer>
      <viewerContentBinding
            viewerId="customnavigator.navigator">
         <includes>
            <contentExtension
                  pattern="customnavigator.navigatorContent">
            </contentExtension>
         </includes>
      </viewerContentBinding>
      <viewerActionBinding
            viewerId="customnavigator.navigator">
         <includes>
            <actionExtension
                  pattern="customnavigator.popup.actionprovider.CustomNewAction">
            </actionExtension>
            <actionExtension
                  pattern="customnavigator.popup.actionprovider.CustomRefreshAction">
            </actionExtension>
         </includes>
      </viewerActionBinding>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.navigatorContent">
      <navigatorContent
            activeByDefault="true"
            contentProvider="customnavigator.navigator.ContentProvider"
            id="customnavigator.navigatorContent"
            labelProvider="customnavigator.navigator.LabelProvider"
            name="%navigatorContent.name">
         <triggerPoints>
            <instanceof
                  value="org.eclipse.core.resources.IWorkspaceRoot">
            </instanceof>
         </triggerPoints>
         <commonSorter
               class="customnavigator.sorter.SchemaCategorySorter"
               id="customnavigator.sorter.schemacategorysorter">
            <parentExpression>
               <or>
                  <instanceof
                        value="customnavigator.navigator.CustomProjectSchema">
                  </instanceof>
               </or>
            </parentExpression>
         </commonSorter>
      </navigatorContent>
      <actionProvider
            class="customnavigator.popup.actionprovider.CustomNewActionProvider"
            id="customnavigator.popup.actionprovider.CustomNewAction">
         <enablement>
            <or>
               <instanceof
                     value="customnavigator.navigator.ICustomProjectElement">
               </instanceof>
               <adapt
                     type="java.util.Collection">
                  <count
                        value="0">
                  </count>
               </adapt>
            </or>
         </enablement>
      </actionProvider>
      <actionProvider
            class="customnavigator.popup.actionprovider.CustomRefreshActionProvider"
            id="customnavigator.popup.actionprovider.CustomRefreshAction">
         <enablement>
            <or>
               <instanceof
                     value="customnavigator.navigator.ICustomProjectElement">
               </instanceof>
               <adapt
                     type="java.util.Collection">
                  <count
                        value="0">
                  </count>
               </adapt>
            </or>
         </enablement>
      </actionProvider>
      <commonWizard
            type="new"
            wizardId="customplugin.wizard.new.custom">
         <enablement></enablement>
      </commonWizard>
   </extension>

</plugin>

Writing an Eclipse Plug-in (Part 20): Return of the Popup Menu (For an Empty Navigator)

April 4, 2010 4 comments

[This is a long post. It also feels like a bit of a mess. Whoda thought that creating a popup menu when there are no resources available in a navigator could be so non-trivial?]

So what’s the problem (or as they say in marketing speak: what is the challenge)?

The challenge (or as they say in real life: the pain) is quite easy to describe: when I right click in the custom navigator the popup menu appears. When I create a custom project and right-click on it the popup menu does not appear.

That behavior has to stop or I am turning this blog around right now (I’m not kidding! I’ll turn around right now!).

Alright. I lied. We actually have two problems:

  1. Remove the undefined menu items from the popup when nothing is available or selected
  2. Enable a specific set of menus when a Custom Project has been created

What this means is we have to decide when menu items appear/disappear or are enabled/disabled based on items being selected/unselected.

Sounds like a lot of combinations. Sounds like a job for a UML State diagram which I actually like when I am writing a real application. The issue here is that I am still kinda just messing with this and the state diagram makes me become too serious (I find that even the squirrels start to complain).

So let’s list the menu items we know so far:

  • New Custom Project
  • New Schema File
  • New Stored Procedure File
  • Open Project
  • Close Project
  • Copy
  • Paste
  • Delete
  • Import
  • Export
  • Refresh
  • Properties

Looks like a lot. Let’s think about this: copy, paste and delete don’t mean what they usually do, except for projects. I expect them to only copy/paste/delete the nodes they represent not entire files. Let’s leave them for last so let’s just remove them.

Open and Close project sounds too cool to be true. They will come after we do copy, paste and delete (and while we’re at it, how about working sets? Nah.).

Import and Export are also unknown. Since importing/exporting anything but Custom Projects seems rather odd, and we don’t know what it means to import or export Custom Projects, they will have to go too.

That leaves us with:

  • New Custom Project
  • New Schema File
  • New Stored Procedure File
  • Refresh
  • Properties

Much more managable. Also, we can expand what it means to be a schema and stored procedure in the Custom Navigator: the schema’s child nodes have New behavior as does the Stored Procedure node. So the list really looks like this:

  • New Custom Project
  • New Schema Table
  • New Schema View
  • New Schema Filter
  • New Stored Procedure
  • Refresh
  • Properties

When nothing is selected the following are enabled:

  • New Custom Project
  • Refresh

We can’t go around randomly creating Tables, Views and Filters just because, now can we?

When a Custom Project, or any custom resource, is selected the following are enabled:

  • New Custom Project
  • New Schema Table
  • New Schema View
  • New Schema Filter
  • New Stored Procedure
  • Refresh
  • Properties

As Richard Dreyfus once yelled into the open air: What does it mean?

What the above means is that the easiest way to control the popup menu for this custom navigator is to make one (or in this case two) rather than rely on the default popup and reconfigure it as we go. I tried desperately to avoid it, but in order to remove the default presentation of New, Import and Export it is just plain ol’ easier to make a new popup menu. Dems the breaks.

Tasks for this post and the next:

  1. Create a popup menu when the Custom navigator is empty
  2. Create a popup menu when a resource is selected in the Custom navigator

How (are we doing it?)

Time to back track. Remove the following:

  1. Remove all three commonWizard entries found under org.eclipse.ui.navigator.navigatorContent. That removes the menu entries under the popup menu New.
  2. Remove navigatorplugin –> plugin.xml –> org.eclipse.ui.menus
  3. Remove org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerActionBinding) entry.

That was the easy stuff. The more involved steps for this post are:

  1. Define the popup menu and its insertionPoints
  2. Define the actionProvider
  3. Define the actionExtension
  4. Implement the action provider code (if not implementing the command framework)

Sounds pretty straightforward doesn’t it?

Let’s see how well we do.

  1. Define the popup menu and its insertionPoints
    1. Create a viewer entry in org.eclipse.ui.viewer
      • org.eclipse.ui.navigator.viewer –> New –> viewer
        • viewerId: customnavigator.navigator
    2. Create a popupMenu entry under the viewer
      • customnavigator.navigator (viewer) –> New –> popupMenu
        • id: customnavigator.navigator#PopupMenu
    3. Add two insertion points under the popupMenu
      • customnavigator.navigator#PopupMenu (popupMenu) –> New –> insertionPoint
        • name: group.new
      • customnavigator.navigator#PopupMenu (popupMenu) –> New –> insertionPoint
        • name: group.build
  2. Define the actionProviders
    1. org.eclipse.ui.navigator.navigatorContent –> New –> actionProvider
      • class: customnavigator.popup.actionprovider.CustomNewActionProvider
      • id: customnavigator.popup.actionprovider.CustomNewAction
      • customnavigator.popup.actionprovider.CustomNewActionProvider (actionProvider) –> enablement –> New –> or
      • or –> New –> adapt
        • type: org.eclipse.core.resources.IResource
      • or –> New –> adapt
        • type: java.util.Collection
      • java.util.Collection –> New –> count
        • value: 0
    2. org.eclipse.ui.navigator.navigatorContent –> New –> actionProvider
      • class: customnavigator.popup.actionprovider.CustomRefreshActionProvider
      • id: customnavigator.popup.actionprovider.CustomRefreshAction
      • customnavigator.popup.actionprovider.CustomRefreshActionProvider (actionProvider) –> enablement –> New –> or
      • or –> New –> adapt
        • type: org.eclipse.core.resources.IResource
      • or –> New –> adapt
        • type: java.util.Collection
      • java.util.Collection –> New –> count
        • value: 0
  3. Define the actionExtensions
    • org.eclipse.ui.navigator.viewer –> New –> viewerActionBinding
      • viewerId: customnavigator.navigator
    • customnavigator.navigator (viewerActionBinding) –> New –> includes
      • includes –> New –> actionExtension
      • pattern: customnavigator.popup.actionprovider.CustomNewAction
    • org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerActionBinding) –> (includes) –> New –> actionExtension and change pattern to:
      • pattern: customnavigator.popup.actionprovider.CustomRefreshAction
  4. Implement the action provider code in the customnavigator plug-in
    customnavigator.popup.actionprovider.CustomNewActionProvider

    /**
     * Coder beware: this code is not warranted to do anything.
     * Some or all of this code is taken from the Eclipse code base.
     *
     * Copyright Mar 28, 2010 Carlos Valcarcel
     */
    package customnavigator.popup.actionprovider;
    
    import org.eclipse.jface.action.IMenuManager;
    import org.eclipse.jface.action.MenuManager;
    import org.eclipse.jface.action.Separator;
    import org.eclipse.ui.IWorkbenchWindow;
    import org.eclipse.ui.PlatformUI;
    import org.eclipse.ui.actions.ActionFactory;
    import org.eclipse.ui.navigator.CommonActionProvider;
    import org.eclipse.ui.navigator.ICommonActionExtensionSite;
    import org.eclipse.ui.navigator.ICommonMenuConstants;
    import org.eclipse.ui.navigator.ICommonViewerWorkbenchSite;
    import org.eclipse.ui.navigator.WizardActionGroup;
    
    public class CustomNewActionProvider extends CommonActionProvider {
    
        private static final String NEW_MENU_NAME = &quot;common.new.menu&quot;;//$NON-NLS-1$
    
        private ActionFactory.IWorkbenchAction showDlgAction;
    
        private WizardActionGroup newWizardActionGroup;
    
        private boolean contribute = false;
    
        @Override
        public void init(ICommonActionExtensionSite anExtensionSite) {
    
            if (anExtensionSite.getViewSite() instanceof ICommonViewerWorkbenchSite) {
                IWorkbenchWindow window = ((ICommonViewerWorkbenchSite) anExtensionSite.getViewSite()).getWorkbenchWindow();
                showDlgAction = ActionFactory.NEW.create(window);
    
                newWizardActionGroup = new WizardActionGroup(window, PlatformUI.getWorkbench().getNewWizardRegistry(), WizardActionGroup.TYPE_NEW, anExtensionSite.getContentService());
    
                contribute = true;
            }
        }
    
        @Override
        public void fillContextMenu(IMenuManager menu) {
            IMenuManager submenu = new MenuManager(
                    &quot;New&quot;,
                    NEW_MENU_NAME);
            if(!contribute) {
                return;
            }
    
            // fill the menu from the commonWizard contributions
            newWizardActionGroup.setContext(getContext());
            newWizardActionGroup.fillContextMenu(submenu);
    
            submenu.add(new Separator(ICommonMenuConstants.GROUP_ADDITIONS));
    
            // Add other ..
            submenu.add(new Separator());
            submenu.add(showDlgAction);
    
            // append the submenu after the GROUP_NEW group.
            menu.insertAfter(ICommonMenuConstants.GROUP_NEW, submenu);
        }
    
        @Override
        public void dispose() {
            if (showDlgAction!=null) {
                showDlgAction.dispose();
                showDlgAction = null;
            }
            super.dispose();
        }
    }
    

    customnavigator.popup.actionprovider.CustomRefreshActionProvider

    /**
     * Coder beware: this code is not warranted to do anything.
     * Some or all of this code is taken from the Eclipse code base.
     *
     * Copyright Apr 4, 2010 Carlos Valcarcel
     */
    package customnavigator.popup.actionprovider;
    
    import java.lang.reflect.InvocationTargetException;
    import java.util.Iterator;
    
    import org.eclipse.core.resources.IProject;
    import org.eclipse.core.resources.WorkspaceJob;
    import org.eclipse.core.runtime.CoreException;
    import org.eclipse.core.runtime.IAdaptable;
    import org.eclipse.core.runtime.IProgressMonitor;
    import org.eclipse.core.runtime.IStatus;
    import org.eclipse.core.runtime.Status;
    import org.eclipse.core.runtime.jobs.ISchedulingRule;
    import org.eclipse.jface.action.IMenuManager;
    import org.eclipse.jface.resource.ImageDescriptor;
    import org.eclipse.jface.viewers.IStructuredSelection;
    import org.eclipse.jface.viewers.StructuredViewer;
    import org.eclipse.jface.window.IShellProvider;
    import org.eclipse.osgi.util.NLS;
    import org.eclipse.swt.widgets.Shell;
    import org.eclipse.ui.IActionBars;
    import org.eclipse.ui.IWorkbenchCommandConstants;
    import org.eclipse.ui.actions.ActionFactory;
    import org.eclipse.ui.actions.RefreshAction;
    import org.eclipse.ui.actions.WorkspaceModifyOperation;
    import org.eclipse.ui.navigator.CommonActionProvider;
    import org.eclipse.ui.navigator.ICommonActionExtensionSite;
    import org.eclipse.ui.navigator.ICommonMenuConstants;
    
    import customnavigator.Activator;
    
    /**
     * The bulk of this code is taken from
     * org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider
     * which is provided with Eclipse in case you want to look up the original.
     *
     * @author carlos
     */
    public class CustomRefreshActionProvider extends CommonActionProvider {
    
        private RefreshAction refreshAction;
    
        private Shell         shell;
    
        /*
         * (non-Javadoc)
         * @see
         * org.eclipse.ui.navigator.CommonActionProvider#init(org.eclipse.ui.navigator.ICommonActionExtensionSite)
         */
        @Override
        public void init(ICommonActionExtensionSite aSite) {
            super.init(aSite);
            shell = aSite.getViewSite().getShell();
            makeActions();
        }
    
        @Override
        public void fillActionBars(IActionBars actionBars) {
            actionBars.setGlobalActionHandler(ActionFactory.REFRESH.getId(), refreshAction);
            updateActionBars();
        }
    
        /**
         * Adds the refresh resource actions to the context menu.
         *
         * @param menu
         * context menu to add actions to
         */
        @SuppressWarnings(&quot;rawtypes&quot;)
        @Override
        public void fillContextMenu(IMenuManager menu) {
            IStructuredSelection selection = (IStructuredSelection) getContext().getSelection();
            boolean hasClosedProjects = false;
            Iterator resources = selection.iterator();
    
            while (resources.hasNext() &amp;&amp; (!hasClosedProjects)) {
                Object next = resources.next();
                IProject project = null;
    
                if (next instanceof IProject) {
                    project = (IProject) next;
                } else if (next instanceof IAdaptable) {
                    project = (IProject) ((IAdaptable) next).getAdapter(IProject.class);
                }
    
                if (project == null) {
                    continue;
                }
    
                if (!project.isOpen()) {
                    hasClosedProjects = true;
                }
            }
    
            if (!hasClosedProjects) {
                refreshAction.selectionChanged(selection);
                menu.appendToGroup(ICommonMenuConstants.GROUP_BUILD, refreshAction);
            }
        }
    
        protected void makeActions() {
            IShellProvider sp = new IShellProvider() {
                @SuppressWarnings(&quot;synthetic-access&quot;)
                @Override
                public Shell getShell() {
                    return shell;
                }
            };
    
            refreshAction = new RefreshAction(sp) {
                @Override
                public void run() {
                    final IStatus[] errorStatus = new IStatus[1];
                    errorStatus[0] = Status.OK_STATUS;
                    final WorkspaceModifyOperation op = (WorkspaceModifyOperation) createOperation(errorStatus);
                    WorkspaceJob job = new WorkspaceJob(&quot;refresh&quot;) { //$NON-NLS-1$
    
                        @SuppressWarnings(&quot;synthetic-access&quot;)
                        @Override
                        public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
                            try {
                                op.run(monitor);
                                if (shell != null &amp;&amp; !shell.isDisposed()) {
                                    shell.getDisplay().asyncExec(new Runnable() {
                                        @Override
                                        public void run() {
                                            StructuredViewer viewer = getActionSite().getStructuredViewer();
                                            if (viewer != null &amp;&amp; viewer.getControl() != null &amp;&amp; !viewer.getControl().isDisposed()) {
                                                viewer.refresh();
                                            }
                                        }
                                    });
                                }
                            } catch (InvocationTargetException e) {
                                String msg = NLS.bind(&quot;Exception in {0}. run: {1}&quot;, getClass().getName(), e.getTargetException());
                                throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, msg, e
                                        .getTargetException()));
                            } catch (InterruptedException e) {
                                return Status.CANCEL_STATUS;
                            }
                            return errorStatus[0];
                        }
    
                    };
                    ISchedulingRule rule = op.getRule();
                    if (rule != null) {
                        job.setRule(rule);
                    }
                    job.setUser(true);
                    job.schedule();
                }
            };
            refreshAction.setDisabledImageDescriptor(getImageDescriptor(&quot;icons/refresh_nav_disabled.gif&quot;));//$NON-NLS-1$
            refreshAction.setImageDescriptor(getImageDescriptor(&quot;icons/refresh_nav_enabled.gif&quot;));//$NON-NLS-1$
            refreshAction.setActionDefinitionId(IWorkbenchCommandConstants.FILE_REFRESH);
        }
    
        /**
         * Returns the image descriptor with the given relative path.
         */
        protected ImageDescriptor getImageDescriptor(String relativePath) {
            return Activator.getIDEImageDescriptor(relativePath);
    
        }
    
        @Override
        public void updateActionBars() {
            IStructuredSelection selection = (IStructuredSelection) getContext().getSelection();
            refreshAction.selectionChanged(selection);
        }
    
    }
    

    Add the following to customnavigator.Activator:

    public class Activator extends AbstractUIPlugin {
    ...
        public static ImageDescriptor getIDEImageDescriptor(String imagePath) {
            return AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, imagePath);
        }
    }
    

    Add the following icons to the customnavigator icons folder:
    refresh_nav_enabled.gif:

    refresh_nav_disabled.gif:

Don’t forget:

  • Fix the warnings in MANIFEST.MF and plugin.xml.
  • Open the Externalize Strings Wizard and move the two strings to messages.properties in the folder with the action provider code.

Why (did we do it that way?)

To do today’s tasks it is necessary to clean the deck. That means removing all the wonderous things that took advantage of all the default GUI hooks and basically putting them back with new hooks. Think of it like spring cleaning…without spring or the cleaning.

Since we have already decided to have only the menu entries we really need we have to remove the commonWizard and org.eclipse.ui.menus entries for now. Don’t worry, we’ll put them back. It will be easier and cleaner to add them in sequence rather than removing some pieces, moving things around and hoping they work eventually.

  1. Remove all three commonWizard entries found under org.eclipse.ui.navigator.navigatorContent. That removes the menu entries under the popup menu New.
  2. Remove navigatorplugin –> plugin.xml –> org.eclipse.ui.menus

    As you should already know from the last post, the menuContribution entry found under org.eclipse.ui.menus let’s you directly add new menu items to an existing popup by giving Eclipse the path to the menu being affected. We’ll use this again later. Some things are too good to give up for long.

  3. Remove org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerActionBinding) entry.The value of actionExtension –> pattern refers to the ids of the actionProvider classes that execute the default behavior when a popup menu item is selected. For example, the value we just removed, org.eclipse.ui.navigator.resources.*, refers to the actionProvider ids found in the org.eclipse.ui.navigator.resources plug-in. Remember how the default popup menu displays New, Import, Export and Refresh? Well, if you open org.eclipse.ui.navigator.resources –> plugin.xml you will find an actionProvider entry for the following classes (there are others, but additional actionProvider do not concern me):
    • org.eclipse.ui.internal.navigator.resources.actions.NewActionProvider
    • org.eclipse.ui.internal.navigator.resources.actions.PortingActionProvider
    • org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider

    The ids for the above classes are:

    • org.eclipse.ui.navigator.resources.NewActions
    • org.eclipse.ui.navigator.resources.PortingActions
    • org.eclipse.ui.navigator.resources.ResourceMgmtActions

    Notice that the above ids fit the pattern org.eclipse.ui.navigator.resources.*. Eclipse doesn’t care about the class name; it cares about the id. Yes, there are other actionProviders, but they have enablement criteria that keeps them from being displayed when nothing is selected.

  4. Define the new popup menu and the insertionPoints.Time to make the donuts.

    Let’s create the popup and add one menu item. In order to do that we have to

    1. Create a viewer entry in org.eclipse.ui.viewer
    2. Create a popupMenu entry under the viewer
    3. Add an insertion point under the popupMenu
    4. Create a viewerActionBinding and actionExtension entry in org.eclipse.ui.navigator.viewer

    Do the following in the customnavigator plugin.xml Extensions tab:

    1. Create a viewer entry in org.eclipse.ui.viewer
      • org.eclipse.ui.navigator.viewer –> New –> viewer
        • viewerId: customnavigator.navigator
    2. Create a popupMenu entry under the viewer
      • customnavigator.navigator (viewer) –> New –> popupMenu
        • id: customnavigator.navigator#PopupMenu
    3. Add an insertion point under the popupMenu
      • customnavigator.navigator#PopupMenu (popupMenu) –> New –> insertionPoint
        • name: group.new
    4. Create a viewerActionBinding and actionExtension entry in org.eclipse.ui.navigator.viewer
      • org.eclipse.ui.navigator.viewer –> New –> viewerActionBinding
        • viewerId: customnavigator.navigator
      • customnavigator.navigator (viewerActionBinding) –> New –> includes
        • includes –> New –> actionExtension
        • pattern: org.eclipse.ui.navigator.resources.NewActions

    Quick note: The name group.new comes from ICommonMenuConstants found in org.eclipse.ui.navigator. Whenever possible I recommend adhering to existing naming conventions just to make things easier to find.

    Just for yucks we are using an existing action provider: org.eclipse.ui.internal.navigator.resources.actions.NewActionProvider whose id is org.eclipse.ui.navigator.resources.NewActions. What is interesting about the NewActionProvider is that it creates a new menu insertion point in the popup which allows menu items to be added as submenus. What is bad about NewActionProvider is that it does it programmatically.

    NewActionProvider.java

    public class NewActionProvider extends CommonActionProvider {
    ...
    	public void fillContextMenu(IMenuManager menu) {
    		IMenuManager submenu = new MenuManager(
    				WorkbenchNavigatorMessages.NewActionProvider_NewMenu_label,
    				NEW_MENU_NAME);
    		if(!contribute) {
    			return;
    		}
    ...
    		// THIS IS A NEW INSERTION POINT! WHODA THUNK IT?
    		menu.insertAfter(ICommonMenuConstants.GROUP_NEW, submenu);
    	}
    ...
    }
    

    That’s right, we cannot declare an insertion point for submenus in plugin.xml; the insertion point for a submenu has to be declared programmatically. Yes, code will have to be written, but we are going to steal copy most of it anyway.

  5. Start the runtime workbench and check that the popup menu appears when there is nothing displayed in the navigator. Exit the runtime workbench when you are done. Let’s create our own version of this code.
  6. Implement a version of CustomNewActionProvider to create an insertion point for the New Wizards.
    1. org.eclipse.ui.navigator.navigatorContent –> New –> actionProvider
      • class: customnavigator.popup.actionprovider.CustomNewActionProvider
      • id: customnavigator.popup.actionprovider.CustomNewAction
        • customnavigator.popup.actionprovider.CustomNewActionProvider (actionProvider) –> enablement –> New –> or
        • or –> New –> adapt
          • type: org.eclipse.core.resources.IResource
        • or –> New –> adapt
          • type: java.util.Collection
        • java.util.Collection –> New –> count
          • value: 0

    Return to customnavigator.popup.actionprovider.CustomNewActionProvider (actionProvider). Click the class link. Create the class. Add the following code:

    /**
     * Coder beware: this code is not warranted to do anything.
     * Some or all of this code is taken from the Eclipse code base.
     *
     * Copyright Mar 28, 2010 Carlos Valcarcel
     */
    package customnavigator.popup.actionprovider;
    
    import org.eclipse.jface.action.IMenuManager;
    import org.eclipse.jface.action.MenuManager;
    import org.eclipse.jface.action.Separator;
    import org.eclipse.ui.IWorkbenchWindow;
    import org.eclipse.ui.PlatformUI;
    import org.eclipse.ui.actions.ActionFactory;
    import org.eclipse.ui.navigator.CommonActionProvider;
    import org.eclipse.ui.navigator.ICommonActionExtensionSite;
    import org.eclipse.ui.navigator.ICommonMenuConstants;
    import org.eclipse.ui.navigator.ICommonViewerWorkbenchSite;
    import org.eclipse.ui.navigator.WizardActionGroup;
    
    public class CustomNewActionProvider extends CommonActionProvider {
    
        private static final String NEW_MENU_NAME = &quot;common.new.menu&quot;;//$NON-NLS-1$
    
        private ActionFactory.IWorkbenchAction showDlgAction;
    
        private WizardActionGroup newWizardActionGroup;
    
        private boolean contribute = false;
    
        @Override
        public void init(ICommonActionExtensionSite anExtensionSite) {
    
            if (anExtensionSite.getViewSite() instanceof ICommonViewerWorkbenchSite) {
                IWorkbenchWindow window = ((ICommonViewerWorkbenchSite) anExtensionSite.getViewSite()).getWorkbenchWindow();
                showDlgAction = ActionFactory.NEW.create(window);
    
                newWizardActionGroup = new WizardActionGroup(window, PlatformUI.getWorkbench().getNewWizardRegistry(), WizardActionGroup.TYPE_NEW, anExtensionSite.getContentService());
    
                contribute = true;
            }
        }
    
        @Override
        public void fillContextMenu(IMenuManager menu) {
            IMenuManager submenu = new MenuManager(
                    &quot;New&quot;,
                    NEW_MENU_NAME);
            if(!contribute) {
                return;
            }
    
            // fill the menu from the commonWizard contributions
            newWizardActionGroup.setContext(getContext());
            newWizardActionGroup.fillContextMenu(submenu);
    
            submenu.add(new Separator(ICommonMenuConstants.GROUP_ADDITIONS));
    
            // Add other ..
            submenu.add(new Separator());
            submenu.add(showDlgAction);
    
            // append the submenu after the GROUP_NEW group.
            menu.insertAfter(ICommonMenuConstants.GROUP_NEW, submenu);
        }
    
        @Override
        public void dispose() {
            if (showDlgAction!=null) {
                showDlgAction.dispose();
                showDlgAction = null;
            }
            super.dispose();
        }
    }
    

    Now associate (bind) the actionProvider to the popup through the viewerActionBinding:

    • Go to org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerActionBinding) –> (includes) –> org.eclipse.ui.navigator.resources.NewActions and change pattern to:
      • pattern: customnavigator.popup.actionprovider.CustomNewAction

    Notice the use of the id, not the class name.

  7. Start the runtime workbench and check that the New popup menu appears and that it has one submenu, Other, which both appears and is enabled when there is nothing displayed in the navigator. Exit the runtime workbench when you are done.
  8. Add Custom Project to the New menu
    Let’s put back one of the pieces we removed earlier:

    • org.eclipse.ui.navigator.navigatorContent –> New –> commonWizard
      • type: new
      • wizardId: customplugin.wizard.new.custom
  9. Start the runtime workbench and check that the popup menu appears when there is nothing displayed in the navigator. Exit the runtime workbench when you are done.
  10. Add the Refresh menu
    Let’s add the Refresh menu without the functionality…just to maintain purity of thought. Besides, I confuse easily. We’ll add the code in a few steps.First, add a new insertionPoint for the Refresh menu:

    • customnavigator.navigator#PopupMenu (popupMenu) –> New –> insertionPoint
      • name: group.build
      • separator: true

    The name group.build comes from ICommonMenuConstants found in org.eclipse.ui.navigator.

    Let’s define the actionProvider for the Refresh menu:

    1. org.eclipse.ui.navigator.navigatorContent –> New –> actionProvider
      • class: customnavigator.popup.actionprovider.CustomRefreshActionProvider
      • id: customnavigator.popup.actionprovider.CustomRefreshAction
    2. customnavigator.popup.actionprovider.CustomRefreshActionProvider –> enablement –> New –> or
    3. or –> New –> adapt
      • type: org.eclipse.core.resources.IResource
    4. or –> New –> adapt
      • type: java.util.Collection
    5. java.util.Collection –> New –> count
      • value: 0

    Next, let’s steal copy, the existing code from ResourceMgmtActionProvider to create CustomRefreshActionProvider.

    Return to customnavigator.popup.actionprovider.CustomRefreshActionProvider (actionProvider). Click the class link, create the class and insert this code:

    /**
     * Coder beware: this code is not warranted to do anything.
     * Some or all of this code is taken from the Eclipse code base.
     *
     * Copyright Apr 4, 2010 Carlos Valcarcel
     */
    package customnavigator.popup.actionprovider;
    
    import java.lang.reflect.InvocationTargetException;
    import java.util.Iterator;
    
    import org.eclipse.core.resources.IProject;
    import org.eclipse.core.resources.WorkspaceJob;
    import org.eclipse.core.runtime.CoreException;
    import org.eclipse.core.runtime.IAdaptable;
    import org.eclipse.core.runtime.IProgressMonitor;
    import org.eclipse.core.runtime.IStatus;
    import org.eclipse.core.runtime.Status;
    import org.eclipse.core.runtime.jobs.ISchedulingRule;
    import org.eclipse.jface.action.IMenuManager;
    import org.eclipse.jface.resource.ImageDescriptor;
    import org.eclipse.jface.viewers.IStructuredSelection;
    import org.eclipse.jface.viewers.StructuredViewer;
    import org.eclipse.jface.window.IShellProvider;
    import org.eclipse.osgi.util.NLS;
    import org.eclipse.swt.widgets.Shell;
    import org.eclipse.ui.IActionBars;
    import org.eclipse.ui.IWorkbenchCommandConstants;
    import org.eclipse.ui.actions.ActionFactory;
    import org.eclipse.ui.actions.RefreshAction;
    import org.eclipse.ui.actions.WorkspaceModifyOperation;
    import org.eclipse.ui.navigator.CommonActionProvider;
    import org.eclipse.ui.navigator.ICommonActionExtensionSite;
    import org.eclipse.ui.navigator.ICommonMenuConstants;
    
    import customnavigator.Activator;
    
    /**
     * The bulk of this code is taken from
     * org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider
     * which is provided with Eclipse in case you want to look up the original.
     *
     * @author carlos
     */
    public class CustomRefreshActionProvider extends CommonActionProvider {
    
        private RefreshAction refreshAction;
    
        private Shell         shell;
    
        /*
         * (non-Javadoc)
         * @see
         * org.eclipse.ui.navigator.CommonActionProvider#init(org.eclipse.ui.navigator.ICommonActionExtensionSite)
         */
        @Override
        public void init(ICommonActionExtensionSite aSite) {
            super.init(aSite);
            shell = aSite.getViewSite().getShell();
            makeActions();
        }
    
        @Override
        public void fillActionBars(IActionBars actionBars) {
            actionBars.setGlobalActionHandler(ActionFactory.REFRESH.getId(), refreshAction);
            updateActionBars();
        }
    
        /**
         * Adds the refresh resource actions to the context menu.
         *
         * @param menu
         * context menu to add actions to
         */
        @SuppressWarnings(&quot;rawtypes&quot;)
        @Override
        public void fillContextMenu(IMenuManager menu) {
            IStructuredSelection selection = (IStructuredSelection) getContext().getSelection();
            boolean hasClosedProjects = false;
            Iterator resources = selection.iterator();
    
            while (resources.hasNext() &amp;&amp; (!hasClosedProjects)) {
                Object next = resources.next();
                IProject project = null;
    
                if (next instanceof IProject) {
                    project = (IProject) next;
                } else if (next instanceof IAdaptable) {
                    project = (IProject) ((IAdaptable) next).getAdapter(IProject.class);
                }
    
                if (project == null) {
                    continue;
                }
    
                if (!project.isOpen()) {
                    hasClosedProjects = true;
                }
            }
    
            if (!hasClosedProjects) {
                refreshAction.selectionChanged(selection);
                menu.appendToGroup(ICommonMenuConstants.GROUP_BUILD, refreshAction);
            }
        }
    
        protected void makeActions() {
            IShellProvider sp = new IShellProvider() {
                @SuppressWarnings(&quot;synthetic-access&quot;)
                @Override
                public Shell getShell() {
                    return shell;
                }
            };
    
            refreshAction = new RefreshAction(sp) {
                @Override
                public void run() {
                    final IStatus[] errorStatus = new IStatus[1];
                    errorStatus[0] = Status.OK_STATUS;
                    final WorkspaceModifyOperation op = (WorkspaceModifyOperation) createOperation(errorStatus);
                    WorkspaceJob job = new WorkspaceJob(&quot;refresh&quot;) { //$NON-NLS-1$
    
                        @SuppressWarnings(&quot;synthetic-access&quot;)
                        @Override
                        public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
                            try {
                                op.run(monitor);
                                if (shell != null &amp;&amp; !shell.isDisposed()) {
                                    shell.getDisplay().asyncExec(new Runnable() {
                                        @Override
                                        public void run() {
                                            StructuredViewer viewer = getActionSite().getStructuredViewer();
                                            if (viewer != null &amp;&amp; viewer.getControl() != null &amp;&amp; !viewer.getControl().isDisposed()) {
                                                viewer.refresh();
                                            }
                                        }
                                    });
                                }
                            } catch (InvocationTargetException e) {
                                String msg = NLS.bind(&quot;Exception in {0}. run: {1}&quot;, getClass().getName(), e.getTargetException());
                                throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, msg, e
                                        .getTargetException()));
                            } catch (InterruptedException e) {
                                return Status.CANCEL_STATUS;
                            }
                            return errorStatus[0];
                        }
    
                    };
                    ISchedulingRule rule = op.getRule();
                    if (rule != null) {
                        job.setRule(rule);
                    }
                    job.setUser(true);
                    job.schedule();
                }
            };
            refreshAction.setDisabledImageDescriptor(getImageDescriptor(&quot;icons/refresh_nav_disabled.gif&quot;));//$NON-NLS-1$
            refreshAction.setImageDescriptor(getImageDescriptor(&quot;icons/refresh_nav_enabled.gif&quot;));//$NON-NLS-1$
            refreshAction.setActionDefinitionId(IWorkbenchCommandConstants.FILE_REFRESH);
        }
    
        /**
         * Returns the image descriptor with the given relative path.
         */
        protected ImageDescriptor getImageDescriptor(String relativePath) {
            return Activator.getIDEImageDescriptor(relativePath);
    
        }
    
        @Override
        public void updateActionBars() {
            IStructuredSelection selection = (IStructuredSelection) getContext().getSelection();
            refreshAction.selectionChanged(selection);
        }
    
    }
    

    I removed any code that did not contribute to the goal: make RefreshAction work. I changed comments as well.

    Yes, there is a compile error in CustomRefreshActionProvider.getImageDescriptor(). To fix that we have to add the following method to customnavigator.Activator:

    public class Activator extends AbstractUIPlugin {
    ...
        public static ImageDescriptor getIDEImageDescriptor(String imagePath) {
            return AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, imagePath);
        }
    }
    

    Compile error fixed.

    In order for the CustomRefreshActionProvider to be called we have to add an entry under org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerActionBinding):

    • org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerActionBinding) –> (includes) –> New –> actionExtension and change pattern to:
      • pattern: customnavigator.popup.actionprovider.CustomRefreshAction

    Again, notice the use of the id, not the class name. Could I simple have one entry for all of these action providers by putting in a pattern of customnavigator.popup.actionprovider.*? Of course, but where’s the fun in that (in other words, use that kind of pattern once you understand why you are using it. Until then, create individual entries)?

  11. Start the runtime workbench and check that the expected popup menus appears when there is nothing displayed in the navigator. Exit the runtime workbench.

    For those of you wondering when we added support for F5: the key binding is added by RefreshAction.
  12. Add Refresh icons
    method makeActions() is looking for an enabled and a disabled image for Refresh. Add the following images to your customnavigator icon folder:

    refresh_nav_enabled.gif:

    refresh_nav_disabled.gif:

  13. Start the runtime workbench and check that the expected popup menus appears when there is nothing displayed in the navigator. Exit the runtime workbench.

What Just Happened?

So we took a few steps back and a few steps forward.

  • We removed the plugin.xml entries that reconfigured the default popup.
  • We created a new popup menu definition with insertion points.
  • We declared and implemented two action providers: New and Refresh.
  • We declared two action extensions that referred to the action providers.
  • We declared a commonWizard entry to add the New Custom Project Wizard to the New popup menu.

Not bad for a post that I just couldn’t find the time for. For some reason it felt like a lot to do to create a new default menu. Adding the other items will be much simpler. I hope.

In other news: some of you may have noticed that in past posts I occasionally mentioned Deployment files as a feature. I am easily confused. The only things we are going to do are Custom Projects, Schemas and Stored Procedures. Any references to anything else are red herrings, blind alleys, and otherwise dead ends. Avoid them unless you are intent on finding a place to sleep.

References

Building a Common Navigator based viewer, Part III: Configuring Menus

Code

Activator.java

package customnavigator;

import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;

/**
 * The activator class controls the plug-in life cycle
 */
public class Activator extends AbstractUIPlugin {

	// The plug-in ID
	public static final String PLUGIN_ID = "customnavigator"; //$NON-NLS-1$

	// The shared instance
	private static Activator plugin;
	
	/**
	 * The constructor
	 */
	public Activator() {
	    // empty for now
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
	 */
	@Override
    public void start(BundleContext context) throws Exception {
		super.start(context);
		plugin = this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
	 */
	@Override
    public void stop(BundleContext context) throws Exception {
		plugin = null;
		super.stop(context);
	}

	/**
	 * Returns the shared instance
	 *
	 * @return the shared instance
	 */
	public static Activator getDefault() {
		return plugin;
	}

    public static Image getImage(String imagePath) {
        ImageDescriptor imageDescriptor = AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, imagePath);
        Image image = imageDescriptor.createImage();

        return image;
    }

    public static ImageDescriptor getIDEImageDescriptor(String imagePath) {
        return AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, imagePath);
    }
}

CustomNewActionProvider.java

/**
 * Coder beware: this code is not warranted to do anything. 
 * Some or all of this code is taken from the Eclipse code base.
 *
 * Copyright Mar 28, 2010 Carlos Valcarcel
 */
package customnavigator.popup.actionprovider;

import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.navigator.CommonActionProvider;
import org.eclipse.ui.navigator.ICommonActionExtensionSite;
import org.eclipse.ui.navigator.ICommonMenuConstants;
import org.eclipse.ui.navigator.ICommonViewerWorkbenchSite;
import org.eclipse.ui.navigator.WizardActionGroup;

/**
 * Provides the new (artifact creation) menu options for a context menu.
 * 
 * <p>
 * The added submenu has the following structure
 * </p>
 * 
 * <ul>
 * <li>a set of context sensitive wizard shortcuts (as defined by
 * <b>org.eclipse.ui.navigator.commonWizard</b>), </li>
 * <li>another separator, </li>
 * <li>a generic "Other" new wizard shortcut action</li>
 * </ul>
 * 
 * @since 3.2
 * 
 */
public class CustomNewActionProvider extends CommonActionProvider {

    private static final String NEW_MENU_NAME = "common.new.menu";//$NON-NLS-1$

    private ActionFactory.IWorkbenchAction showDlgAction;

    private WizardActionGroup newWizardActionGroup;

    private boolean contribute = false;

    @Override
    public void init(ICommonActionExtensionSite anExtensionSite) {

        if (anExtensionSite.getViewSite() instanceof ICommonViewerWorkbenchSite) {
            IWorkbenchWindow window = ((ICommonViewerWorkbenchSite) anExtensionSite.getViewSite()).getWorkbenchWindow();
            showDlgAction = ActionFactory.NEW.create(window);

            newWizardActionGroup = new WizardActionGroup(window, PlatformUI.getWorkbench().getNewWizardRegistry(), WizardActionGroup.TYPE_NEW, anExtensionSite.getContentService());

            contribute = true;
        }
    }

    /**
     * Adds a submenu to the given menu with the name "group.new" see
     * {@link ICommonMenuConstants#GROUP_NEW}). The submenu contains the following structure:
     * 
     * <ul>
     * <li>a set of context sensitive wizard shortcuts (as defined by
     * <b>org.eclipse.ui.navigator.commonWizard</b>), </li>
     * <li>another separator, </li>
     * <li>a generic "Other" new wizard shortcut action</li>
     * </ul>
     */
    @Override
    public void fillContextMenu(IMenuManager menu) {
        IMenuManager submenu = new MenuManager(
                Messages.CustomNewActionProvider_popupNewLabel,
                NEW_MENU_NAME);
        if(!contribute) {
            return;
        }

        // fill the menu from the commonWizard contributions
        newWizardActionGroup.setContext(getContext());
        newWizardActionGroup.fillContextMenu(submenu);

        submenu.add(new Separator(ICommonMenuConstants.GROUP_ADDITIONS));

        // Add other ..
        submenu.add(new Separator());
        submenu.add(showDlgAction);

        // append the submenu after the GROUP_NEW group.
        menu.insertAfter(ICommonMenuConstants.GROUP_NEW, submenu);
    }

    /* (non-Javadoc)
     * @see org.eclipse.ui.actions.ActionGroup#dispose()
     */
    @Override
    public void dispose() {
        if (showDlgAction!=null) {
            showDlgAction.dispose();
            showDlgAction = null;
        }
        super.dispose();
    }
}

CustomRefreshActionProvider.java

/**
 * Coder beware: this code is not warranted to do anything. 
 * Some or all of this code is taken from the Eclipse code base.
 * 
 * Copyright Apr 4, 2010 Carlos Valcarcel
 */
package customnavigator.popup.actionprovider;

import java.lang.reflect.InvocationTargetException;
import java.util.Iterator;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.window.IShellProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.RefreshAction;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.navigator.CommonActionProvider;
import org.eclipse.ui.navigator.ICommonActionExtensionSite;
import org.eclipse.ui.navigator.ICommonMenuConstants;

import customnavigator.Activator;

/**
 * The bulk of this code is taken from
 * org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider
 * which is provided with Eclipse in case you want to look up the original.
 * 
 * @author carlos
 */
public class CustomRefreshActionProvider extends CommonActionProvider {

    private RefreshAction refreshAction;

    private Shell         shell;

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.ui.navigator.CommonActionProvider#init(org.eclipse.ui.navigator.ICommonActionExtensionSite)
     */
    @Override
    public void init(ICommonActionExtensionSite aSite) {
        super.init(aSite);
        shell = aSite.getViewSite().getShell();
        makeActions();
    }

    @Override
    public void fillActionBars(IActionBars actionBars) {
        actionBars.setGlobalActionHandler(ActionFactory.REFRESH.getId(), refreshAction);
        updateActionBars();
    }

    /**
     * Adds the refresh resource actions to the context menu.
     * 
     * @param menu
     * context menu to add actions to
     */
    @SuppressWarnings("rawtypes")
    @Override
    public void fillContextMenu(IMenuManager menu) {
        IStructuredSelection selection = (IStructuredSelection) getContext().getSelection();
        boolean hasClosedProjects = false;
        Iterator resources = selection.iterator();

        while (resources.hasNext() && (!hasClosedProjects)) {
            Object next = resources.next();
            IProject project = null;

            if (next instanceof IProject) {
                project = (IProject) next;
            } else if (next instanceof IAdaptable) {
                project = (IProject) ((IAdaptable) next).getAdapter(IProject.class);
            }

            if (project == null) {
                continue;
            }

            if (!project.isOpen()) {
                hasClosedProjects = true;
            }
        }
        
        if (!hasClosedProjects) {
            refreshAction.selectionChanged(selection);
            menu.appendToGroup(ICommonMenuConstants.GROUP_BUILD, refreshAction);
        }
    }

    protected void makeActions() {
        IShellProvider sp = new IShellProvider() {
            @SuppressWarnings("synthetic-access")
            @Override
            public Shell getShell() {
                return shell;
            }
        };

        refreshAction = new RefreshAction(sp) {
            @Override
            public void run() {
                final IStatus[] errorStatus = new IStatus[1];
                errorStatus[0] = Status.OK_STATUS;
                final WorkspaceModifyOperation op = (WorkspaceModifyOperation) createOperation(errorStatus);
                WorkspaceJob job = new WorkspaceJob("refresh") { //$NON-NLS-1$

                    @SuppressWarnings("synthetic-access")
                    @Override
                    public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
                        try {
                            op.run(monitor);
                            if (shell != null && !shell.isDisposed()) {
                                shell.getDisplay().asyncExec(new Runnable() {
                                    @Override
                                    public void run() {
                                        StructuredViewer viewer = getActionSite().getStructuredViewer();
                                        if (viewer != null && viewer.getControl() != null && !viewer.getControl().isDisposed()) {
                                            viewer.refresh();
                                        }
                                    }
                                });
                            }
                        } catch (InvocationTargetException e) {
                            String msg = NLS.bind(Messages.CustomRefreshActionProvider_invocationTargetExceptionMessage, getClass().getName(), e.getTargetException());
                            throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, msg, e
                                    .getTargetException()));
                        } catch (InterruptedException e) {
                            return Status.CANCEL_STATUS;
                        }
                        return errorStatus[0];
                    }

                };
                ISchedulingRule rule = op.getRule();
                if (rule != null) {
                    job.setRule(rule);
                }
                job.setUser(true);
                job.schedule();
            }
        };
        refreshAction.setDisabledImageDescriptor(getImageDescriptor("icons/refresh_nav_disabled.gif"));//$NON-NLS-1$
        refreshAction.setImageDescriptor(getImageDescriptor("icons/refresh_nav_enabled.gif"));//$NON-NLS-1$
        refreshAction.setActionDefinitionId(IWorkbenchCommandConstants.FILE_REFRESH);
    }

    /**
     * Returns the image descriptor with the given relative path.
     */
    protected ImageDescriptor getImageDescriptor(String relativePath) {
        return Activator.getIDEImageDescriptor(relativePath);

    }

    @Override
    public void updateActionBars() {
        IStructuredSelection selection = (IStructuredSelection) getContext().getSelection();
        refreshAction.selectionChanged(selection);
    }

}

Messages.java

package customnavigator.popup.actionprovider;

import org.eclipse.osgi.util.NLS;

public class Messages extends NLS {
    private static final String BUNDLE_NAME = "customnavigator.popup.actionprovider.messages"; //$NON-NLS-1$
    public static String        CustomNewActionProvider_popupNewLabel;
    public static String CustomRefreshActionProvider_invocationTargetExceptionMessage;
    static {
        // initialize resource bundle
        NLS.initializeMessages(BUNDLE_NAME, Messages.class);
    }

    private Messages() {
    }
}

messages.properties

CustomNewActionProvider_popupNewLabel=New
CustomRefreshActionProvider_invocationTargetExceptionMessage=Exception in {0}. run: {1}

plugin.xml

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.ui.views">
      <category
            id="customnavigator.category"
            name="%category.name">
      </category>
      <view
            allowMultiple="false"
            category="customnavigator.category"
            class="org.eclipse.ui.navigator.CommonNavigator"
            icon="icons/navigator.png"
            id="customnavigator.navigator"
            name="%view.name">
      </view>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.viewer">
      <viewer
            viewerId="customnavigator.navigator">
         <popupMenu
               id="customnavigator.navigator#PopupMenu">
            <insertionPoint
                  name="group.new">
            </insertionPoint>
            <insertionPoint
                  name="group.build"
                  separator="true">
            </insertionPoint>
         </popupMenu>
      </viewer>
      <viewerContentBinding
            viewerId="customnavigator.navigator">
         <includes>
            <contentExtension
                  pattern="customnavigator.navigatorContent">
            </contentExtension>
         </includes>
      </viewerContentBinding>
      <viewerActionBinding
            viewerId="customnavigator.navigator">
         <includes>
            <actionExtension
                  pattern="customnavigator.popup.actionprovider.CustomNewAction">
            </actionExtension>
            <actionExtension
                  pattern="customnavigator.popup.actionprovider.CustomRefreshAction">
            </actionExtension>
         </includes>
      </viewerActionBinding>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.navigatorContent">
      <navigatorContent
            activeByDefault="true"
            contentProvider="customnavigator.navigator.ContentProvider"
            id="customnavigator.navigatorContent"
            labelProvider="customnavigator.navigator.LabelProvider"
            name="%navigatorContent.name">
         <triggerPoints>
            <instanceof
                  value="org.eclipse.core.resources.IWorkspaceRoot">
            </instanceof>
         </triggerPoints>
         <commonSorter
               class="customnavigator.sorter.SchemaCategorySorter"
               id="customnavigator.sorter.schemacategorysorter">
            <parentExpression>
               <or>
                  <instanceof
                        value="customnavigator.navigator.CustomProjectSchema">
                  </instanceof>
               </or>
            </parentExpression>
         </commonSorter>
      </navigatorContent>
      <actionProvider
            class="customnavigator.popup.actionprovider.CustomNewActionProvider"
            id="customnavigator.popup.actionprovider.CustomNewAction">
         <enablement>
            <or>
               <adapt
                     type="org.eclipse.core.resources.IResource">
               </adapt>
               <adapt
                     type="java.util.Collection">
                  <count
                        value="0">
                  </count>
               </adapt>
            </or>
         </enablement>
      </actionProvider>
      <actionProvider
            class="customnavigator.popup.actionprovider.CustomRefreshActionProvider"
            id="customnavigator.popup.actionprovider.CustomRefreshAction">
         <enablement>
            <or>
               <adapt
                     type="org.eclipse.core.resources.IResource">
               </adapt>
               <adapt
                     type="java.util.Collection">
                  <count
                        value="0">
                  </count>
               </adapt>
            </or>
         </enablement>
      </actionProvider>
      <commonWizard
            type="new"
            wizardId="customplugin.wizard.new.custom">
         <enablement></enablement>
      </commonWizard>
   </extension>

</plugin>

MANIFEST.MF

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: customnavigator;singleton:=true
Bundle-Version: 1.0.2.0
Bundle-Activator: customnavigator.Activator
Require-Bundle: org.eclipse.ui,
 org.eclipse.core.runtime,
 org.eclipse.core.resources,
 org.eclipse.ui.ide;bundle-version="3.6.0",
 org.eclipse.ui.navigator,
 customplugin;bundle-version="1.0.1"
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Export-Package: customnavigator,
 customnavigator.navigator,
 customnavigator.popup.actionprovider,
 customnavigator.sorter

Writing an Eclipse Plug-in (Part 11): Common Navigator: Displaying Custom Resources or Refresh Or Die or The Magic of navigatorContent

November 14, 2009 8 comments

Today’s tasks are:

  • Open the navigator in a perspective (doesn’t matter which one)
  • Create a Custom Project
  • Create a non-custom project. It should not appear in the custom navigator.

In breaking with tradition I am going to simply tell you what needs to be done (What to Do) and then I will rationalize the implementation for the work of art that it is (Why did we do it?).

What to Do

Let’s configure customnavigator’s plugin.xml:

  1. customnavigator –> plugin.xml –> org.eclipse.ui.views –> Custom Plug-in Navigator
  2. Change customnavigator.navigator.CustomNavigator to org.eclipse.ui.navigator.CommonNavigator
  3. Delete CustomNavigator.java (YAGNI for the foreseeable future)
  4. Change org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> (or) –> customnavigator.navigator.CustomProjectWorkbenchRoot to org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> org.eclipse.core.resources.IWorkspaceRoot

Change the ContentProvider code to reflect the following changes:

ContentProvider.getParent()

    @Override
    public Object getParent(Object element) {
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

ContentProvider.hasChildren()

    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

ContentProvider.getChildren()/createCustomProjectParents()/createCustomProjectParent()

    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = createCustomProjectParent(projects[i]);
            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

ContentProvider.inputChanged()/ContentProvider constructor/dispose()

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

ContentProvider.resourceChanged()

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        _viewer.refresh();
    }

Why did we do it?

So, as we saw in a previous post, we can display a custom project that has specialized child nodes based on the kind of project we create; in this case a project with a nature of customplugin.projectNature.

Today’s task (a repeat of what I listed above) was:

  • Open the navigator in a perspective (doesn’t matter which one)
  • Create a Custom Project
  • Create a non-custom project. It should not appear in the custom navigator.

The challenge is: how to configure the Custom Navigator so that it will only display custom projects. In my never ending quest to write as little code as possible I will show you what to configure in plugin.xml and what code to add to ContentProvider to make it happen.

Supremely obvious point #1: without content a common navigator doesn’t display anything.
Supremely obvious point #2: you can configure a resource-based CNF navigator to do one of three things (if you can think of more, let me know or better yet write it up):

  1. Display only certain resources,
  2. Display resources in a custom way or
  3. Some combination of 1 and 2.

We want to do #3: the navigator should only display custom projects and resources related to custom projects should be displayed in a custom way.

If we had wanted to display all of the top level resources in the navigator we would just define a viewerContentBinding of org.eclipse.ui.navigator.resourceContent. Since that content binding is already defined our work would be done. That however would be too easy and wrong for what we want to do (it is amazing how often easy and wrong go together).

We know that we can display custom projects and related resources in a custom way (we did that, remember?). Now we want to make sure that no matter how many projects we create the navigator will only display our projects our way (like fast food without the fat).

In order to do this we are going to take a few steps back and a few steps forward. We are going to:

  • Delete our existing navigator class (back one step)
  • Replace the existing navigator class with a CommonNavigator (back another step)
  • Attach a content binding to the viewer (which is actually already there, but we are changing it; back another step)
  • Configure a navigatorContent (which is also already there)
    • Define an object type to send into the content provider (called the triggerPoint): IWorkspaceRoot
    • Implement the content provider code to wrap our project and its children
    • Register a resource change listener in the content provider to update the viewer when new projects are added

First the ending:
Let’s configure customnavigator’s plugin.xml:

  1. Go to customnavigator –> plugin.xml –> org.eclipse.ui.views –> Custom Plug-in Navigator
  2. Change customnavigator.navigator.CustomNavigator to org.eclipse.ui.navigator.CommonNavigator
  3. Delete CustomNavigator.java (YAGNI)
  4. Change org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> (or) –> customnavigator.navigator.CustomProjectWorkbenchRoot to org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> org.eclipse.core.resources.IWorkspaceRoot

Why change the triggerPoint from CustomProjectWorkbenchRoot to IWorkspaceRoot? Believe it or not, the CustomProjectWorkbenchRoot is overkill for what we want. In fact, using CustomProjectWorkbenchRoot will keep the navigator from working properly so in the tradition of YAGNI, we got rid of it. We may live to regret it depending on how complex this project gets, but for now out with the trash.

And now for the ContentProvider code:
ContentProvider.getParent()

    @Override
    public Object getParent(Object element) {
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

What is this code doing?

  • If an IWorkspaceRoot comes in return null as it has no parent.
  • If an IProject comes in, return the Workspace root.
  • If a node of type ICustomProjectElement, and all of the custom project wrappers are this type, ask the object for its parent. The project will return the Workspace root, the child nodes will return the project or its custom parent node.
  • For all others return null (no parent)

Since the “all others” and IWorkspaceRoot return null we just need to check for the IProject and ICustomProjectElement.

ContentProvider.hasChildren()

    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

So who has children? The only two types I care about are IWorkspaceRoot and ICustomProjectElement, otherwise return null.

  • If an IWorkspaceRoot return true if there are any projects. I know, I know, I should check if any of the projects have the custom project nature, but that is overhead I will safely ignore for now.
  • If an ICustomProjectElement comes in ask if it has any children.

Stay on target.

ContentProvider.getChildren()/createCustomProjectParents()/createCustomProjectParent()

    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = createCustomProjectParent(projects[i]);
            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

In getChildren() all I care about is:

  • Returning wrapped custom project resources
  • Returning the children of a custom project
  • Returning an empty array If I don’t recognize the incoming element .

The createCustomProjectParents()/createCustomProjectParent() methods wrap my custom project IProjects; all other projects will be ignored.

ContentProvider.inputChanged()/ContentProvider constructor/dispose()

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

The order of the methods above are out of sequence for the class, but in sequence for this explanation.

In inputChanged() someone has done something that we need to pay attention to. In this case, a new project was added (we haven’t coded for deletions yet). Just save the viewer reference; we’ll need it later. However, if the input has changed we need a way to determine what that change was. Enter resource change listeners.

[If you don’t know anything about event handling in Java, much less Eclipse, the following explanation is probably not going to make a lot of sense.]

For now, the easiest place to register a listener is in the ContentProvider constructor so I placed it there. Of course, whatever we register we should unregister so in dispose() we remove the resource change listener.

Which leads us to the resource change listener itself.

ContentProvider.resourceChanged()

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        _viewer.refresh();
    }

Almost too easy. Our simple navigator just needs to be told to refresh itself when a change occurs.

To summarize: when our input changes we save the viewer; when a resource changes we call resourceChanged() which refreshes the viewer.

Life is beautiful.

What Just Happened?

Now it is time for me to admit something: I hate beets (that is: the vegetable).

Well, more significantly, I have to admit it took a while for me to figure out how simple this truly was. I looked at examples, read posts, drank plenty of Guinness. Finally, I woke up one morning and thought Oh! Is that all it is?

What I found was that the examples did not do what I was trying to do (don’t you hate when that happens?). They always seem to use the pre-existing resourceContent binding in addition to other stuff which didn’t help me at all (well, it did actually help, but kept me confused for too many nights).

The moral of the story: use resourceContent if you need to show the existing resources in a standard way or with exceptions.

Don’t use resourceContent if you want to take full control over the navigator.

That might be too general and all encompassing, but, what the hell, what isn’t?

And of course all this may change as I add more functionality to fill the custom project with custom content. Stay tuned, boys and girls.

Next time: keeping the nodes opened or closed when we add a resource.

References and Thanks

Building a Common Navigator based viewer, Part I: Defining the Viewer

Code

plugin.xml

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.ui.views">
      <category
            id="customnavigator.category"
            name="%category.name">
      </category>
      <view
            allowMultiple="false"
            category="customnavigator.category"
            class="org.eclipse.ui.navigator.CommonNavigator"
            icon="icons/navigator.png"
            id="customnavigator.navigator"
            name="%view.name">
      </view>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.viewer">
      <viewerActionBinding
            viewerId="customnavigator.navigator">
         <includes>
            <actionExtension
                  pattern="org.eclipse.ui.navigator.resources.*">
            </actionExtension>
         </includes>
      </viewerActionBinding>
      <viewerContentBinding
            viewerId="customnavigator.navigator">
         <includes>
            <contentExtension
                  pattern="customnavigator.navigatorContent">
            </contentExtension>
         </includes>
      </viewerContentBinding>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.navigatorContent">
      <navigatorContent
            activeByDefault="true"
            contentProvider="customnavigator.navigator.ContentProvider"
            id="customnavigator.navigatorContent"
            labelProvider="customnavigator.navigator.LabelProvider"
            name="%navigatorContent.name">
         <triggerPoints>
            <instanceof
                  value="org.eclipse.core.resources.IWorkspaceRoot">
            </instanceof>
         </triggerPoints>
         <commonSorter
               class="customnavigator.sorter.SchemaCategorySorter"
               id="customnavigator.sorter.schemacategorysorter">
            <parentExpression>
               <or>
                  <instanceof
                        value="customnavigator.navigator.CustomProjectSchema">
                  </instanceof>
               </or>
            </parentExpression>
         </commonSorter>
      </navigatorContent>
   </extension>

</plugin>
ContentProvider.java
/**
 * Coder beware: this code is not warranted to do anything.
 * Copyright Oct 17, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;

import customplugin.natures.ProjectNature;

/**
 * @author carlos and Hidden Clause
 */
public class ContentProvider implements ITreeContentProvider, IResourceChangeListener {

    private static final Object[]   NO_CHILDREN = {};
    Viewer _viewer;
    private int _count = 1;

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
     */
    @Override
    public Object[] getChildren(Object parentElement) {
        System.out.println("ContentProvider.getChildren: " + parentElement.getClass().getName()); //$NON-NLS-1$
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
     */
    @Override
    public Object getParent(Object element) {
        System.out.println("ContentProvider.getParent: " + element.getClass().getName()); //$NON-NLS-1$
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
     */
    @Override
    public boolean hasChildren(Object element) {
        System.out.println("ContentProvider.hasChildren: " + element.getClass().getName()); //$NON-NLS-1$
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
     */
    @Override
    public Object[] getElements(Object inputElement) {
        // This is the same as getChildren() so we will call that instead
        System.out.println("ContentProvider.getElements: " + inputElement.getClass().getName()); //$NON-NLS-1$
        return getChildren(inputElement);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
     */
    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
     */
    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        _viewer.refresh();
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = createCustomProjectParent(projects[i]);
            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

}

Writing an Eclipse Plug-in (Part 8): Common Navigator: Adding a New Sorter Under navigatorContent

October 24, 2009 Leave a comment

[Just an FYI: I updated Part 7‘s code to reflect the suggestions from Simon Zambrovski so the code is slightly different. The zip file was also updated.]

[I have upgraded to Eclipse 3.6 M2. Nothing should need to be changed.]

Today’s tasks:

  • Remove the warning from plugin.xml
  • Add a customSorter to the navigator
  • Refactor the CustomProjectSchema* classes
  • Implement the SchemaCategorySorter
  • Remove the warning from plugin.xml

Remove the Warning from plugin.xml

This is code hygiene.

  1. Open plugin.xml
  2. Click on the light bulb, select Add Missing Packages
  3. Save plugin.xml
    1. The manifest editor will add a new package to Export-Package:

      MANIFEST.MF
      ...
      Export-Package: customnavigator,
       customnavigator.navigator
      

      Pat yourself on the back.

      Add a customSorter to the Navigator

      A funny thing happened when I took a good look at the categories listed under Custom Project: the Schema categories were not in the right order. I wanted them to appear as:

      • Tables
      • Views
      • Filters

      Instead they appear as:

      • Filters
      • Tables
      • Views

      In other words, they are appearing in alphabetical order instead of the arbitrary order I have determined is the correct one for my custom project. How to fix such a stain on humanity? Add a custom sorter.

      Do the following in the customnavigator plugin.xml:

      1. Open org.eclipse.ui.navigator.navigatorContent
      2. Right-click on Custom Navigator Content and select New –> commonSorter. Enter:
        • class: customnavigator.sorter.SchemaCategorySorter
        • id: customnavigator.sorter.schemacategorysorter
      3. Click on the class link and, when the New Java Class wizard opens, click Finish.
      4. Right click customnavigator.sorter.schemacategorysorter (commonSorter) and select New –> parentExpression
      5. Right click parentExpression and select New –> or
      6. Right click or and select New –> instanceof. Since we want to sort the categories under the Schema category we set the class to trigger on to be the CustomProjectSchema. Enter:
        • value: customnavigator.navigator.CustomProjectSchema
      7. Save plugin.xml

      Refactor the CustomProjectSchema* classes

      Before we implement the SchemaCategorySorter let’s do some upfront work. Refactor the constant NAME into the 3 category class CustomProjectSchemaTables, CustomProjectSchemaViews, CustomProjectSchemaFilters.

      public class CustomProjectSchemaTables implements ICustomProjectElement {
      
          public static final String NAME = "Tables"; //$NON-NLS-1$
          ...
          public String getText() {
              return NAME;
          }
          ...
      }
      
      public class CustomProjectSchemaViews implements ICustomProjectElement {
      
          public static final String NAME = "Views"; //$NON-NLS-1$
          ...
          public String getText() {
              return NAME;
          }
          ...
      }
      
      public class CustomProjectSchemaFilters implements ICustomProjectElement {
      
          public static final String NAME = "Filters"; //$NON-NLS-1$
          ...
          public String getText() {
              return NAME;
          }
          ...
      }
      

      I know, I know…there is a lot of great refactoring in these classes. Patience. All will be revealed (in another post).

      Implement the SchemaCategorySorter

      The sorter is straightforward.

      The SchemaCategorySorter works just like a Comparable object: it is expecting either a 1, 0 or -1, or more generically, a positive number, a zero or a negative number depending on how you want to sort the incoming items. We inherit from ViewerSorter as that is the superclass the New Java Class wizard selected as the parent class. Who are we to argue?

      We override compare() to make Tables first and Filters last:

      /**
       * Coder beware: this code is not warranted to do anything.
       *
       * Copyright Oct 24, 2009 Carlos Valcarcel Hidden Clause
       */
      package customnavigator.sorter;
      
      import java.text.Collator;
      
      import org.eclipse.jface.viewers.Viewer;
      import org.eclipse.jface.viewers.ViewerSorter;
      
      import customnavigator.navigator.CustomProjectSchemaFilters;
      import customnavigator.navigator.CustomProjectSchemaTables;
      import customnavigator.navigator.CustomProjectSchemaViews;
      import customnavigator.navigator.ICustomProjectElement;
      
      /**
       * @author carlos
       *
       */
      public class SchemaCategorySorter extends ViewerSorter {
      
          /**
           * 
           */
          public SchemaCategorySorter() {
              // purposely empty
          }
      
          /**
           * @param collator
           */
          public SchemaCategorySorter(Collator collator) {
              super(collator);
          }
      
          @Override
          public int compare(Viewer viewer, Object e1, Object e2) {
              String catName1 = ((ICustomProjectElement)e1).getText();
              String catName2 = ((ICustomProjectElement)e2).getText();
              
              int result = -1;
              if (catName1.equals(CustomProjectSchemaTables.NAME)) {
                  result = -1;
              } else if (catName2.equals(CustomProjectSchemaTables.NAME)) {
                  result = 1;
              } else if (catName1.equals(CustomProjectSchemaViews.NAME)) {
                  result = -1;
              } else if (catName1.equals(CustomProjectSchemaFilters.NAME)) {
                  result = 1;
              } // else result == -1
              
              return result;
          }
      
      }
      

      Remove the warning from plugin.xml

      In the plugin.xml editor go to the MANIFEST.ML tab. There is a warning icon in the left hand margin of the file. Click on the icon; it will open a window with a single suggestion for fixing the warning: Add Missing Packages. Double click Add Missing Packages. The manifest editor will add a new package to Export-Package:

      MANIFEST.MF
      ...
      Export-Package: customnavigator,
       customnavigator.navigator,
       customnavigator.sorter
      

      Go ahead. Start the runtime workbench and take a look.
      custom-navigator-end-of-part-8

      So What Just Happened?

      After correcting the missing package export (which we should have done in the previous post) we:

      • Added a new commonSorter to the navigatorContent extension
      • Created and implemented the SchemaCategorySorter
      • Minimally refactored the CustomProjectSchema* classes so their names would be available in constants
      • Exported the customnavigator.sorter in MANIFEST.MF

      The cat is alive and resting comfortably.