Archive

Posts Tagged ‘eclipse 3.6’

Writing an Eclipse Plug-in (Part 22): Common Navigator: Adding submenus (Presentation)

May 30, 2010 2 comments

[If anyone cares: I have upgraded to Eclipse 3.6 RC3]

Happy Memorial Day weekend, everyone (at least those in the United States)!

For those who have accepted that life is meaningless, short and painful: chow down at the grill! Eat, drink and be merry for tomorrow you die!

For those who believe that life is meaningful, long and joyous: don’t overdo your carbs, remember that hot dogs have artificial colors and mystery meat, and grilling your food causes the formation of cancer causing agents due to carbonization. In other words, don’t eat, drink or be too merry because the odds are you won’t be dying tomorrow (or maybe you will).

But, hey! Enjoy the weekend!

Well, with that sense of merriment out of the way it is time to go back to the real reason we are here: finishing up the popup menu.
Read more…

Writing an Eclipse Plug-in (Part 10): Custom Project: Creating a Custom File Type

October 31, 2009 7 comments

Happy Halloween! Trick or treat!

Ah! I love the smell of plug-in progress in the morning. Who knows? Somewhere around Part 42 there might be enough done to actually accomplish something.

Speaking of which: our custom project doesn’t do very much at this point. While there is plenty to add to the custom navigator I fear that the customplugin project has been feeling neglected.

I have a few things to add to the customplugin before returning to the navigator; the navigator will continue to burn a significant amount of my attention…unless I decide to go back to my genetic programming examples. Maybe I’ll toss a coin to see which I decide to do next. Or maybe I’ll just check on the cat.

Anyway, for this post we have a few simple things to do:

  • Add the schema file type to the Custom Wizards category
  • Add the deployment file type to the Custom Wizards category
  • Implement the schema file creation code
  • Implement the deployment file creation code

Adding A New Schema File Wizard

You know the drill. Follow the yellow brick road:

  1. customplugin –> plugin.xml –> Extensions –> org.eclipse.ui.newWizards –> new –> wizard
    • id: customplugin.wizard.file.schema
    • name: Schema File
    • class: customplugin.wizards.CustomProjectNewSchemaFile
    • icon: icons/schema-file_16x16.png [copy another image to your icons folder and point to it here]
    • category: customplugin.category.wizards
    • descriptionImage: icons/schema-file_32x32.png [copy another image to your icons folder and point to it here]
  2. Click the class link for CustomProjectNewSchemaFile
  3. Everything looks good
  4. Click Finish to close the New Java Class Wizard
  5. [If you must: start the runtime workbench, press Ctrl+N and look in the Custom Wizards folder; the Schema File and icon should be visible. Exit the runtime workbench.]

Now that we have a schema file creation wizard defined let’s implement some code.

First the constructor so the wizard window has a title:

CustomProjectNewSchemaFile.java

public class CustomProjectNewSchemaFile extends Wizard implements INewWizard {

    public CustomProjectNewSchemaFile() {
        setWindowTitle("New Schema File");
    }

    ...
}

[Update: In doing the above I remembered that I never got around to doing this for the New Custom Project Wizard. The New Custom Project Wizard post, and the New Custom Project Wizard refactoring post have been updated.]

Next, in init() we save the incoming selection values as we will need them later for the constructor of the file creation page:

    private IWorkbench _workbench;
    private IStructuredSelection _selection;

    ...

    /* (non-Javadoc)
     * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection)
     */
    @Override
    public void init(IWorkbench workbench, IStructuredSelection selection) {
        _workbench = workbench;
        _selection = selection;
    }

    ...

In addPages() we call a class we haven’t created yet: WizardSchemaNewFileCreationPage. It subclasses WizardNewFileCreationPage which does exactly what we want: create a file for us. This is just another reason why it is so important to know what pieces are available within Eclipse for your use.

    private WizardNewFileCreationPage _pageOne;

    ...

    @Override
    public void addPages() {
        super.addPages();

        _pageOne = new WizardSchemaNewFileCreationPage(_selection);

        addPage(_pageOne);
    }

    ...

The variable declaration for _pageOne is purposely declared as WizardNewFileCreationPage even though the call in addPages() is to WizardSchemaNewFileCreationPage. We are only overriding method calls to facilitate the initialization of the object. Create the new class: press Ctrl+1 on the line in addPages() and select Create Class WizardSchemaNewFileCreationPage. It should automatically select WizardNewFileCreationPage as the superclass. Click Finish to create the new class.

Implement WizardSchemaNewFileCreationPage with:

public class WizardSchemaNewFileCreationPage extends WizardNewFileCreationPage {

    public WizardSchemaNewFileCreationPage(IStructuredSelection selection) {
        super("Custom Plug-in Schema File Wizard", selection);

        setTitle("Schema File Wizard");
        setDescription("Create a Schema File");
        setFileExtension("xml");
    }

    @Override
    protected InputStream getInitialContents() {
        String xmlTemplate = "<hc-schema>\n"
            + "  <tables></tables>\n"
            + "  <filters></filters>\n"
            + "  <views></views>\n"
            + "</hc-schema>\n";
        return new ByteArrayInputStream(xmlTemplate.getBytes());
    }
}

The constructor above speaks for itself. The getInitialContents() not so much. This method is used by the wizard to find default contents for the new file if one is created. Since this is my schema file I now have a test version of the file to display. Since this will be refactored into another file we are safe hard-coding it for now.

Returning to CustomProjectNewSchemaFile: Add the following to performFinish():

    @Override
    public boolean performFinish() {
        boolean result = false;

        IFile file = _pageOne.createNewFile();
        result = file != null;

        if (result) {
            try {
                IDE.openEditor(_workbench.getActiveWorkbenchWindow().getActivePage(), file);
            } catch (PartInitException e) {
                e.printStackTrace();
            }
        } // else no file created...result == false

        return result;
    }

The code is simple and self explanatory except for the call to openEditor();it automatically opens the editor for our XML file using whatever editor has been configured for XML files or the default editor if an XML editor is not available. The first argument to openEditor() needs a reference to IWorkbenchPage and we get it by asking the workbench for it. A small price to pay to have the editor opened automatically for us.

Go ahead and try the runtime workbench to check out your handiwork. You should be able to create a plain vanilla resource project and create a schema file to go with it. Life is good.

With that test out of the way we are now emboldened to use a template file instead of a hard coded XML string.

Create a folder named templates under your customplugin folder. In the templates folder create a file named schema-template.xml. Insert the following XML into the file.

<hc-schema>
    <tables>
    </tables>
    <views>
    </views>
    <filters>
    </filters>
</hc-schema>

With the above in place update getInitialContents():

WizardSchemaNewFileCreationPage.java
    protected InputStream getInitialContents() {
        String templateFilePath = "/templates/schema-template.xml";
        InputStream inputStream = null;
        try {
            inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
        } catch (IOException e) {
            // send back null
        }

        return inputStream;
    }

Confirm nothing is broken by starting the runtime workbench.

Adding A New Deployment File Wizard

