Eclipse Search

Loading

May 29, 2009

Search menu in Mac - the implementation

In the previous entry I mentioned about the Search Menu in Mac. Lets see what that search menu contains:


 

The first item is the "Clear" menu. It clears the recent search history. Next comes the recent searches. If you want to call it as "Search History", you can do it in the title field menu item. The title field & clear menu item are visible only when there is at least one entry in the recent searches. The menu can also have custom items, where you can include you own action items. Lets see how to add all these in an SWT app:


Text searchText = new Text(shell, SWT.SEARCH | SWT.ICON_CANCEL | SWT.ICON_SEARCH);
  SearchFieldSupport searchFieldSupport = new SearchFieldSupport(searchText);
  Menu menu = new Menu(searchText);
  
  MenuItem customItem = new MenuItem(menu, SWT.NONE);
  customItem.setText("Custom Action");
  customItem.addSelectionListener(new SelectionAdapter() {
   public void widgetSelected(SelectionEvent e) {
    MessageDialog.openInformation(shell, "Search Field", "Custom action is done here");
   };
  });
  
  MenuItem sep1 = new MenuItem(menu, SWT.SEPARATOR);
  
  MenuItem recentMenuItem = new MenuItem(menu, SWT.NONE);
  recentMenuItem.setText("Search History");
  SearchFieldSupport.setRecentSearchesTitle(recentMenuItem);
  
  // for the search history
  MenuItem recentsMenuItem = new MenuItem(menu, SWT.PUSH);
  SearchFieldSupport.setRecentSearches(recentsMenuItem);
  
  MenuItem sep2 = new MenuItem(menu, SWT.SEPARATOR);

  MenuItem clearMenuItem = new MenuItem(menu, SWT.PUSH);
  clearMenuItem.setText("Clear History");
  SearchFieldSupport.setClearRecents(clearMenuItem);

  searchFieldSupport.setMenu(menu);


Here is the result:

Its almost perfect except that when we clear the search history and see the menu, it looks like this:


Its because the separators are still shown even when there is no recent menu items. The hack is to set the separator as a resentSearches menu item:

MenuItem sep2 = new MenuItem(menu, SWT.SEPARATOR);
  SearchFieldSupport.setRecentSearchesTitle(sep2);

The Search Field also supports a custom menu item which tells that there is no recent search items. This menu item is automatically hidden when there search history is not empty. We can add that also before the second separator:

MenuItem recentsMenuItem = new MenuItem(menu, SWT.PUSH);
  SearchFieldSupport.setRecentSearches(recentsMenuItem);
  
  MenuItem noRecentMenuItem = new MenuItem(menu, SWT.NONE);
  noRecentMenuItem.setText("No Search History");
  SearchFieldSupport.setNoRecentSearches(noRecentMenuItem);
  
  MenuItem sep2 = new MenuItem(menu, SWT.SEPARATOR);
  SearchFieldSupport.setRecentSearchesTitle(sep2);

Now we are all set:


The last missing piece of the puzzle, the SearchFieldSupport class:

package org.eclipse.ui.cocoa.ext;

import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.cocoa.ext.internal.NSSearchField;

/**
 * 
 * @author Prakash G.R.
 * 
 */
public class SearchFieldSupport implements KeyListener, DisposeListener {
 
 private final NSSearchField nsSearchField;
 private final Text text;

 public SearchFieldSupport(Text text) {
  this.text = text;
  text.addKeyListener(this);
  text.addDisposeListener(this);
  nsSearchField = new NSSearchField(text);
 }

 public Text getText() {
  return text;
 }

 public String[] getRecentSearchStrings() {
  return nsSearchField.getRecentSearches();
 }
 
 public void setRecentSearchStrings(String[] recentSearches) {
  nsSearchField.setRecentSearches(recentSearches);
 }

 public void setMenu(Menu menu) {
  nsSearchField.setMenu(menu);
 }

 public static void setNoRecentSearches(MenuItem menuItem) {
  NSSearchField.setTag(menuItem, NSSearchField.NSSearchFieldNoRecentsMenuItemTag);
 }

 public static void setClearRecents(MenuItem menuItem) {
  NSSearchField.setTag(menuItem, NSSearchField.NSSearchFieldClearRecentsMenuItemTag);
 }

 public static void setRecentSearchesTitle(MenuItem menuItem) {
  NSSearchField.setTag(menuItem, NSSearchField.NSSearchFieldRecentsTitleMenuItemTag);
 }

