2005-05-29

Searching Spotlight/Google/Dictionary.app from Java text components

In 10.4, NSTextView menus gained "Search in Spotlight", "Search in Google", and "Look Up in Dictionary". Three different ways of searching outside of the text itself for stuff relating to the selection. Today, I added equivalent items to the menu of PTextArea, the Java/Swing replacement for JTextArea that Phil Norman started, and that we're still working on.

I don't like the way Swing does menus. In my experience, it's more convenient to have a component ask a set of menu item providers whether they have any menu items, at the time the popup trigger is actuated. One of the great things about this scheme is that it's trivial to have, as with NSTextView, a set of default items (such as "Cut", "Copy", and "Paste"), a set of items corresponding to a misspelling at the cursor (the guesses, "Ignore Spelling", and "Learn Spelling), and a set of items that appear if there's a non-empty selection ("Search in Spotlight", "Search in Google", and "Look Up in Dictionary").

We have a class in salma-hayek called EPopupMenu that takes care of this, and also makes sure you're using AWT menus on Mac OS and Swing menus everywhere else. (Swing's menus look terribly unrealistic on Mac OS, and Linux's AWT menus are Motif, which is like a bad flashback to the early 1990s.) PTextArea uses EPopupMenu, so all we need to do is register a new provider that checks for a non-empty selection, and write a few new actions.

The Search in Google action is trivial. BrowserLauncher is from Eric J. Albert's sourceforge project of the same name, slightly modified to use /usr/bin/sensible-browser (I kid you not!) instead of the antiquated netscape.

private class SearchInGoogleAction extends AbstractAction {
public SearchInGoogleAction() {
super("Search in Google");
}

public void actionPerformed(ActionEvent e) {
try {
String encodedSelection =
StringUtilities.urlEncode(textArea.getSelectedText().trim());
BrowserLauncher.openURL("http://www.google.com/search?q=" +
encodedSelection + "&ie=UTF-8&oe=UTF-8");
} catch (Exception ex) {
Log.warn("Exception launching browser", ex);
}
}
}

The Look Up in Dictionary action is also pretty simple, though there are a couple of gotchas:

private class LookUpInDictionaryAction extends AbstractAction {
public LookUpInDictionaryAction() {
super("Look Up in Dictionary");
setEnabled(GuiUtilities.isMacOs());
}

public void actionPerformed(ActionEvent e) {
try {
// We need to rewrite spaces as "%20" for them to find their
// way to Dictionary.app unmolested. The usual url-encoded
// form ("+") doesn't work, for some reason.
String encodedSelection =
textArea.getSelectedText().trim().replaceAll("\\s+", "%20");
// In Mac OS 10.4.1, a dict: URI that causes Dictionary.app to
// start doesn't actually cause the definition to be shown, so
// we need to ask twice. If we knew the dictionary was already
// open, we could avoid the flicker. But we may as well wait
// for Apple to fix the underlying problem.
BrowserLauncher.openURL("dict:///");
BrowserLauncher.openURL("dict:///" + encodedSelection);
} catch (Exception ex) {
Log.warn("Exception launching browser", ex);
}
}
}

I was interested to find while testing this that the equivalent NSTextView menu item doesn't work outside of Safari. I don't know if it's just my machine, but I can't get it to work in Dictionary, Mail, or Text Edit.

The Search in Spotlight action is a bit trickier, though. I had to write a little Objective-C++ program to do the hard part of calling NSPerformService:

#include <Cocoa/Cocoa.h>
#include <iostream>

void doService(const std::string& service, const std::string& text) {
NSPasteboard* pb = [NSPasteboard pasteboardWithUniqueName];
[pb declareTypes:[NSArray arrayWithObject:NSStringPboardType]
owner:nil];
[pb setString:[NSString stringWithUTF8String:text.c_str()]
forType:NSStringPboardType];

NSString* serviceString = [NSString stringWithUTF8String:service.c_str()];
BOOL success = NSPerformService(serviceString, pb);
if (success == NO) {
NSLog(@"NSPerformService failed.");
exit(1);
}
}

static void usage(std::ostream& os, const std::string& name) {
os << "Usage: " << name << " <service> <text>" << std::endl;
os << "Examples:" << std::endl;
os << " " << name << " Spotlight blackberry" << std::endl;
os << " " << name << " 'Mail/Send To' root@localhost" << std::endl;
}

int main(int argCount, char* args[]) {
if (--argCount != 2) {
usage(std::cerr, args[0]);
exit(1);
}

NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
[NSApplication sharedApplication];
doService(args[1], args[2]);
[pool release];
return 0;
}

So there you have it: anything NSTextView can do, your Java application can do better. Better in that you can offer some of the functionality on other platforms, and better in that Search in Google works even if your application isn't called Safari!

As usual, the latest version of all this stuff is in salma-hayek, along with the PTextArea I mentioned.