Now let’s do the same for the deployment file:

  1. customplugin –> plugin.xml –> Extensions –> org.eclipse.ui.newWizards –> new –> wizard
    • id: customplugin.wizard.file.deployment
    • name: Deployment File
    • class: customplugin.wizards.CustomProjectNewDeploymentFile
    • icon: icons/deployment-file_16x16.png [copy another image to your icons folder and point to it here]
    • category: customplugin.category.wizards
    • descriptionImage: icons/deployment-file_32x32.png [copy another image to your icons folder and point to it here]
      • Click the class link for CustomProjectNewDeploymentFile
      • Everything looks good
      • Click Finish to close the New Java Class Wizard
      • [Again, quick test: start the runtime workbench, press Ctrl+N and look in the Custom Wizards folder; the Deployment File and icon should be visible. Exit the runtime workbench.]

          Since we already know that we are using the WizardNewFileCreationPage as a subclass for the page that will create our file let’s create it upfront.

          public class WizardDeploymentNewFileCreationPage extends WizardNewFileCreationPage {
          
              public WizardDeploymentNewFileCreationPage(IStructuredSelection selection) {
                  super("Custom Plug-in Deployment File Wizard", selection);
                  
                  setTitle("Deployment File Wizard");
                  setDescription("Create a Deployment File");
                  setFileExtension("xml");
              }
          
              @Override
              protected InputStream getInitialContents() {
                  String templateFilePath = "/templates/deployment-template.xml";
                  InputStream inputStream = null;
                  try {
                      inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
                  } catch (IOException e) {
                      // send back null
                  }
          
                  return inputStream;
              }
          }
          

          Create another template file in the templates folder named deployment-schema.xml. Contents follow:

          <hc-deployment>
          </hc-deployment>
          

          The actual wizard code for the CustomProjectNewDeploymentFile also looks suspiciously like the code for the schema file:

          public abstract class CustomProjectNewDeploymentFile extends Wizard implements INewWizard {
          
              private WizardNewFileCreationPage _pageOne;
              private IWorkbench                _workbench;
              private IStructuredSelection      _selection;
          
              public CustomProjectNewDeploymentFile() {
                  setWindowTitle("New Deployment File");
              }
          
              @Override
              public void addPages() {
                  super.addPages();
          
                  _pageOne = new WizardDeploymentNewFileCreationPage(_selection);
          
                  addPage(_pageOne);
              }
              @Override
              public boolean performFinish() {
                  boolean result = false;
          
                  IFile file = _pageOne.createNewFile();
                  result = file != null;
          
                  if (result) {
                      try {
                          IDE.openEditor(_workbench.getActiveWorkbenchWindow().getActivePage(), file);
                      } catch (PartInitException e) {
                          e.printStackTrace();
                      }
                  } // else no file created...result == false
          
                  return result;
              }
          
              @Override
              public void init(IWorkbench workbench, IStructuredSelection selection) {
                  _workbench = workbench;
                  _selection = selection;
              }
          }
          

          After all of the above in the runtime workbench you should be able to create a schema file and a deployment file and the default text editor should open once each file is defined in the New File Wizard.

          Run a quick test. From the runtime workbench create a project, create a schema file and deployment file.

          Woo hoo! All done!

          Oh…wait…time to refactor a few things.

          Refactor Strings

          Time to wash up!

          Once again, go to the plugin.xml Overview tab of the customplugin and run the Externalize String Wizard to externalize the two new strings we added. Change:

          • wizard.name.0 to wizard.name.schema
          • wizard.name.1 to wizard.name.deployment

          In the schema file related files refactor:

          • the wizard window title to WIZARD_NAME and add $NON-NLS$ using the Quick Fix functionality.
          • the WizardSchemaNewFileCreationPage title to PAGE_NAME.

          In WizardSchemaNewFileCreationPage:

          • Open the Externalize String wizard (one way: hover over one of the strings to open the wizard)
          • Change WizardSchemaNewFileCreationPage_0 to WizardSchemaNewFileCreationPage_Schema_File_Wizard by clicking in the desired field and typing Schema_File_Wizard
          • Change WizardSchemaNewFileCreationPage_1 to WizardSchemaNewFileCreationPage_Create_a_Schema_File by clicking in the desired field and typing Create_a_Schema_File
          • Change WizardSchemaNewFileCreationPage_2 to WizardSchemaNewFileCreationPage_Schema_File_Extension by clicking in the desired field and typing Schema_File_Extension
          • Change WizardSchemaNewFileCreationPage_3 to WizardSchemaNewFileCreationPage_Schema_Template_Location by clicking in the desired field and typing Schema_Template_Location
          • Click Configure in the Accessor class section at the bottom of the window.
          • Click Browse for Class Name and select NewWizardMessages; might as well put these strings with the others.
          • Click OK
          • Click Next and take a look at the various changes that are about to take place
          • Click Finish

          Do the same for the WizardDeploymentNewFileCreationPage only use the word Deployment instead of Schema.

          A question I have asked myself is: Should the WIZARD_NAME and PAGE_NAME also be externalized? While these posts have been examples of how to do things, with a mix of useful and useless things, the answer is…it depends. If i18n is important to the ultimate user of the plug-in then yes, externalize everything, otherwise don’t sweat it.

          [If I were being paid to do this I would fight tooth-and-nail to externalize every last string. It makes absolutely no sense to have to recompile one or more files just because a string has changed.]

          Refactor Code

          Both CustomProjectNewSchemaFile and CustomProjectNewDeploymentFile have the same constructor, performFinish() and init() methods. Let’s create a new parent class and move everything, but addPages() into it (it is the only method with custom code).

          The following will do the trick:

          1. Right click in the editor for CustomProjectNewSchemaFile and select Refactor –> Extract Superclass
            • Superclass Name: CustomProjectNewFile
          2. In Types to Extract a Superclass From click Add and add CustomProjectNewDeploymentFile
          3. In Specify Actions for Members check:
            • _pageOne
            • _workbench
            • _selection
            • performFinish()
            • init()
          4. Click Next
          5. In Subtypes of Type select init() and performFinish() to be removed from both subclasses. Notice that the constructors have not been recognized as having common behavior.
          6. Click Finish.

          Oddly enough, when the refactoring is over the superclass has a compile error! How could that be? Well, CustomProjectNewFile is missing the INewWizard interface that is needed to properly find init(). The refactoring missed it, but the compiler didn’t. Manually (yuck) add it to CustomProjectNewFile and remove it from CustomProjectNewSchemaFile and CustomProjectNewDeploymentFile (if you don’t remove it nothing will happen, but your code might develop cooties).

          Let’s change the constructors for CustomProjectNewSchemaFile and CustomProjectNewDeploymentFile. Change:

          CustomProjectNewSchemaFile.java
          
              public CustomProjectNewSchemaFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          

          to:

          CustomProjectNewSchemaFile.java
          
              public CustomProjectNewSchemaFile() {
                  super(WIZARD_NAME);
              }
          

          Change CustomProjectNewDeploymentFile from:

          CustomProjectNewDeploymentFile.java
          
              public CustomProjectNewDeploymentFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          

          to:

          CustomProjectNewDeploymentFile.java
          
              public CustomProjectNewDeploymentFile() {
                  super(WIZARD_NAME);
              }
          

          Use quick fix to create the new constructor in the CustomProjectNewFile parent class:

              public CustomProjectNewFile(String wizardName) {
                  setWindowTitle(wizardName);
              }
          

          Not too shabby.

          Run a quick test if you are so inclined.

          customplugin-part-10-custom-file-types-in-editor

          What just happened?

          Well, this was an interesting visit. We certainly coded more than usual for the customplugin, but it was all pretty trivial.

          We created two new wizards based on the built-in file creation page and create two templates so our custom files have a good starting point.

          The refactoring will make changing labels and such easier.

          The body count so far:
          customplugin: 11 classes, one properties file
          customnavigator: 13 classes

          Next post: back to the Custom Navigator, probably to display the Stored Procedure category with a Java file as an entry. The deployment file is not displayed, but will be found in the deployment-files folder.

          Unless I decide to go back to GP for a while.

          In celebration of this religious holiday go have some candy.

          (Anybody seen a cat?)

          References and Thanks

          How to create a new File Wizard? for reminding me about bundle functionality.

          Code

          CustomProjectNewFile.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          import org.eclipse.core.resources.IFile;
          import org.eclipse.jface.viewers.IStructuredSelection;
          import org.eclipse.jface.wizard.Wizard;
          import org.eclipse.ui.INewWizard;
          import org.eclipse.ui.IWorkbench;
          import org.eclipse.ui.PartInitException;
          import org.eclipse.ui.dialogs.WizardNewFileCreationPage;
          import org.eclipse.ui.ide.IDE;
          
          public abstract class CustomProjectNewFile extends Wizard implements INewWizard {
          
              protected WizardNewFileCreationPage _pageOne;
              private IWorkbench _workbench;
              protected IStructuredSelection _selection;
          
              public CustomProjectNewFile() {
                  super();
              }
          
              @Override
              public boolean performFinish() {
                  boolean result = false;
                  
                  IFile file = _pageOne.createNewFile();
                  result = file != null;
              
                  if (result) {
                      try {
                          IDE.openEditor(_workbench.getActiveWorkbenchWindow().getActivePage(), file);
                      } catch (PartInitException e) {
                          e.printStackTrace();
                      }
                  } // else no file created...result == false
                  
                  return result;
              }
          
              @Override
              public void init(IWorkbench workbench, IStructuredSelection selection) {
                  _workbench = workbench;
                  _selection = selection;
              }
          
          }
          
          

          CustomProjectNewSchemaFile.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          
          /**
           * @author carlos
           *
           */
          public class CustomProjectNewSchemaFile extends CustomProjectNewFile {
          
              private static final String WIZARD_NAME = "New Schema File"; //$NON-NLS-1$
              /**
               * 
               */
              public CustomProjectNewSchemaFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          
              @Override
              public void addPages() {
                  super.addPages();
          
                  _pageOne = new WizardSchemaNewFileCreationPage(_selection);
          
                  addPage(_pageOne);
              }
          }
          
          

          CustomProjectNewDeploymentFile.java

          /**
           * Coder beware: this code is not warranted to do anything.
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          
          /**
           * @author carlos
           */
          public class CustomProjectNewDeploymentFile extends CustomProjectNewFile {
              private static final String WIZARD_NAME = "New Deployment File"; //$NON-NLS-1$
          
              public CustomProjectNewDeploymentFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          
              @Override
              public void addPages() {
                  super.addPages();
          
                  _pageOne = new WizardDeploymentNewFileCreationPage(_selection);
          
                  addPage(_pageOne);
              }
          
          }
          

          WizardSchemaNewFileCreationPage.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          import java.io.IOException;
          import java.io.InputStream;
          
          import org.eclipse.jface.viewers.IStructuredSelection;
          import org.eclipse.ui.dialogs.WizardNewFileCreationPage;
          
          import customplugin.Activator;
          
          /**
           * @author carlos
           *
           */
          public class WizardSchemaNewFileCreationPage extends WizardNewFileCreationPage {
          
              private static final String PAGE_NAME = "Custom Plug-in Schema File Wizard"; //$NON-NLS-1$
          
              public WizardSchemaNewFileCreationPage(IStructuredSelection selection) {
                  super(PAGE_NAME, selection);
                  
                  setTitle(NewWizardMessages.WizardSchemaNewFileCreationPage_Schema_File_Wizard);
                  setDescription(NewWizardMessages.WizardSchemaNewFileCreationPage_Create_a_Schema_File);
                  setFileExtension(NewWizardMessages.WizardSchemaNewFileCreationPage_Schema_File_Extension);
              }
          
              @Override
              protected InputStream getInitialContents() {
                  String templateFilePath = NewWizardMessages.WizardSchemaNewFileCreationPage_Schema_Template_Location;
                  InputStream inputStream = null;
                  try {
                      inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
                  } catch (IOException e) {
                      // send back null
                  }
          
                  return inputStream;
              }
          
          }
          

          WizardDeploymentNewFileCreationPage.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          import java.io.IOException;
          import java.io.InputStream;
          
          import org.eclipse.jface.viewers.IStructuredSelection;
          import org.eclipse.ui.dialogs.WizardNewFileCreationPage;
          
          import customplugin.Activator;
          
          /**
           * @author carlos
           *
           */
          public class WizardDeploymentNewFileCreationPage extends WizardNewFileCreationPage {
          
              private static final String PAGE_NAME = "Custom Plug-in Deployment File Wizard"; //$NON-NLS-1$
          
              public WizardDeploymentNewFileCreationPage(IStructuredSelection selection) {
                  super(PAGE_NAME, selection);
                  
                  setTitle(NewWizardMessages.WizardDeploymentNewFileCreationPage_Deployment_File_Wizard);
                  setDescription(NewWizardMessages.WizardDeploymentNewFileCreationPage_Create_a_Deployment_File);
                  setFileExtension(NewWizardMessages.WizardDeploymentNewFileCreationPage_Deployment_File_Extension);
              }
          
              @Override
              protected InputStream getInitialContents() {
                  String templateFilePath = NewWizardMessages.WizardDeploymentNewFileCreationPage_Deployment_Template_Location;
                  InputStream inputStream = null;
                  try {
                      inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
                  } catch (IOException e) {
                      // send back null
                  }
          
                  return inputStream;
              }
          
          }
          

