Eclipse Search

Loading

Oct 31, 2008

A glimpse at the Faceted Project Framework

If you haven't noticed, the Faceted Project Framework from WTP is now proposed as a separate project. I like the framework very much, probably because my very first Eclipse Plug-in is simply a WTP Facet :-) When I wrote that plug-in, there was no much documentation available, but later Konstantin wrote a nice article about it. It might get into platform for the e4 timeframe, so lets see what's in the box.
Before 'what' and 'how' the question of 'why'. Stealing Konstantin's thoughts on this question from the newsgroup:
Natures are extremely simple. The original use case for natures is to basically serve as a flag modeling something akin to a project type. The good UI that you speak of for adding and removing natures would need a lot more information and callable behaviors than is provided by the natures API. The limitations make sense since the natures API was written with simple use cases in mind and natures were not intended to be fully-described and self-supporting.

Let's look at one API as an example. The IProjectNature interface has configure() and deconfigure() methods that nature authors get to implement. These methods get called when a nature is added to or removed from a project. Notice how those methods take no parameters. That means it is not possible to pass in a configuration object that would let the user have input as these actions take place. So what, you say. We've solved that problem before. We will just create IProjectNature2 with variants of those two methods capable of taking parameters. Problem solved. Not quite. The issue is that there are many natures already implemented in various Eclipse projects and commercial products. For years nature authors have been working around this API limitation. The most common work-around is to move most of the interesting code outside these two methods. In the case of the configure() method all of this code is embedded in various project creation wizards and "Enable function X" menu actions. The configure() implementations themselves typically are not sufficient to properly configure the project and often rely on stuff happening prior to the nature getting added. For the most part, the deconfigure() methods are implemented as no-ops. Since there is no UI for the user to remove a nature, why bother with implementing this method?

In short, the biggest obstacle to making it possible for users to add and remove natures are the existing nature implementations. Practically every nature out there would be broken behaviorally if UI for adding and removing natures was introduced.

Ok. So we need a more gradual phased-in approach that doesn't give everyone in the Eclipse Community a massive coronary. Enter facets. We started from scratch and designed the facets API together with UI for adding and removing them. The framework evolved over the years to cover a lot of different use cases that adopters brought to the table. Since making a project faceted is controlled by a flag (a nature, actually), the project wizard owner get to opt-in when they are ready. Part of the opt-in process is gathering all of the code that strewn about in various wizards and actions and writing a facet based on that code. It often make sense to retain any existing natures for backwards compatibility, but facets would now control adding and removing them. The pattern of wrapping a facet around a nature is particularly important when you consider the case of an Eclipse Project re-using another Project's functionality. We need a solution for enabling a downstream Project to transition to facets while the upstream Project is not ready yet and continues using natures. The original example of this is WTP's re-use of JDT's java nature for various WTP projects like Dynamic Web Project. It was necessary for WTP to move forward with creating faceted projects without depending on JDT adopting the same approach and making requisite changes to it's code base. The facets architecture allowed us in WTP to implement a java facet on our own that wraps JDT's java nature. JDT's Java Project (as well as all the other projects that re-use java nature) continue to work as before while WTP users start benefiting from the richer experience provided by facets. If JDT ever decided to adopt facets, the implementation of the java facet can move to JDT. For now, it will live in the JDT Enablement component of this Project. In short, the approach taken is to enable gradual and controlled adoption without asking everyone in the community to jump at the same time. :)

Ok. So how do we create a Facet?
Facets are declared thru the extension point: org.eclipse.wst.common.project.facet.core.facets. You can categorize a facet; a facet is identified by its id + version; and it can depend on other facets. Each version of the facet can be associated with its actions like install or uninstall or version change. Say if you want to install Java facet, typically the install action would add the Java nature, Java builder, set up the classpath. The install action for a Plugin Facet would probably involve adding the manifest.mf & plugin.xml files, adding the required libraries etc. So pretty much all the activities can be handled by the actions. If you want to get the plug-in id, name, etc from the user, you can extend the wizardPages extension and contribute to the wizard. All these are nicely explained in the article, so you have to refer there for details. Remember, the facets doesn't depend on any other component of WTP, so all you need is org.eclipse.wst.common.project.facet.core & org.eclipse.wst.common.project.facet.ui in your dependencies !
For this tip, I've created two facets and one preset:
Java Facet:
   1: <project-facet id="java.facet">
   2:    <default-version version="1.0"/>
   3:    <label>Java Facet</label>
   4: </project-facet>
   5:  
   6: <project-facet-version facet="java.facet" version="1.0">
   7:    <action type="install">
   8:       <delegate class="com.eclipse_tips.facets.InstallJavaDelegate"/>
   9:    </action>
  10: </project-facet-version>