 public static void setRecentSearches(MenuItem menuItem) {
  NSSearchField.setTag(menuItem, NSSearchField.NSSearchFieldRecentsMenuItemTag);
 }

 public void widgetDisposed(DisposeEvent e) {
  text.removeKeyListener(this);
 }

 public void keyPressed(KeyEvent e) {
  // do nothing
 }

 public void keyReleased(KeyEvent e) {
  if (e.keyCode == '\r') {
   String[] recentSearchStrings = getRecentSearchStrings();
   int oldSize = recentSearchStrings.length;
   String[] newSearchStrings = new String[recentSearchStrings.length + 1];
   System.arraycopy(recentSearchStrings, 0, newSearchStrings, 0, recentSearchStrings.length);
   newSearchStrings[oldSize] = text.getText();
   setRecentSearchStrings(newSearchStrings);
   text.setSelection(0, text.getText().length());
  }
 }

}

and the custom NSSearchField class:

package org.eclipse.ui.cocoa.ext.internal;

import java.lang.reflect.Field;

import org.eclipse.swt.internal.cocoa.NSArray;
import org.eclipse.swt.internal.cocoa.NSMenu;
import org.eclipse.swt.internal.cocoa.NSMenuItem;
import org.eclipse.swt.internal.cocoa.NSMutableArray;
import org.eclipse.swt.internal.cocoa.NSString;
import org.eclipse.swt.internal.cocoa.OS;
import org.eclipse.swt.internal.cocoa.id;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;

/**
 * @author Prakash G.R.
 * 
 */
@SuppressWarnings("restriction")
public class NSSearchField extends org.eclipse.swt.internal.cocoa.NSSearchField {

 public static final int NSSearchFieldRecentsTitleMenuItemTag = 1000;
 public static final int NSSearchFieldRecentsMenuItemTag = 1001;
 public static final int NSSearchFieldClearRecentsMenuItemTag = 1002;
 public static final int NSSearchFieldNoRecentsMenuItemTag = 1003;

 private static final int /* long */sel_setSearchMenuTemplate = OS.sel_registerName("setSearchMenuTemplate:");
 private static final int /* long */sel_setTag = OS.sel_registerName("setTag:");
 private static final int /* long */sel_setRecentSearches = OS.sel_registerName("setRecentSearches:");

 public NSSearchField(id id) {
  super(id);
 }

 public NSSearchField(Text text) {
  super(text.view);
 }