Writing an Eclipse Plug-in (Part 9): Custom Project: Defining a Custom File Type

October 25, 2009 Leave a comment

So things have been going swimmingly for the last 8 parts. Time to return to our roots: the original project.

But first: time to floss. Open customplugin MANIFEST.MF and click on the light bulb to fix the warning. It should now display:

MANIFEST.MF
...
Export-Package: customplugin,
 customplugin.natures,
 customplugin.perspectives,
 customplugin.projects,
 customplugin.wizards

Also, return to the Overview tab and click on the Externalize Strings Wizard. That should take care of externalizing 4 strings from the MANIFEST.MF and plugin.xml files. Click Finish.

There. My teeth feel so much better.

Okay, now on to the serious stuff.

The custom project will use 3 files types (for now):

  1. Schema definition
  2. Deployment descriptor
  3. Java source code

The two descriptor files will be XML files. They are what we need to define.

The current tasks are to:

  • Define a custom project file type for the Schema Definition.
  • Define a custom project file type for the Deployment Definition.

There are two things we need to know before defining the file type:

  • how can we determine the type of a file?
  • can we leverage existing Eclipse functionality?

The butler did it. Metaphorically anyway. There is no need to wait until the end of the story to be told what the answer is: Eclipse can associate the content type of an XML file by looking inside it and reading the root element.

By configuring the content type extension with a base-type of org.eclipse.core.runtime.xml and selecting a parser of type org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 we can create an extensive list of content types that are all XML files, but have different file formats. Very convenient. Very cool. That’s two verys.

Define a Custom Project File Type for the Schema Definition.

First:

  1. Open customplugin –> plugin.xml –> Extensions –> Add –> contenttypes
  2. Select org.eclipse.core.contenttype.contentTypes
  3. Click Finish
  4. Enter:
    • ID: customplugin.contenttype
  5. Select org.eclipse.core.contenttype.contentTypes –> new –> content-type
  6. Enter:
    • id: customplugin.contenttype.schema
    • name: Hidden Clause Schema Definition
    • base-type: org.eclipse.core.runtime.xml
    • file-extensions: xml
    • priority: normal
  7. Select Schema Definition –> new –> describer
  8. Enter:
    • class: org.eclipse.core.runtime.content.XMLRootElementContentDescriber2
  9. Select org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 –> new –> parameter
  10. Enter:
    • name: element
    • value: hc-schema