Plugin Facet:


   1: <project-facet id="plugin.facet">
   2:    <default-version version="1.0"/>
   3:    <label>Plug-in Facet</label>
   4: </project-facet>
   5: <project-facet-version facet="plugin.facet" version="1.0">
   6:    <action type="install">
   7:       <delegate class="com.eclipse_tips.facets.InstallPluginDelegate"/>
   8:    </action>
   9: </project-facet-version>
Preset:


   1: <static-preset id="com.eclipse-tips.facets.pluginProject">
   2:    <description>
   3:       This will create a project with both java and plugin facet
   4:    </description>
   5:    <facet id="java.facet" version="1.0" />
   6:    <facet id="plugin.facet" version="1.0" />
   7:    <label>
   8:       Default PDE Project
   9:    </label>
  10: </static-preset>
To create a Faceted Project, you can use the New Faceted Project Wizard:

image


 You can choose from the available list of facets and their versions. If you choose an invalid combination, (say both Plug-in Facet and a Feature Facet), you should get an error (assuming the facets declare the conflict in the extension point. The preset will be available in the Configurations, so that you can do a one click selection of the facets you would need.

In case you wish to change the facets, the same wizard UI is available in the Project->Properties->Project Facets. So you can change your Plug-in project into a Feature project or add C++ capabilities to your Java project easily. All the necessary files will be added and classpath will be set for you.

How to programmatically create a Faceted Project?

If you want to manipulate the facets on a project in the code, the ProjectFacetsManager is the key. You can create IFacetedProject thru ProjectFacetsManager.create(*) methods and obtain a working copy. You can do all the operations like add/remove a facet, change the version of the facet etc, and then finally you can do a commit on the working copy to persist the changes. Yup, you can revert the changes as well, if you didn't want to persist them. Here is the sample code, which takes a normal project; converts to a Faceted Project; adds the Java Facet; and commits the changes:



   1: IFacetedProject facetedProject = ProjectFacetsManager.create(project, true, null);
   2: IFacetedProjectWorkingCopy workingCopy = facetedProject.createWorkingCopy();
   3:  
   4: IProjectFacet javaFacet = ProjectFacetsManager.getProjectFacet("java.facet");
   5: IProjectFacetVersion defaultJavaFacet = javaFacet.getDefaultVersion();
   6: workingCopy.addProjectFacet(defaultJavaFacet);
   7: workingCopy.commitChanges(null);
With the ProjectFacetsManager, you can pretty much do everything, except for defining new facets. If you really need that, probably you should raise a bug. Might get addressed for e4 :-)

Oct 21, 2008

Multi instance Property View - a first look

Currently the workbench supports only one instance of the PropertyView. So if you ever wanted to compare the properties of two different elements, you have to open two windows and select each element in a different window. Yes, this is ugly and becomes more uglier when you wanted to compare three or more elements. Thanks to Markus, Eclipse 3.5 (Galileo) will host a multi-instance Property View. The code is not even checked into the CVS, so what ever I write is based on patch available in the bug. So the final version could be little different than what it is now.

So first things first. How does the new multi-instance Property View looks like? Well, just like the old one except for the two new buttons in the tool bar:
multi instance property view
The first button is to toggle the pin property and the second one is to open up a new instance of the property view. Once you click on the second one, you will get another instance of the view. The first instance is pinned to the selection for your convenience (if its not already pinned). You can move around the new instance to other place and change the selection to a different object and you can compare the properties. I've pinned the first instance to MyFile1.txt and the current selection is MyFile2.txt. Now I can compare the properties of these two files side-by-side:

Multi instance Property view
You can create any number of instances and arrange them in any place in your perspective and use them. If you change any preferences like pinning of selection or column widths or hide categories, etc, all those changes are isolated to the particular instance which you are changing. The rest will stay unaffected. You can close any/all of the instances. When the workbench is closed and restarted, all the secondary instances will be gone. You will start only one view.
This is all good, so that the user wants to manipulate. As a plug-in developer how can we manipulate this? Opening a new instance is implemented as a Command. So if you want to create a new instance, then all you need to do is to call the command programmatically:

   1: try {
   2: IHandlerService handlerService = (IHandlerService) PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
   3:     handlerService.executeCommand("org.eclipse.ui.views.properties.NewPropertySheetCommand", null);
   4: } catch (Exception e) {
   5:     // handle exception
   6: }
There is currently no way to get the handle of the newly created instance. It would be very nice if the command returns the newly created view instead of null. Probably a dirty workaround would be to add a IPartListener before executing the command and look for the PropertyView created in the listener. I didn't try, but hope that would work.

Had the command taken a parameter for the secondary id, it would be easier for you to find the newly created instance. But the command neither had option for that. We still can iterate thru the view references in the window and get hold of the views. With the views, you can get/set the pinned property on them:


   1: IViewReference[] viewReferences = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getViewReferences();
   2: for (int i = 0; i < viewReferences.length; i++) {
   3:     if("org.eclipse.ui.views.PropertySheet".equals(viewReferences[i].getId())){
   4:
   5:         // this is one of the property view instance
   6:         PropertySheet propertyView = (PropertySheet) viewReferences[i].getView(true);
   7:     
   8:         // just toggle the pinned property
   9:         boolean pinned = propertyView.isPinned();
  10:         propertyView.setPinned(!pinned);
  11:     }
  12: }