 public void setMenu(Menu menu) {
  try {
   Field field = Menu.class.getDeclaredField("nsMenu");
   field.setAccessible(true);

   NSMenu nsMenu = (NSMenu) field.get(menu);

   OS.objc_msgSend(this.id, sel_setSearchMenuTemplate, nsMenu.id);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 public static boolean setTag(MenuItem menuItem, int tag) {
  try {
   Field field = MenuItem.class.getDeclaredField("nsItem");
   field.setAccessible(true);

   NSMenuItem nsMenuItem = (NSMenuItem) field.get(menuItem);
   OS.objc_msgSend(nsMenuItem.id, sel_setTag, tag);

   // no action for titles
   if (tag == NSSearchFieldRecentsTitleMenuItemTag || tag == NSSearchFieldNoRecentsMenuItemTag) {
    nsMenuItem.setAction(0);
   }
   return true;
  } catch (Exception e) {
   return false;
  }
 }

 public String[] getRecentSearches() {
  NSArray recentSearches = super.recentSearches();
  String[] recentSearchStrings = new String[recentSearches.count()];
  for (int i = 0; i < recentSearchStrings.length; i++) {
   recentSearchStrings[i] = (new NSString(recentSearches.objectAtIndex(i))).getString();
  }
  return recentSearchStrings;
 }

 public void setRecentSearches(String[] recentSearchStrings) {
  
  NSMutableArray recentSearches = NSMutableArray.arrayWithCapacity(recentSearchStrings.length);
  for (String aRecentSearcb : recentSearchStrings) {
   NSString nsString = NSString.stringWith(aRecentSearcb);
   recentSearches.addObject(nsString);
  }
  OS.objc_msgSend(this.id, sel_setRecentSearches, recentSearches.id);
  
 }
}

Enjoy!

May 26, 2009

Search menu in Mac

In Mac applications, the little search box can have a pull down menu (similar to a View's pull down menu). It can have recent searches, user contributed menu entries, a menu item for clearing the recent searches etc. Here is the search box of Tweetie (the twitter app that I use):


Eclipse by default offers the SWT.SEARCH style which will bring this search box instead of the default text box. But there is no option to add the menu. For today's SWT-Cocoa hack, I tried to enhance the Search box with the search menu and here is the result:

The code is so ugly that I can't post it in public yet. Will post it with refinements tomorrow.

Update: The implementation is available here.

May 23, 2009

Badge label on Mac Dock icon

I'm passionate on both Mac and Eclipse. Although Eclipse runs well on a Mac, its not a marriage made in heaven. Its really hard to get the complete Mac experience with a portable UI tool kit. If you think of features like this one, its actually endless list. One such thing is the badge on the Dock Icon. In Mac, the Dock icon can have a badge - like the number of unread mails in your inbox. How much effort does it takes to add a badge to our RCP mail sample? Not much. Since SWT doesn't have a NSDockTile, we need a NSDockTile class with two methods, one to get the DockTile for the application and the other to set the badge on it (there is an easier way by using the Mac Generator tool, but that will be a patch to SWT itself, which I wanted to avoid):

/**
 * @author Prakash G.R. (grprakash@gmail.com) 
 * 
 */
@SuppressWarnings("restriction")
public class NSDockTile extends NSResponder {

 private static final int sel_setBadgeLabel_ = OS.sel_registerName("setBadgeLabel:");
 private static final int sel_dockTile_ = OS.sel_registerName("dockTile");
 private static final int sel_display_ = OS.sel_registerName("display");

 public NSDockTile(int id) {
  super(id);
 }

 public static NSDockTile getApplicationDockTile() {
  NSApplication sharedApplication = NSApplication.sharedApplication();
  int id = OS.objc_msgSend(sharedApplication.id, sel_dockTile_);
  NSDockTile dockTile = new NSDockTile(id);
  return dockTile;
 }

 public void setBadgeLabel(String badgeLabel) {
   NSString nsBadgeLabel = NSString.stringWith(badgeLabel);
   OS.objc_msgSend(this.id, sel_setBadgeLabel_, nsBadgeLabel.id);
   OS.objc_msgSend(this.id, sel_display_);
 }
}

And a two line code to set the badge:

NSDockTile nsDockTile = NSDockTile.getApplicationDockTile();
nsDockTile.setBadgeLabel("6");

There you go in the dock icon and in the minimized window:


In case you were wondering how the GMail icon for the application is set, here is the code for that:

ImageDescriptor imageDescriptor = ImageDescriptor.createFromURL(iconUrl);
Image image = imageDescriptor.createImage();
NSApplication app = NSApplication.sharedApplication();
app.setApplicationIconImage(image.handle);

Oh, BTW, I've just started learning Objective C, Cocoa & related things. So stay tuned, you will see more Mac related posts here. Also what is your favourite Mac feature that you are missing in Eclipse?

Update 15-Mar-2010:
   As of 3.6 M6, you don't have to do this hack. SWT has introduced a class TaskItem the API. See this SWT Snippet for usage.

May 16, 2009

Creating a custom Property View

In an RCP application, you might need a Properties View, which shows only the properties of a specific view or a set of views. But the generic Properties View will show from all the other views that support it. Armed up with the knowledge of how a PageBookView works, lets see how to hack the Properties View to listen only to a specific view.

The isImportant() method is the one which decides whether to create an IPage for the specific IWorkbenchPart or not. The idea is to override that method and return false for all the workbenchPart that we are not interested in. Lets create the view first:

<view
            class="com.eclipse_tips.views.CustomPropertiesView"
            icon="icons/sample.gif"
            id="com.eclipse-tips.views.customePropertiesView"
            name="My Properties View">
</view>

The CustomPropertiesView should extend PropertySheet and override the isImportant():

public class CustomPropertiesView extends PropertySheet {

 @Override
 protected boolean isImportant(IWorkbenchPart part) {
  if (part.getSite().getId().equals(IPageLayout.ID_PROJECT_EXPLORER))
   return true;
  return false;
 }
}

In this case, I'm making the view only to respond to Project Explorer and ignore other views. Here is the CustomPropertyView in action:
 
 

If you are on 3.5 or above, you would see the Pin Action in your Custom Properties View. If you don't want the Pin action in your properties view, there is no way to prevent the PropertySheet to adding the action. The action is added to both tool bar and menu in the createControl() method. Only way to get rid of the action is to remove it after the PropertySheet adds it:

@Override
 public void createPartControl(Composite parent) {
  
  super.createPartControl(parent);
  IMenuManager menuManager = getViewSite().getActionBars().getMenuManager();
  IContributionItem[] items = menuManager.getItems();
  for (IContributionItem iContributionItem : items) {
   if(iContributionItem instanceof ActionContributionItem) {
    if(((ActionContributionItem) iContributionItem).getAction() instanceof PinPropertySheetAction) {
     menuManager.remove(iContributionItem);
     break;
    }
   }
  }

  IToolBarManager toolBarManager = getViewSite().getActionBars().getToolBarManager();
  items = toolBarManager.getItems();
  for (IContributionItem iContributionItem : items) {
   if(iContributionItem instanceof ActionContributionItem) {
    if(((ActionContributionItem) iContributionItem).getAction() instanceof PinPropertySheetAction)) {
     toolBarManager.remove(iContributionItem);
     break;
    }
   }
  }
 }

And don't forget to override the isPinned() method:

@Override
 public boolean isPinned() {
  return false;
 }

There you go:

May 14, 2009

How to create a PageBookView

Think of Properties View. It displays the properties of the selected element in the active part. Whenever the selection changes or the active part changes, it tracks them and displays the properties (unless you used the 'Pin to selection' feature available from 3.5) There are many views like this, which update themselves when the active part changes. Outline view, Templates view, GEF Palette view, etc. If you want to create such view, its not a tough job - Eclipse provides you all the basic features in the class PageBookView. All you need is to extend this class and fill the void by implementing the abstract methods. Thats what we are going to see in this tip. The use case is to create a ActivePartTrackerView, which will display the name of the current active workbench part.

First lets create a View:

<extension
         point="org.eclipse.ui.views">
      <view
            class="com.eclipse_tips.views.SelectionView"
            icon="icons/sample.gif"
            id="com.eclipse-tips.views.pagebookview"
            name="Selection Provider View">
      </view>
   </extension>


The contents of the PageBookView is arranged in pages (IPage). There can be multiple pages, each of them is associated with a corresponding IWorkbenchPart. When the associated part becomes active, the PageBookView automatically switches to the respective page. When a PageBookView is created, it asks for a bootstrap part by calling getBootstrapPart() method. When no bootstrap part is found, it uses a default page. That default page is also shown when there are no pages for the currently active part. Lets start by creating this default page. To do that we need to implement the createDefaultPage() method, which returns the default page. Lets use the MessagePage which simply displays a string.

@Override
 protected IPage createDefaultPage(PageBook book) {
  MessagePage messagePage = new MessagePage();
  initPage(messagePage);
  messagePage.setMessage("No interested in this part");
  messagePage.createControl(book);
  return messagePage;
 } 


Now our SelectionView is up and running, except that its showing a static content not creating any IPages for the active parts. Before we create an IPage, how to determine whether to create an IPage for a given IWorkbenchPart or ignore it? Its by the isImportant() method. Lets say, we want to respond only to the parts that are contributed by the Platform UI.

@Override
 protected boolean isImportant(IWorkbenchPart part) {
  return part.getSite().getPluginId().startsWith("org.eclipse.ui");
 }

So if a Package Explorer(contributed by JDT) or the Manifest Editor(contributed by PDE) is the active part, then our SelectionView will not create any page and use the default page. If its a TextEditor or Project Explorer, then it will create the page and show it.

To create an IPage for a given part, we need to override the doCreatePage() method. Unlike the createDefaultPage() method this doesn't return a IPage, rather a PageRec. Why? This Page Record stores additional information - the associated workbench part and action bars:

@Override
 protected PageRec doCreatePage(IWorkbenchPart part) {
  MessagePage messagePage = new MessagePage();
  initPage(messagePage);
  messagePage.setMessage("Page for "+part.getTitle());
  messagePage.createControl(getPageBook());
  return new PageRec(part, messagePage);
 }

 
 

When you are switching between the TextEditor and the ProjectExplore view, the PageBookView will track and find the page for the active part and automatically show it. When the page is no longer needed (the associated part is closed), it would call the doDestroyPage(). This is where you would ideally remove any listeners and dispose of the resources.

So the whole class goes here:

package com.eclipse_tips.views;

import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.part.IPage;
import org.eclipse.ui.part.MessagePage;
import org.eclipse.ui.part.PageBook;
import org.eclipse.ui.part.PageBookView;

/**
 * @author Prakash G.R.
 *
 */
public class ActivePartTrackerView extends PageBookView {

 @Override
 protected IPage createDefaultPage(PageBook book) {
  MessagePage messagePage = new MessagePage();
  initPage(messagePage);
  messagePage.setMessage("No interested in this part");
  messagePage.createControl(book);
  return messagePage;
 }

 @Override
 protected PageRec doCreatePage(IWorkbenchPart part) {
  MessagePage messagePage = new MessagePage();
  initPage(messagePage);
  messagePage.setMessage("Page for "+part.getTitle());
  messagePage.createControl(getPageBook());
  return new PageRec(part, messagePage);
 }

 @Override
 protected void doDestroyPage(IWorkbenchPart part, PageRec pageRecord) {
  pageRecord.page.dispose();
 }

 @Override
 protected IWorkbenchPart getBootstrapPart() {
  IWorkbenchPage page = getSite().getPage();
  if(page != null) {
   // check whether the active part is important to us
   IWorkbenchPart activePart = page.getActivePart();
   return isImportant(activePart)?activePart:null;
  }
  return null;
 }

 @Override
 protected boolean isImportant(IWorkbenchPart part) {
  return part.getSite().getPluginId().startsWith("org.eclipse.ui");
 }

}

May 12, 2009

Sheet support in SWT

For those who have downloaded 3.5 M7 would have seen Sheets support has been added to SWT and Platform UI has used the API wherever applicable. For those who are wondering what a Sheet means, its an eye-candy in Mac. Here is a sample of a MessageDialog shown with and without Sheet style:



(in case you didn't notice the obvious, I'm back on a Mac :-P)

So what is a Sheet?

A Sheet is essentially a Dialog, which is tied to a parent window. It acts as a Modal Dialog, so the user cannot perform any operations on a window until the Dialog is dismissed. (He is free to work on other windows) The dialog is
  • always attached with the window until dismissed
  • placed in the center of the window
  • moves along when the window is moved
To Sheet or not to Sheet?

  • Use Sheets when the interaction is very short like the question "Is Eclipse a cool product?  ". Opening a 15 step Wizard to choose your life partner is not right place to use Sheet.
  • Do not use Sheets where the user might still need to interact with the Window (like copy something from window and paste it in the Sheet). Since Sheets are Window Modal, it doesn't allow the user to interact with the associated Window
  • Sheets are not Application Modal. So if you want a dialog to be Application Modal, don' use Sheets
  • Unlike normal Dialogs, Sheets do not have title. Avoid Sheets where title provides valuable information.
  • Do not use nested Sheets (says Apple's UI guidelines)
How do I use Sheet?

Simple. Set the SWT.Sheet style to the Shell of the dialog. In case you are using the MessageDialog, pass on the style bit as the last argument to the MessageDialog.open() method

Commands Part 7: Adding standard commands

In the earlier installments, we have seen adding our commands to menus and toolbars. But what about the standard menu items like Cut, Copy, Paste etc? We'll see how to add these to a context menu of the Sample View.

Create the Sample View through the PDE's Extension Point Wizard. In the SampleView class, notice these lines:


private void hookContextMenu() {

// rest of the code...
getSite().registerContextMenu(menuMgr, viewer);
}

private void fillContextMenu(IMenuManager manager) {

//rest of the code
manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
}


The first one registers this context menu, so that other plugins can contribute their commands to it. The second one adds a separator, which serves as a place holder/location where the contribution goes. So the SampleView is ready to accept the contributions, lets now contribute:

   <extension
point="org.eclipse.ui.menus">
<menuContribution
locationURI="popup:com.eclipse_tips.commands.part7.views.SampleView?after=additions">
<command commandId="org.eclipse.ui.edit.cut"/>
<command commandId="org.eclipse.ui.edit.copy"/>
<command commandId="org.eclipse.ui.edit.paste"/>
</menuContribution>
</extension>


The id for the context menu, is by default the View's id, and we add our contribution after the separator. We can add any command, either the Platform defined or our own. Result:


Now we have the menu items, images, shortcut keys everything in place (you ought to love the command framework :-)), except that the items are not enabled. Its because there is no handler for these commands for the given context. There are multiple ways to contribute the handler, since we are adding this to our view, lets use the activePartId variable:

   <extension
point="org.eclipse.ui.handlers">
<handler
class="com.eclipse_tips.commands.part7.handlers.CutHandler"
commandId="org.eclipse.ui.edit.cut">
<activeWhen>
<with
variable="activePartId">
<equals
value="com.eclipse_tips.commands.part7.views.SampleView">
</equals>
</with>
</activeWhen>
</handler>


Other commands also have similar handlers. Now:


There you go. In the similar way you can add all the standard commands to any menu/context menu you prefer. Just make sure that you have the handler for the commands with the right activeWhen & enabledWhen expression.

The standard command Ids can be found in the IWorkbenchCommandConstants class (available in 3.5)