Start the runtime workbench and open Window –> Preferences –> General –> Content Types –> Text –> XML. Hidden Clause Schema Definition should be listed as an XML content type.

customplugin-part-9-preferences-with-new-contenttype

From the Resource Perspective create a project, custom or otherwise. Create a file and name it special-schema.xml. Enter the following into the file:

<hc-schema>
  <tables>
  </tables>
</hc-schema>

Right click on the file and select Properties –> Resource. The file should be listed as Hidden Clause Schema Definition (the name we gave it above).

customplugin-part-9-properties-file-hc-schema-def

Define a Custom Project File Type for the Deployment Definition.

Time for the second file type:

  1. Select org.eclipse.core.contenttype.contentTypes –> new –> content-type
  2. Enter:
    • id: customplugin.contenttype.deployment
    • name: Hidden Clause Deployment Definition
    • base-type: org.eclipse.core.runtime.xml
    • file-extensions: xml
    • priority: normal
  3. Select Schema Definition –> new –> describer
  4. Enter:
    • class: org.eclipse.core.runtime.content.XMLRootElementContentDescriber2
  5. Select org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 –> new –> parameter
  6. Enter:
    • name: element
    • value: hc-deployment

Start the runtime workbench and open Window –> Preferences –> General –> Content Types –> Text –> XML. Hidden Clause Deployment Definition should be listed as an XML content type.

customplugin-part-9-preferences-with-contenttype-deployment

From the Resource Perspective, using the same project as before create another file and name it special-dep.xml. Enter the following into the file:

<hc-deployment>
  <tables>
  </tables>
</hc-deployment>

Right click on the file and select Properties –> Resource. The file should be listed as Hidden Clause Deployment Definition.

customplugin-part-9-properties-file-hc-deploy-def

Take a bow.

Cleaning Up

Return to the Overview tab of plugin.xml and click on the Externalize Strings Wizard. Oh, look: the two custom file type names are there.

  • Assign the key content-type.name.schema to Hidden Clause Schema Definition
  • Assign the key content-type.name.deployment to Hidden Clause Deployment Definition
  • Click Finish

I think we might have missed a spot behind the ears, but they feel dry so I think we are okay.

What Just Happened?

Well, adding two new content types was pretty straightforward. Some would say that we should have done this directly in XML. Others think we should learn latin in school. Others think we should go back to the oceans.

Personally, I prefer to use editors to control configuration files; it keeps me from doing something complicated like…forgetting a closing brace, or misspelling an element name.

References

The Eclipse help files were somewhat useful, but not as useful as just trying it out. This is one of the few times where stumbling around in the room is actually useful.

Of course the Eclipse Tip: Define Custom Content Types to Identify Your Data Files was rather helpful in pointing out the use of org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 as an existing content type XML parser that also supports namespaces. Very cool. Very useful.

The cat is alive.

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.

Writing an Eclipse Plug-in (Part 7): Creating a Custom Navigator

October 18, 2009 30 comments

[October 23, 2009 – I updated some of the code to reflect suggestions made by Simon Zambrovski: the CustomProjectWorkbenchRoot no longer inherits from PlatformObject and, by extension, CustomNavigator.getInitialInput() now returns Object. The downloadable code also reflects the change.]

(This is going to be a long one.)

I know I keep harping on this, but I’m going to do it again: so far we have created:

  • a plug-in project that created a new project wizard to get the name and location of a new project
  • a plug-in project that creates a navigator to display the project information
  • a testable class that created the actual project and tests to go with it

Even though we have two plug-in projects we have only 7 classes, one of which just loads strings, 2 which are Activators and the others are mostly empty.

The custom navigator will change that…slightly…well, more than slightly.

Create a Category for the Custom Navigator

First task: before creating a true custom navigator let’s create a category for it. Why? Because the last time I did it second and this time I want to do it first. In fact, I expect to be doing such forward thinking a lot in the future. Especially when I get to think about things for about a week and sketch out all the things I want to do first. Like creating a custom navigator and then give it a category. Only configure the category first.

Okay, let’s go to the customnavigator plugin.xml file Extensions tab.

Create the category:

  1. Right click on org.eclipse.ui.views –> New –> Category
  2. Enter the following:
    • id: customnavigator.category
    • name: Custom Projects
  3. Save plugin.xml.

Notice the lack of code.

To bind the category to the custom navigator:

  1. Select Custom Plug-in Navigator (view).
  2. In the Category field to the right enter:
    • Category: customnavigator.category
  3. Save plugin.xml.

Of course, I can’t let this one go by without an icon so…create another 16×16 image and store it in the customnavigator icons folder (what? You don’t have an icons folder in the Plug-in project? Create one. After that create an image and store it in the icons folder. All I did was copy the perspective.png from the customproject plug-in and rename it navigator.png).

To add the icon go to the Extensions tab:

  1. Select Custom Plug-in Navigator (view).
  2. In the icon field to the right enter:
    • icon: icons/navigator.png
  3. Save plugin.xml.

Start the runtime workbench for the customnavigator plug-in. From the runtime workbench, starting from the main menu, open Window –> Show View –> Other –> Custom Projects. Look! The Custom Navigator! And it has a pretty icon!

Okay, exit the runtime workbench.

Did I mention that we didn’t have to write any code?

Refactor plugin.xml and MANIFEST.ML

Time for some plug-in project hygiene. The plugin.xml file has some strings that need to be externalized and the MANIFEST.ML file has both a string that need to be externalized and packages that should be declared as exported. Naughty, naughty (I know: we need to do the same for customplugin. Next time).

Luckily, this is easily solved.

if you scroll down In the Overview tab, you will find a link labeled Externalize Strings Wizard. Click on the link, accept everything, and click Finish at your earliest possible opportunity.

The Externalize Strings Wizard will create a file named OSGI-INF/I10n/bundle.properties. Seems like a long name for such a few strings, but it will serve us well as we add/subtract strings over time. If you prefer you could instead create a file named plugin.properties that contains the same properties. If you do that please make sure to add the plugin.properties file to the build.properties bin.includes property or else the plug-in will not know where to find the externalized strings.

To see the effect of the externalization close plugin.xml and reopen it. Viola! The perfect soufflé!

One last thing: if you go to the MANIFEST.ML tab you will notice that there is a warning icon in the top left hand corner at the top of the file. If you 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 line at the end of the file:

...
Export-Package: customnavigator

Save the MANIFEST.ML file to see the warning go away.

Alright, enough fooling around. Let’s get down to business.

Create a Custom Navigator

Creating a navigator using the Common Navigator Framework (CNF) is not hard (once you know what the hell you are doing), but it is not trivial either (someone once told me that using the word but in sentence basically negates everything said in the phrase before it. Perhaps using but is shorthand for damning with faint praise).

Here are the 6 7 8 6 5 steps to success (with lots of spackle in between so I’m not wrong by too often):

  1. Subclass CommonNavigator
  2. Create a root PlatformObject
  3. Add Extension navigatorContent and bind it to the navigator
  4. Implement the Custom Folder and File Types, ContentProvider and LabelProvider
  5. Add a ResourceListener

However, all of the above has to be understood in context of:

  • how the navigator behaves when it is first instantiated
  • how the navigator behaves after instantiation and the workspace changes