I don't see any API to get hold of the current selection in the view, but you can trigger a property view to show the properties of an element by firing the selectionChanged method and then calling setPinned(true);

As I mentioned, all these info is subject to change, and I'll add more info to this tip as the bug gets updated.

Oct 20, 2008

Extending FilteredItemsSelectionDialog

In a previous tip, you have seen various Selection Dialogs in Eclipse. One thing which was not explained in it was FilteredItemsSelectionDialog, as it deserves a tip on its own. In this tip, I'll explain how to extend that class.

The Open Resource Dialog and Open Type Dialog are the ones you might be using very frequently. They are subclasses of FilteredItemsSelectionDialog. The dialog looks like this:

image  

Lets see what does it makes to create a simple dialog like this which allows us to select some Persons. First step is to extend the FilteredItemsSelectionDialog:

 


   1: public class FilteredPersonsSelectionDialog extends FilteredItemsSelectionDialog {
   2:  
   3:     private final List<Person> persons;
   4:  
   5:     public FilteredPersonsSelectionDialog(Shell shell, List<Person> persons) {
   6:         super(shell);
   7:         this.persons = persons;
   8:         setListLabelProvider(getListLabelProvider());
   9:         setDetailsLabelProvider(getDetailsLabelProvider());
  10:         setSelectionHistory(new PersonHistory());
  11:     }
  12:  
  13:     @Override
  14:     protected void fillContentProvider(AbstractContentProvider contentProvider,
  15:             ItemsFilter itemsFilter, IProgressMonitor progressMonitor)
  16:             throws CoreException {
  17:  
  18:         progressMonitor.beginTask("Looking for persons...", persons.size());
  19:         for (Person person : persons) {
  20:             contentProvider.add(person, itemsFilter);
  21:             progressMonitor.worked(1);
  22:         }
  23:  
  24:     }
  25:     private static final String SETTINGS = FilteredPersonsSelectionDialog.class.getCanonicalName();
  26:  
  27:     @Override
  28:     protected IDialogSettings getDialogSettings() {
  29:         IDialogSettings settings = Activator.getDefault().getDialogSettings().getSection(SETTINGS);
  30:  
  31:         if (settings == null) {
  32:             settings = Activator.getDefault().getDialogSettings().addNewSection(SETTINGS);
  33:         }
  34:  
  35:         return settings;
  36:     }
  37:  
  38:     @Override
  39:     protected ItemsFilter createFilter() {
  40:         return new PersonFilter();
  41:     }
  42:     
  43:     // other members
  44:  



When you have hundreds of thousands of items, adding it to the list up front is a huge task. So ideally, what happens is the dialog is displayed first and then the fillContentProvider is called when the user starts typing. (Or if you have specified the initial pattern, then its called as soon as the dialog is displayed to the user). For the sake of simplicity, I'm passing in all the items in the constructor and adding them to the contentProvider in the loop. This can't happen if the number of items is huge. Ideally you can specify the place where to search for thru the constructor and in the fillContentProvider, you start searching for all the items and add them. The progress will be shown below the search text box.


Apart from this FilteredPersonsSelectionDialog class, you would need few helper classes. The first one is the filter class, which should extend ItemFilter. This decides how to filter the items with the query provided in the search text box. You can decide on the wildcard strings, whether CamelCase searching is allowed etc. 


The next one is related to the history. The FilteredItemsSelectionDialog can remember the selection from the previous searches. To enable that, you need to extend the SelectionHistory and set it as we have did it in the line number 10. You need to override the abstract methods in the SelectionHistory to store & restore the item from the memento. The location where the memento saved into is dictated by the getDialogSettings().


The last set of classes are the LabelProviders. You need two of them. One for displaying in the list and the other for displaying in the details area, which should ideally provide more information than the first one.


image


Now the last bit of info. Invoking this dialog:




   1: FilteredPersonsSelectionDialog dialog = new FilteredPersonsSelectionDialog(window.getShell(), persons);
   2: dialog.setTitle("Select Person");
   3: dialog.setInitialPattern("?");   
   4: dialog.open();

Oct 6, 2008

Finding the platform

If you are doing something specific to Mac platform, so far you might have been doing like SWT.getPlatform().equals("carbon"), stop doing it. Moving forward to 3.5, SWT will be supporting Cocoa as well, so you have to check for the cocoa string as well. This issue also pops up for Windows. You can't rely upon the platform String to be "win32", it could be "wpf" for the Vista. Just to make your life easier, there are new methods in the JFace Util class: isMac(), isWindows(), etc. You should be using these methods instead of the String comparison. More details on these methods are available in the associated bug.