So the overlay to the 6 7 8 6 5 steps above is:

  • Implement the Common Navigator and its behavior upon instantiation
    1. Subclass CommonNavigator
    2. Create a root PlatformObject
    3. Add Extension navigatorContent and bind it to the navigator
    4. Implement the Custom Folder and File Types, ContentProvider and LabelProvider
  • Register a listener that will be called when the workspace changes and the view needs to be updated
    1. Add a ResourceListener

Subclass CommonNavigator

In Eclipse you have many ways to create a new class. For the purposes of this example, let’s define the CustomNavigator within plugin.xml and use its infrastructure to keep everything in sync.

  • Open plugin.xml –> Extensions –> Custom Plug-in Navigator (view)
  • Change the Class entry from the default CommonNavigator class to

Class: customnavigator.navigator.CustomNavigator
Just type it in. The class doesn’t exist yet.

  • Click on Class link to open the New Java Class Wizard.
  • Click Browse for Superclass and choose type CommonNavigator. Click OK.
  • Click Finish to close the New Java Class Wizard.

When the editor opens delete the CustomNavigator constructor and its comment.

Override getInitialInput() with the following code and return an object of a type we haven’t defined yet: CustomProjectWorkbenchRoot.

public class CustomNavigator extends CommonNavigator {
    @Override
    protected Object getInitialInput() {
        return new CustomProjectWorkbenchRoot();
    }
}

Damn, that was a lot of code.

Create a Root PlatformObject

Based on advice from Simon Zambrovski the CustomProjectWorkbenchRoot should not inherit from PlatformObject any more. Who am I to argue (if you like you can read up on at Simon’s site)?

Create the class CustomProjectWorkbenchRoot any way you like. I typically move the cursor to the offending line, press Ctrl+1, and select Create Class. I also created it in the same package as the CustomNavigator.

Leave CustomProjectWorkbenchRoot empty.

Run the runtime workbench if you like; open the Custom Navigator, create a Java project, create a file to go with it and the project and class file should appear in the Custom Navigator. Makes you kinda proud doesn’t it?

Of course, if all we wanted to do was create a clone of the Resource Navigator it would be easier to just go out for a walk until the feeling passed (yawn). The real goal is to have the Custom Navigator only display projects of type CustomProject (and a few other things, but one thing at a time).

Add Extension navigatorContent

Time to add a new Extension. This extension will define what content will appear in the navigator. Its name is, you guessed it, navigatorContent.

In plugin.xml go to Extensions and click Add. When the Extensions dialog opens type nav and select org.eclipse.ui.navigator.navigatorContent. Click Finish.

Right click on org.eclipse.ui.navigator.navigatorContent and select New –> navigatorContent. Enter the following:

  • id: customnavigator.navigatorContent
  • name: Custom Navigator Content
  • contentProvider: customnavigator.navigator.ContentProvider
  • labelProvider: customnavigator.navigator.LabelProvider

If the goal is to only display projects of type CustomProject then we need content/label providers that only recognize those resource types. Time to write some more code.

Click on the contentProvider link to open the New Java Class wizard. We like what we see. Click Finish.

Do the same for labelProvider.

Display Only Custom Nature Projects

This is where we perform a lobotomy, or more accurately an extension-ectomy, to the configuration of the Custom Navigator to get it to display projects of type Custom Project.

Let’s clean up the non-essential extensions:

  • Open plugin.xml –> Extensions
  • Open org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerContentBinding) –> (includes)
  • Delete all of the nodes under (includes) (but NOT (includes))
  • Save plugin.xml

Now add the extension that binds the navigatorContent to the navigator:

  • Go back to org.eclipse.ui.navigator.viewer –> customnavigator.navigator (viewerContentBinding) –> (includes)
  • Right click on (includes) and select New –> contentExtension
  • Enter the id of the Custom Navigator Content extension in the pattern field:
    • pattern: customnavigator.navigatorContent

To see a Custom Project in the Custom Navigator we need to add the triggerPoints that tell CNF which resources we care about (there is more to it than that, but that explanation will have to do):

  • Open org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content (navigatorContent)
  • Select Custom Navigator Content (navigatorContent) –> New –> triggerPoints
  • Select triggerPoints –> New –> or
  • Select or –> New –> instanceOf
  • In the value field click Browse and select customnavigator.navigator.CustomProjectWorkbenchRoot.

We’ve actually done quite a bit. If we were to add print statements in the ContentProvider and LabelProvider classes, start the runtime workbench, and create a Custom Project you would see output in the Console view. Very cool, but still not quite what we are looking for especially since the Custom Navigator does not know how to update itself when new resources are created.

Implement the Custom Folder and File Types, ContentProvider and LabelProvider

(Yeah, long title because a whole bunch of things have to be done first before we can turn on the switch and see the navigator returning custom content.)

Now that our ContentProvider and LabelProvider are being called what should they return? If we only want to display CustomProjects then the ContentProvider needs to return only CustomProject information and the LabelProvider has to know how to talk to our CustomProject objects to return the proper label information.

Let’s look at the ContentProvider first. We don’t want it talking to an object that is not a Custom Project (I hate double negatives) so we have to put an isInstance() check for objects of type CustomProjectWorkbenchRoot (we’ll add CustomProjectParent later). Why CustomProjectWorkbenchRoot? The call to getElements()/getChildren() will return the CustomProjectParent objects that will be queried for their text and displayable image.

Once the ContentProvider is sure that an object of the proper type is available it can do the following:
– if this is the first time in retrieve all the projects from the workbench (hence the call to initializeParent() if _customProjectParents is null)
– wrap each Custom Project in a CustomProjectParent object

From ContentProvider.java

public class ContentProvider implements ITreeContentProvider {

    private static final Object[] NO_CHILDREN = {};
    private CustomProjectParent[] _customProjectParents;

    @Override
    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (CustomProjectWorkbenchRoot.class.isInstance(parentElement)) {
            if (_customProjectParents == null) {
                _customProjectParents = initializeParent(parentElement);
            }

            children = _customProjectParents;
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    ...

    private CustomProjectParent[] initializeParent(Object parentElement) {
        IProject [] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
        CustomProjectParent[] result = new CustomProjectParent[projects.length];
        for (int i = 0; i < projects.length; i++) {
            result[i] = new CustomProjectParent(projects[i]);
        }

        return result;
    }
}

In initializeParent() we ask the workspace for its current contents and we gratuitously wrap all the IProjects in a CustomProjectParent.

We have the veritable chicken and egg problem: without knowing what the parent should do how do we implement it? Using the YAGNI rule we create it and leave it empty…until the next complaint: the constructor to take in the IProject is missing. So, add that and store the IProject reference. We’ll need it later.

public class CustomProjectParent {

    private IProject _project;

    public CustomProjectParent(IProject iProject) {
        _project = iProject;
    }

    public String getProjectName() {
        return _project.getName();
    }
}

The LabelProvider needs to:
– make sure the incoming object is a CustomProjectParent and if it is:
– get the name of the project and return it
– get the image associated with CustomProjects and return it

Both of these tasks are going to be delegated to the CustomProjectParent otherwise the LabelProvider is going to be a tangle of wires as we add more and more types to the navigator tree.

LabelProvider.java

public class LabelProvider implements ILabelProvider {
    ...

    @Override
    public String getText(Object element) {
        String text = "";
        if (CustomProjectParent.class.isInstance(element)) {
            text = ((CustomProjectParent)element).getProjectName();
        }

        return text;
    }

    ...
}

The above code is going to change, but it is always instructional to see the evolution of the code.

Run a test:
– Start the runtime workbench
– Close the Welcome window
– Create a Java project
– Create a Custom Project
– When the Open Associated Perspective dialog opens click No
– Open Window –> Show View –> Other –> Custom Projects –> Custom Plug-in Navigator
Both projects will appear in the Custom Navigator as labels but no images.

Next task: exclude all the projects that are not members of our club. We do that in he ContentProvider.

ContentProvider.java

    private CustomProjectParent[] initializeParent(Object parentElement) {
        IProject [] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();

        List<CustomProjectParent> list = new Vector<CustomProjectParent>();
        for (int i = 0; i < projects.length; i++) {
            try {
                if (projects[i].getNature(ProjectNature.NATURE_ID) != null) {
                    list.add(new CustomProjectParent(projects[i]));
                }
            } catch (CoreException e) {
                // Go to the next IProject
            }
        }

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

        return result;
    }

Oh, oh! We are out of sync with ourselves. The navigator plug-in is referencing customplugin.natures.ProjectNature which is not on its classpath or dependency list. In the customnavigator plug-in:

  • plugin.xml –> Dependencies –> Add
  • Select A Plug-in: customplugin
  • Click OK
  • Save plugin.xml

The LabelProvider should compile cleanly.

Run the (decidely) manual test again and now the Custom Project will be the only project displayed in the Custom Navigator.

[This needs a packaged test. In my haste to present how to configure the navigator I skipped a standard-practice step. Rather than interrupt the flow, which I am doing right now, I will present the steps to run the first series of tests on the navigator in the next post.]

The LabelProvider needs one more thing: an image to display along with the label. Copy the project-folder.png from customplugin or whatever image you decided to use in customplugin. We will take advantage of Eclipse’s property handling capabilities to centralize this functionality (besides, other plug-ins do the same thing. Who am I to rock the boat?).

Add this to CustomProjectParent:

CustomProjectParent.java

    private Image _image;

    ...

    public Image getImage() {
        if (_image == null) {
            _image = Activator.getImage("icons/project-folder.png"); //$NON-NLS-1$
        }

        return _image;
    }

[Hey! I just noticed that the folder to hold the icons is called icon instead of icons. Select it in the Package Explorer, press F2 and rename icon to icons.]

No, Activator.getImage() doesn’t exist. Press Ctrl+1 to auto-create it in the Activator class. Put in the code below.

Activator.java

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

        return image;
    }

Yes, AbstractUIPlugin will take care of loading the image for us. Sweet.

Back to LabelProvider:

LabelProvider.java

    public Image getImage(Object element) {
        System.out.println("LabelProvider.getImage: " + element.getClass().getName());
        Image image = null;

        if (CustomProjectParent.class.isInstance(element)) {
            image = ((CustomProjectParent)element).getImage();
        }
        // else ignore the element

        return image;
    }

Run the manual test again. The screen capture below is a little different than you are probably seeing from the comfort of your own home, but shows two projects in the Project Explorer vs one in the Custom Plug-in Navigator.

custom-navigator-with-custom-project

Display a List of Categories per Project

A CustomProject displayed in the CustomNavigator will have two categories one of which has 3 sub-categories:

  • Schema
    • tables
    • views
    • filters
  • Stored Procedures

Tables, views and filters will list individual entries and Stored Procedures will list individual stored procs.

As usual the categories are bogus, but give us something to work with rather than parent1 and parent2.

This next list is going be somewhat daunting. These items need to be implemented/modified for the Custom Navigator to be of any value:

  • ContentProvider.getChildren() – return the children of CustomProjectParents
  • ContentProvider.getParent() – return the parent of CustomProjectParents and their children
  • ContentProvider.hasChildren() – return true if the incoming element has children. CustomProjectWorkbenchRoot will return true if any CustomProjects exist, CustomProjectParents will always return true since they will have Schema and Stored Proc children, Schema will return true as it has 3 children, Stored Procedures will return true if it has any entries.
  • LabelProvider.getImage() – check the object type to retrieve the proper image
  • LabelProvider.getText() – check the object type to retrieve the proper text
  • CustomProjectParent.getChildren() – will always return children (Schema and Stored Procedures)
  • CustomProjectSchema – implement getText(), getImage(), getParent(), getChildren() and a constructor that instantiates the Tables, Views and Filters children using the contents of an IProject
  • CustomProjectStoredProcedures – implement getText(), getImage(), getParent(), getChildren() and a constructor that will create children to represent file entries
  • CustomProjectSchemaTables – Display tables from an XML file
  • CustomProjectSchemaViews – Display views from an XML file
  • CustomProjectSchemaFilters – Display filters from an XML file
  • ICustomProjectElement – an interface used by all of the parents and children (except CustomProjectWorkbenchRoot) to make sure they all define getText() and get Image(). This should make the logic in LabelProvider.get[Text|Image]() a little simpler

(When I said this post was going to be long I wasn’t kidding.)

Due to the length of this post you can download the code up to this point instead of cutting-and-pasting until your fingers start to bleed.

In any case, let’s see what it will take to do a sampling of the above.

ContentProvider.getChildren()

Modifying ContentProvider.getChildren() to support CustomProjectParent means changing:

  • ContentProvider.getChildren()
  • CustomProjectParent.getChildren()
  • LabelProvider.getText()
  • LabelProvider.getImage()

and creating:

  • CustomProjectSchema.getChildren()
  • CustomProjectSchemaTables.getChildren()
  • CustomProjectSchemaViews.getChildren()
  • CustomProjectSchemaFilters.getChildren()
  • CustomProjectStoredProcedures.getChildren()

The above entailed passing in the parent reference through the constructor, creating children if appropriate and using the parent reference to find the reference to the IProject. Having the IProject reference means we can work out the leaf nodes of the categories created above.

I created a common interface, ICustomProjectElement, to the various parent and children to make their use in ContentProvider and LabelProvider simpler.

The interface did not come to me full blown. I grew it as I wrote the code for the children and it became obvious which methods were needed.

public interface ICustomProjectElement {

    public Image getImage();

    public Object[] getChildren();

    public String getText();

    public boolean hasChildren();

    public IProject getProject();

    public Object getParent();
}

In ContentProvider I changed getChildren() to reflect the use of ICustomProjectElement:

    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (CustomProjectWorkbenchRoot.class.isInstance(parentElement)) {
            if (_customProjectParents == null) {
                _customProjectParents = initializeParent(parentElement);
            }

            children = _customProjectParents;
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

LabelProvider gained from the use of the interface as well:

    public String getText(Object element) {
        String text = ""; //$NON-NLS-1$
        if (ICustomProjectElement.class.isInstance(element)) {
            text = ((ICustomProjectElement)element).getText();
        }
        // else ignore the element

        return text;
    }

Since the CustomProjectParent category will always have 2 children the code is also reasonable straightforward:

public class CustomProjectParent implements ICustomProjectElement {

    ...

    public ICustomProjectElement[] getChildren() {
        if (_children == null) {
            _children = initializeChildren(_project);
        }
        // else we have already initialized them

        return _children;
    }

    ...

    private ICustomProjectElement[] initializeChildren(IProject project) {
        ICustomProjectElement[] children = {
                new CustomProjectSchema(this),
                new CustomProjectStoredProcedures(this)
        };

        return children;
    }
}

Starting the runtime workbench and creating a custom project gives us a strong beginning to giving our folder-centric project a category-based view.

custom-navigator-end-of-part-7

All of the child nodes, CustomProjectSchema, CustomProjectStoredProcedures, CustomProjectSchemaTables, etc., will have code that looks suspiciously similar. It will all be refactored in the post after the navigator is mostly done. Something I learned in my years as a consultant/instructor/mentor is that you can’t test your way to learning. We’re still learning here. The thing to bear in mind is that until you know what you don’t know, you don’t know. Worry about duplicate/sloppy code at refactoring time.

[This is where the explanation for how to take an existing project structure, automatically categorize files located in arbitrary folders, and display elements from one of the XML files as information under one of the categories. As this post has gone on longer than I had planned, and I expected it to go pretty long, I will complete those last two steps in the next post. Rest easy; the hard part is over…I think.]

Rewind

So what just happened?

  1. I managed to clean up the look of the custom wizard and custom perspective by adding icon images to them.
  2. After much twisting and shouting the Custom Navigator displays only Custom Projects and the CustomProject is displayed in a way that will eventually categorizes things within the project in a more intuitive way.

I am sure you are walking away feeling like you don’t know much more about how to program/configure the CNF than you did when you started. That’s okay. There are plenty of other sources for that sort of boring information. Depth of knowledge is overrated…until it’s not. Then you’re screwed.

Download the code up to this point. Let me know if it helps.

(BTW, I cheated like there was no tomorrow. I had print statements, books, web pages…anything I thought would give me some insight into how the CNF works. This is not an easy framework. But it is cool!)

The cat is alive, but barely.

Links

Resources

http://www.eclipse.org/articles/Article-TreeViewer/TreeViewerArticle.htm
http://www.techjava.de/topics/2009/04/eclipse-common-navigator-framework/

Icons

filter – http://www.iconspedia.com/icon/document-2454.html
binoculars – http://www.clker.com/clipart-2908.html
database – http://www.clker.com/clipart-14651.html
schema – http://www.clker.com/clipart-1780.html
tables – http://www.clker.com/clipart-3493.html

Code

CustomNavigator.java

/**
 * Coder beware: this code is not warranted to do anything.
 * Copyright Oct 16, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

import org.eclipse.ui.navigator.CommonNavigator;

/**
 * @author carlos
 */
public class CustomNavigator extends CommonNavigator {
    @Override
    protected Object getInitialInput() {
        return new CustomProjectWorkbenchRoot();
    }
}

ContentProvider.java

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

import java.util.List;
import java.util.Vector;

import org.eclipse.core.resources.IProject;
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
 */
public class ContentProvider implements ITreeContentProvider {

    private static final Object[]   NO_CHILDREN = {};
    private ICustomProjectElement[] _customProjectParents;

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.
     * Object)
     */
    @Override
    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (CustomProjectWorkbenchRoot.class.isInstance(parentElement)) {
            if (_customProjectParents == null) {
                _customProjectParents = initializeParent(parentElement);
            }

            children = _customProjectParents;
        } 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 (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        }
        return parent;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.
     * Object)
     */
    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (CustomProjectWorkbenchRoot.class.isInstance(element)) {
            hasChildren = _customProjectParents.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
        return getChildren(inputElement);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
     */
    @Override
    public void dispose() {
        System.out.println("ContentProvider.dispose"); //$NON-NLS-1$
        // TODO Auto-generated method stub

    }

    /*
     * (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) {
        System.out
                .println("ContentProvider.inputChanged: old: " + oldInput.getClass().getName() + " new: " + newInput.getClass().getName()); //$NON-NLS-1$ //$NON-NLS-2$
        // TODO Auto-generated method stub

    }

    private ICustomProjectElement[] initializeParent(Object parentElement) {
        IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();

        List<CustomProjectParent> list = new Vector<CustomProjectParent>();
        for (int i = 0; i < projects.length; i++) {
            try {
                if (projects[i].getNature(ProjectNature.NATURE_ID) != null) {
                    list.add(new CustomProjectParent(projects[i]));
                }
            } catch (CoreException e) {
                // Go to the next IProject
            }
        }

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

        return result;
    }

}

LabelProvider.java

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

import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.swt.graphics.Image;

/**
 * @author carlos
 *
 */
public class LabelProvider implements ILabelProvider {

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object)
     */
    @Override
    public Image getImage(Object element) {
        Image image = null;
        
        if (ICustomProjectElement.class.isInstance(element)) {
            image = ((ICustomProjectElement)element).getImage();
        }
        // else ignore the element
        
        return image;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object)
     */
    @Override
    public String getText(Object element) {
        String text = ""; //$NON-NLS-1$
        if (ICustomProjectElement.class.isInstance(element)) {
            text = ((ICustomProjectElement)element).getText();
        }
        // else ignore the element
        
        return text;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener)
     */
    @Override
    public void addListener(ILabelProviderListener listener) {
        System.out.println("LabelProvider.addListener: " + listener.getClass().getName()); //$NON-NLS-1$
        // TODO Auto-generated method stub

    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
     */
    @Override
    public void dispose() {
        System.out.println("LabelProvider.dispose"); //$NON-NLS-1$
        // TODO Auto-generated method stub

    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String)
     */
    @Override
    public boolean isLabelProperty(Object element, String property) {
        System.out.println("LabelProvider.isLabelProperty: " + element.getClass().getName()); //$NON-NLS-1$
        // TODO Auto-generated method stub
        return false;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener)
     */
    @Override
    public void removeListener(ILabelProviderListener listener) {
        System.out.println("LabelProvider.removeListener: " + listener.getClass().getName()); //$NON-NLS-1$
        // TODO Auto-generated method stub

    }

}

Writing an Eclipse Plug-in (Part 5): Adding Icons and A New Project Structure

October 11, 2009 7 comments

It has been a while since I lasted posted on the implementation of a custom Eclipse project. Well, I’m back.

In the last posts we created:

  • a plug-in project that created a new project wizard to get the name and location of a new project
  • a navigator to display the project information
  • a testable class that created the actual project and tests to go with it

In addition we configured the new wizard to open a custom perspective and added the code to make the automatic opening of the project related perspective open automatically.

Not bad for 5 mostly empty classes, 1 class that actually does something, 1 test class and a configuration file.

Since the last post I have reconsidered including the custom navigator in the original plug-in so in this post we will remove it from the commonplugin plug-in and add a separate plug-in for the navigator instead. I hope doing that will make the navigator easier to maintain.

Today’s task list:

  1. Add icons to the Custom Project and Custom Perspective.
  2. Create a default folder structure at project creation.
  3. Create a new plug-in and add a custom navigator to it.
  4. Remove the custom navigator from the commonplugin.
  5. Integrate the custom navigator with the original plug-in (at this point we will be back to where we started. Woo hoo).
  6. Add an icon when a Custom Project is created.
  7. The custom navigator will display only projects of the proper nature with a list of categories that will eventually contain files taken from the physical folder structure.

Sounds like a lot for one blog. I will probably break this up into 2 parts.

Add Custom Icons to the Project and Perspective

Have I mentioned how important I think standards are (except when they are not)? Well, if you are doing any kind of plug-in development on Eclipse you should read the User Interface Guidelines for Eclipse which spell out in excruciating detail the things you should be doing to adhere to the look-and-feel of the really really cool platform.

In my case I care that the icons that appear next to perspectives, projects, folders and files are all supposed to be 16×16 pixels. Here are the two I used for this installment of this edge-of-your-seat blog (displayed as 32×32).

perspective project-folder

There is nothing very fancy about them. I had considered using Underdog and company as the icons, but I changed my mind (do you know how hard it is to find a decent icon of Riff Raff? Way harder than you think).

Giving our Custom Project Wizard an icon means:

  1. Go to the plugin.xml Extensions tab
  2. Open the org.eclipse.ui.newWizards extension.
  3. Select Custom Project (wizard) on the right and to the left click the Browse button on the same line as icon.
  4. Select your picture file.
  5. Save plugin.xml.

You should see your icon appear next to the Custom Project (wizard) extension entry.

menu-editor-new-wizard-with-icon

Giving our Custom Perspective an icon means:

  1. Open the org.eclipse.ui.perspectives extension.
  2. Select Custom Plug-in Perspective (perspective) on the right and to the left click the Browse button on the same line as icon.
  3. Select your picture file.
  4. Save plugin.xml.

You should see your icon appear next to the Custom Plug-in Perspective (perspective) extension entry.

menu-editor-perspective-with-icon

Quick test:

  1. Start the runtime workbench.
  2. Select Window –> Open Perspective –> Other
  3. The icon should appear next to the Custom Plug-in Perspective entry in the Open Perspective dialog.
  4. open-perspective-custom-plug-in-icon

  5. Click Cancel. First test passed.
  6. Press Ctrl+N.
  7. Open the Custom Wizards foilder of the New dialog.
  8. The icon should appear next to the Custom Project entry.
  9. new-dialog-custom-project-icon

  10. Click Cancel and exit the runtime workbench. The second test passed.

One down. Dozens to go.

Create a Default Folder Structure at Project Creation

There was a test of the folder structure located in CustomProjectSupportTest that looked for a rather bogus project structure that looked something like:
parent
  |- child1-1
    |- child2
  |- child1-2
    |- child2
    |- child3

The new structure is going to use the project folder as its root (certainly inspired) and will create three child folders: one to store a schema file, one to store deployment XML files and the other to store some Java files. This, of course, is just pretend as I have no hidden custom project type I am creating this for. In other words, this structure is no less bogus than the previous one. The names are just recognizable; in other words, easier for me.

project-folder
|- schema
|- deployment-files
|- clause
  |- java
    |- source
      |- hidden-clause

The schema folder will contain an XML file. This represents one category.

The deployment-files and clause folders are related. They represent another category. The deployment-files folder will contain an XML file. The clause/java/source folder will contain Java code.

When we get around to creating a custom navigator the two categories will be displayed and the contents of the related folders will be properly shown.

The original test code in CustomProjectSupportTest looked like this:

    private void assertFolderStructureIn(String projectPath) {
        String[] paths = { "parent/child1-1/child2", "parent/child1-2/child2/child3" };
        for (String path : paths) {
            File file = new File(projectPath + "/" + path);
            if (!file.exists()) {
                Assert.fail("Folder structure " + path + " does not exist.");
            }
        }
    }

Change the path assignment to:

    private void assertFolderStructureIn(String projectPath) {
        String[] paths = {
                    "schema", //$NON-NLS-1$
                    "deployment-files/java", //$NON-NLS-1$
                    "clause/java/source/hidden-clause"}; //$NON-NLS-1$
...
    }

When you run the test it should fail. Update CustomProjectSupport.createProject() with the new paths:

    public static IProject createProject(String projectName, URI location) {
        Assert.isNotNull(projectName);
        Assert.isTrue(projectName.trim().length() > 0);

        IProject project = createBaseProject(projectName, location);
        try {
            addNature(project);

            String[] paths = {
                    "schema", //$NON-NLS-1$
                    "deployment-files/java", //$NON-NLS-1$
                    "clause/java/source/hidden-clause"}; //$NON-NLS-1$
            addToProjectStructure(project, paths);
        } catch (CoreException e) {
            e.printStackTrace();
            project = null;
        }

        return project;
    }

Run the test and it should pass.

As a more complete test, execute the runtime workbench, create a project, open the regular Navigator and check out the folder structure. Life is good…at least for this step.

Create the Custom Navigator Plug-in

Let’s create a new plug-in just for the Custom Navigator.

  1. Press Ctrl+N and select Plug-in Project
  2. Enter:
    • Project Name: customnavigator
    • Eclipse version: 3.4
  3. Click Next
    • Name: Custom Navigator Plug-in
    • Activator: customnavigator.Activator
  4. Click Finish.

In plugin.xml:
Overview tab:
Name: Custom Navigator Plug-in

Extensions Tab (this is actually cut-and-pasted from Part 2 with minor revisions):

  1. Click Add, click the Extension Wizards tab and select the Common Navigator View template.
  2. Click Next
  3. Enter the following:
    • View Id: customnavigator.navigator
    • View Name: Custom Plug-in Navigator
    • Uncheck Add to Resource Perspective
  4. Click Finish.
  5. If a dialog opens requesting to save changes click Yes. Two extensions have been added as well as three dependencies.

Just for yucks, execute the runtime workbench. Go to Window –> Show View –> Other –> Other –> Custom Navigator. The Custom Navigator should be listed with a large red square next to it (very catchy icon on the same level as a pointy stick to the eye).

A Metaphorical 90 Degree Turn

When you try to run the custom navigator plug-in you may find that it doesn’t run as cleanly as you would like. At least, I found that various things would be visible that I did not expect.

So, from the customnavigator plugin.xml Overview tab, after you click on Launch An Eclipse Application and kill the runtime workbench, open the Run Configurations dialog (Run –> Run Configurations or any of about 8 other ways). Make sure the following are set in their respective tabs:

  • Main
    • Location: ${workspace_loc}/../runtime-customnavigator
    • [check] Clear: workspace
  • Plug-ins
    • Launch with: plug-ins selected below only [click Deselect All, check customnavigator, and click Add Required Plug-ins]
    • Check Include optional dependencies when computing required plug-ins
    • Uncheck Add new workspace plug-ins to this launch configuration automatically
    • Check Validate plug-ins automatically prior to launching
  • Configuration
    • Check Use Default Location
    • Check Clear the configuration area before launching

The above should keep the runtime workbench working in an isolated way so anything we do is not affected by things that shouldn’t be there.

We now metaphorically turn back 90 degrees.

Amazing how simple some of this can be…well, until we actually try to make it do something. Another few steps and we shall see what we shall see.

Remove the Navigator from the commonplugin

Time to perform a navigatorectomy.

From the customplugin plug-in (not from customnavigator!) remove:

  • org.eclipse.ui.views
  • org.eclipse.ui.navigator.viewer

The easiest way to do the above is to right click on the extension and select Delete. Save customplugin plugin.xml when you are done.

Run the runtime workbench for the customplugin plug-in. Create a Custom Project. Everything should be the same except that the Custom Navigator will not appear when the Custom Plug-in Perspective opens.

We’ll take care of that next. Close the runtime workbench.

Integrate the Navigator with commonplugin

In the customplugin plugin.xml file’s Extensions tab select org.eclipse.ui.perspectiveExtensions --> customplugin.perspective (perspectiveExtension) --> customplugin.navigator (view). In the ID field enter the name of the new navigator: customnavigator.navigator. Save the commonplugin plugin.xml file.

Open the Run Configurations dialog for customplugin and go to the Plug-ins tab. Check customnavigator and click Run. From the runtime workbench create a new Custom Project, say Yes to opening the custom perspective and the Custom Navigator should appear where it was before displaying the new Custom Project.

We are now back to where we started. Woo hoo.

What did we do this time around?

  1. Added icons to the Custom Project and Custom Perspective.
  2. Created a default folder structure at project creation.
  3. Created a new plug-in and add a custom navigator to it.
  4. Removed the custom navigator from the commonplugin.
  5. Integrated the custom navigator with the original plug-in.

Not bad for an old guy.

Next time I will show you how to:

  1. Add an icon when a Custom Project is created.
  2. Display in the Custom Navigator only projects of the proper nature with a list of categories that will eventually contain files taken from the physical folder structure.

I will check out the cat’s health next time.