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.

2005-05-28

Passing MS Outlook meeting requests from Mail to iCal

Mail's supposed to be able to do this itself. There's an option "Add invitations to iCal" in the preferences, and I have it set to "automatically". Yet it does nothing.

Mail shows me the text/plain or text/html alternate, and there isn't even an attachment icon for the text/calendar form.

I had hoped that this would be my first opportunity to make real use of mdfind(1), but mdls(1) shows that there's nothing useful stored about a message containing a meeting request:

kMDItemAttributeChangeDate = 2005-05-25 09:42:11 -0700
kMDItemAuthorEmailAddresses = ("user@mail.server")
kMDItemAuthors = ("Martin Dorey")
kMDItemContentCreationDate = 2005-05-24 13:12:22 -0700
kMDItemContentModificationDate = 2005-05-25 09:42:07 -0700
kMDItemContentType = "com.apple.mail.emlx"
kMDItemContentTypeTree = ("com.apple.mail.emlx", "public.data",
"public.item", "public.message")
kMDItemDisplayName = "Buy FogBugz"
kMDItemFSContentChangeDate = 2005-05-25 09:42:07 -0700
kMDItemFSCreationDate = 2005-05-24 13:13:14 -0700
kMDItemFSCreatorCode = 0
kMDItemFSFinderFlags = 0
kMDItemFSInvisible = 0
kMDItemFSLabel = 0
kMDItemFSName = "14660.emlx"
kMDItemFSNodeCount = 0
kMDItemFSOwnerGroupID = 501
kMDItemFSOwnerUserID = 501
kMDItemFSSize = 6195
kMDItemFSTypeCode = 0
kMDItemID = 3638978
kMDItemKind = "emlx"
kMDItemLastUsedDate = 2005-05-24 13:12:22 -0700
kMDItemRecipientEmailAddresses = ("user@mail.server")
kMDItemRecipients = ("Elliott Hughes")
kMDItemTitle = "Buy FogBugz"

So traditional Unix tools will have to do. Fine by me...

#!/usr/bin/ruby -w

require 'tempfile.rb'

inbox_roots = "~/Library/Mail/*/INBOX.imapmbox/Messages"

filenames=`find #{inbox_roots} -name "*.emlx" -print0 | xargs -0 grep -wl text/calendar`.split("\n")
filenames.each() {
|filename|
puts("Extracting meeting request from '#{filename}'...")
filename =~ /\/(\d+).emlx$/
message_id = $1

# Pull just the .ics data out of the message.
message = File.new(filename).read()
message =~ /\btext\/calendar\b.*\n\n(.*)\n\n/m
meeting_request = $1

# iCal is clever enough (it seems) to recognize the identical
# UID, so we don't have to bother.
ics_filename = "/tmp/meeting_request.#{message_id}.ics"
ics_file = File.new(ics_filename, "w")
ics_file.puts(meeting_request)
system("open #{ics_filename}")
}
exit(0)

It took me less time to write that than I've spent complaining about the lack/brokennesss of this functionality.

I'd rather things just worked, as they're supposed to on Mac OS, but this will do in the meantime. (I don't know what was supposed to happen under 10.3, but there I used to find that meeting requests wouldn't appear in Mail at all. I could connect to the IMAP server myself and LIST and RETR them, but Mail either couldn't see them, or wouldn't show them.)

2005-05-24

Automatically detecting AWT event dispatch thread hangs

I mentioned Apple's "Spin Control" recently, which detects hung Cocoa applications. It doesn't work for Java applications because they're packed full of multithreaded goodness, and the main AppKit thread doesn't tend to get stuck. The problematic thread in a Java application is the AWT's event dispatch thread. Do too much work on that, and your GUI will become unresponsive.

I've been meaning to write something similar for Java ever since I read the changes in the first 1.5 beta and saw that it was going to be possible to get a stack trace for a different thread. Unfortunately, it's taken Apple until now to give me a Java 1.5 implementation.

The principle is simple. We push our own EventQueue implementation on top of the system's one. That makes a note of when we start to dispatch an event before calling the normal dispatch code. There's a timer (a java.util one, not a javax.swing one, because that would try to call us back on the event thread, which would be useless) going off every 100 ms. Each time the timer fires, we see how long we've been dispatching the current event. If it's taken us more than 500 ms, which is long enough for a human to notice, though not particularly annoying in most circumstances, we dump a stack trace for the event dispatch thread, showing where it's got to. When the normal dispatch code returns, if we dumped a stack trace, we print a message saying how long it took in total.

Experience with similar watchdog systems on other systems suggests that a single stack trace as soon as you notice a problem is usually sufficient to find and fix the problem.

To use this in your own programs, just call the static initMonitoring method on EventDispatchThreadHangMonitor and you're away.

Here's an example of the output when I put Thread.sleep(2500) in one of my applications' Action subclasses:

--- event dispatch thread hung for 502 ms:
java.lang.Thread.sleep(Native Method)
e.edit.FixedFontAction.getFont(FixedFontAction.java:16)
e.edit.ChangeFontAction.actionPerformed(ChangeFontAction.java:86)
javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1882)
javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2202)
javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:420)
javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:258)
javax.swing.AbstractButton.doClick(AbstractButton.java:334)
apple.laf.ScreenMenuItem.actionPerformed(ScreenMenuItem.java:104)
java.awt.MenuItem.processActionEvent(MenuItem.java:597)
java.awt.MenuItem.processEvent(MenuItem.java:556)
java.awt.MenuComponent.dispatchEventImpl(MenuComponent.java:298)
java.awt.MenuComponent.dispatchEvent(MenuComponent.java:286)
java.awt.EventQueue.dispatchEvent(EventQueue.java:466)
--- event dispatch thread unstuck after 2506 ms.

And here's the code. This is LGPL, and the latest version is available in the (stupidly named) salma-hayek library.

package e.debug;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

/**
* Monitors the AWT event dispatch thread for events that take longer than
* a certain time to be dispatched.
*
* The principle is to record the time at which we start processing an event,
* and have another thread check frequently to see if we're still processing.
* If the other thread notices that we've been processing a single event for
* too long, it prints a stack trace showing what the event dispatch thread
* is doing, and continues to time it until it finally finishes.
*
* This is useful in determining what code is causing your Java application's
* GUI to be unresponsive.
*
* @author Elliott Hughes <enh@jessies.org>
*/
public final class EventDispatchThreadHangMonitor extends EventQueue {
private static final EventQueue INSTANCE = new EventDispatchThreadHangMonitor();

// Time to wait between checks that the event dispatch thread isn't hung.
private static final long CHECK_INTERVAL_MS = 100;

// Maximum time we won't warn about.
private static final long UNREASONABLE_DISPATCH_DURATION_MS = 500;

// Used as the value of startedLastEventDispatchAt when we're not in
// the middle of event dispatch.
private static final long NO_CURRENT_EVENT = 0;

// When we started dispatching the current event, in milliseconds.
private long startedLastEventDispatchAt = NO_CURRENT_EVENT;

// Have we already dumped a stack trace for the current event dispatch?
private boolean reportedHang = false;

// The event dispatch thread, for the purpose of getting stack traces.
private Thread eventDispatchThread = null;

private EventDispatchThreadHangMonitor() {
initTimer();
}

/**
* Sets up a timer to check for hangs frequently.
*/
private void initTimer() {
final long initialDelayMs = 0;
final boolean isDaemon = true;
Timer timer = new Timer("EventDispatchThreadHangMonitor", isDaemon);
timer.schedule(new HangChecker(), initialDelayMs, CHECK_INTERVAL_MS);
}

private class HangChecker extends TimerTask {
@Override
public void run() {
// Synchronize on the outer class, because that's where all
// the state lives.
synchronized (INSTANCE) {
checkForHang();
}
}

private void checkForHang() {
if (startedLastEventDispatchAt == NO_CURRENT_EVENT) {
// We don't destroy the timer when there's nothing happening
// because it would mean a lot more work on every single AWT
// event that gets dispatched.
return;
}
if (timeSoFar() > UNREASONABLE_DISPATCH_DURATION_MS) {
reportHang();
}
}

private void reportHang() {
if (reportedHang) {
// Don't keep reporting the same hang every 100 ms.
return;
}

reportedHang = true;
System.out.println("--- event dispatch thread stuck processing event for " + timeSoFar() + " ms:");
StackTraceElement[] stackTrace = eventDispatchThread.getStackTrace();
printStackTrace(System.out, stackTrace);
}

private void printStackTrace(PrintStream out, StackTraceElement[] stackTrace) {
// We know that it's not interesting to show any code above where
// we get involved in event dispatch, so we stop printing the stack
// trace when we get as far back as our code.
final String ourEventQueueClassName = EventDispatchThreadHangMonitor.class.getName();
for (StackTraceElement stackTraceElement : stackTrace) {
if (stackTraceElement.getClassName().equals(ourEventQueueClassName)) {
return;
}
out.println(" " + stackTraceElement);
}
}
}

/**
* Returns how long we've been processing the current event (in
* milliseconds).
*/
private long timeSoFar() {
long currentTime = System.currentTimeMillis();
return (currentTime - startedLastEventDispatchAt);
}

/**
* Sets up hang detection for the event dispatch thread.
*/
public static void initMonitoring() {
Toolkit.getDefaultToolkit().getSystemEventQueue().push(INSTANCE);
}

/**
* Overrides EventQueue.dispatchEvent to call our pre and post hooks either
* side of the system's event dispatch code.
*/
@Override
protected void dispatchEvent(AWTEvent event) {
preDispatchEvent();
super.dispatchEvent(event);
postDispatchEvent();
}

/**
* Stores the time at which we started processing the current event.
*/
private synchronized void preDispatchEvent() {
if (eventDispatchThread == null) {
// I don't know of any API for getting the event dispatch thread,
// but we can assume that it's the current thread if we're in the
// middle of dispatching an AWT event...
eventDispatchThread = Thread.currentThread();
}

reportedHang = false;
startedLastEventDispatchAt = System.currentTimeMillis();
}

/**
* Reports the end of any ongoing hang, and notes that we're no longer
* processing an event.
*/
private synchronized void postDispatchEvent() {
if (reportedHang) {
System.out.println("--- event dispatch thread unstuck after " + timeSoFar() + " ms.");
}
startedLastEventDispatchAt = NO_CURRENT_EVENT;
}
}

2005-05-23

Spin Control and non-GUI NSApplication programs

I've been running /Developer/Applications/Performance Tools/Spin Control.app continuously of late. I realized that the one thing better than Activity Monitor's three-click sampling of a hung application (one click on the Activity Monitor icon, one click on the errant process, one click on "Sample Process") would be something that just ran in the background, recognized a hung application, and sampled it for me. And that's what Apple's Spin Control is.

Every time Address Book crashes (there's a problem involving the interaction between the Large Type menu option on phone numbers and tool tips), I get an entry. Every time Safari decides it's going to ignore me for a few seconds, I get an entry. (I'd heard other people complain about Safari, but it's only recently I've used Safari enough to notice; I prefer Camino, in the main.) Mail shows up from time to time, too.

Anyway, the only bit of my code that shows up in Spin Control is the little command-line tool I wrote to pretend to be ispell(1) but work by calling NSSpellChecker. The idea being to have a fairly portable (to other Unixes) way of offering spelling checking in Java applications, in a way that uses the same dictionaries and algorithms as native components.

I surfed the web in vain for a while. I was sure there must be a way to tell Mac OS that my program isn't really a GUI application; it just has to start up NSApplication because NSSpellChecker aborts otherwise. Something to do with LSBackgroundOnly or LSUIElement, I thought.

It turns out that there's not much you can do about this configuration at run-time. Especially not if you're just a command-line program (as opposed to something launched from an application bundle). So what's the answer? Well, my program wasn't getting a Dock icon, so all I needed to do was make sure we respond to whatever heartbeat it is that Spin Control is using. I still don't know what that is, exactly. But I did guess that [NSApplication run] would make the problem go away.

So my solution is to detach a new thread to run the real "main" program, and main itself then goes straight into the NSApplication run loop.

Here's the full code:

#include <iostream>
#include <string>

#import <AppKit/AppKit.h>

/**
* Creates and releases an NSAutoreleasePool in its scope. Having to manually
* release an autorelease pool on every exit point from a method is silly, but
* Objective-C++ lets us fix this.
*/
class ScopedAutoReleasePool {
public:
ScopedAutoReleasePool() {
m_pool = [[NSAutoreleasePool alloc] init];
}

~ScopedAutoReleasePool() {
[m_pool release];
}

private:
NSAutoreleasePool* m_pool;
};

/**
* Allows NSString to be output like std::string or a const char*.
*/
std::ostream& operator<<(std::ostream& os, NSString* rhs) {
os << [rhs UTF8String];
return os;
}

@interface Ispell : NSObject
+ (void) ispellRunLoop:(id) unusedParameter;
@end

@implementation Ispell

NSSpellChecker* checker;

+ (void) showSuggestionsForWord:(NSString*) word {
ScopedAutoReleasePool pool;
std::ostream& os = std::cout;

NSArray* guesses = [checker guessesForWord:word];
if ([guesses count] == 0) {
os << "# " << word << " 0\n";
return;
}

os << "& " << word << " " << [guesses count] << " 0: ";
for (size_t i = 0; i < [guesses count]; ++i) {
if (i > 0) {
os << ", ";
}
NSString* guess = [guesses objectAtIndex:i];
os << guess;
}
os << "\n";
}

+ (bool) isCorrect:(NSString*) word {
ScopedAutoReleasePool pool;
NSRange range = [checker checkSpellingOfString:word startingAt:0];
bool isCorrect = (range.length == 0);
return isCorrect;
}

+ (void) ispellRunLoop:(id) unusedParameter {
(void) unusedParameter;

// Each thread needs its own pool, and we don't get one for free.
ScopedAutoReleasePool pool;
checker = [NSSpellChecker sharedSpellChecker];

std::ostream& os(std::cout);
os << "@(#) International Ispell 3.1.20 (but really NSSpellChecker)\n";
while (true) {
ScopedAutoReleasePool pool;
std::string line;
getline(std::cin, line);
if (line.length() == 0) {
// We're done.
[[NSApplication sharedApplication] terminate:nil];
} else if (line == "!") {
// set terse mode; ignore.
} else if (line[0] == '^') {
std::string word(line.substr(1));
NSString* string = [NSString stringWithUTF8String:word.c_str()];
if ([Ispell isCorrect:string] == false) {
[Ispell showSuggestionsForWord:string];
}
os << std::endl;
} else if (line[0] == '*') {
std::string word(line.substr(1));
// FIXME: insert word into dictionary.
} else {
abort();
}
}
}

@end

int main(int /*argc*/, char* /*argv*/[]) {
ScopedAutoReleasePool pool;

// Pretend to be ispell in a new thread, so we can enter the normal event
// loop and prevent Spin Control from mistakenly diagnosing us as a hung
// GUI application.
[NSThread detachNewThreadSelector:@selector(ispellRunLoop:)
toTarget:[Ispell class]
withObject:nil];

[[NSApplication sharedApplication] run];
}

[When AppKit methods return nil... is an update to this article explaining the race condition in this code.]

2005-05-21

Slideshows in Mail.app

In case it wasn't obvious from my post about how to do Mac OS slideshows from Java/shell scripts, I've been won over. I know I complained (and I still wished they'd spent more time fixing Mail's problems with wrapping), but I will admit that Mail's slideshow button is useful, and it probably didn't take more than one edit/build/test cycle to write.

2005-05-15

YourKit Java Profiler

I gave the YourKit Java Profiler a go. Here are my comments. It seems promising, but it's not great yet.

2005-05-14

JLS3 PDF

You can get JLS3 as one big PDF file now. No HTML, though. Once upon a time that would have annoyed me, but PDF is a whole different experience on Mac OS than anywhere else. It's so fast it's actually useful. The search & bookmarks are pretty good, too, though they (especially the latter) depend somewhat on the quality of the PDF file itself.

It's certainly easier for me to search one PDF file in Preview than a bunch of HTML files in Safari (and Google doesn't offer quite such focused searching). Both, of course, are more conveniently searched than is a dead tree.

Sun offer this apology:

Note: This online version differs from the final print version in minor ways, primarily the absence of quotations due to copyright issues. The physical book should be available in June 2005.

Which sounds to me like an additional reason to favor the PDF. I've never liked the pseudo-relevant quotations writers of books on programming languages are so keen on starting chapters with. It just reads like a sign that even the author thinks the book is dull and needs livening up.

I'd be more impressed if they spent their time factoring out semi-duplicate chunks of the specification, as they would if it were a program. For example: How do unary numeric promotion and binary numeric promotion differ, exactly? Is the difference between "In any case, value set conversion is then applied" and "After the type conversion, if any, value set conversion is applied" deliberate? Does "if any" mean "whether or not conversion occurred" or "only if conversion occurred"?

People complain about cases where the implementation is the specification, but at least that's unambiguous!

Mac OS 10.4 slideshows from Java

You may have seen the slideshows provided by iPhoto, and now the Finder, and Mail. They're pretty nice. But how do they work, and how can you start one from a Java program?

To find out, I selected some images in the Finder, started a profile, and then started a slideshow. This line stood out:

203 -[Slideshow runSlideshowWithDataSource:options:]

Admittedly, I could probably have guessed that name, but it's always good to be sure. locate(1) came up with a private framework called Slideshow, and nm(1) on the shared library within showed that we were looking in the right place.

Once you know that, class-dump is broadly equivalent to Java's javap(1), and will make you a .h file for the private framework (part of being private is that you don't get the header files).

Slideshow follows the usual sharedSlideshow pattern, so there's no need to worry about how to create a suitable instance. There are two 'run' methods, runSlideshowWithDataSource:options: and runSlideshowWithPDF:options:. The latter is trivially easy to use. Just give it an NSURL pointing to a PDF file, and you're away. (You can usually recognize when something's expecting an NSURL if you give it an NSString and it complains that there's no scheme method.)

But I wanted a slideshow of pictures, not the pages in a PDF file.

If you're a Java programmer, you probably think of an interface as a static thing. So you'd expect to be able to find statically the methods your "data source" is supposed to implement. In Objective-C, this isn't necessarily the case. A common pattern is for the caller to invoke respondsToSelector: to see what you're capable of, and only invoke other methods if it knows you have them. So if you pass a new NSObject instance as your data source, for example, you won't get any errors, but you won't get a slideshow either.

What you want is a method like this, that prints the name of everything it's asked for:

- (BOOL)respondsToSelector:(SEL)aSelector {
NSLog(@"respondsToSelector: %@", NSStringFromSelector(aSelector));
return [super respondsToSelector: aSelector];
}

If you provide such an object to Slideshow, you see you're asked about the following selectors:

2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: numberOfObjectsInSlideshow
2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: slideshowObjectAtIndex:
2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: canExportObjectAtIndexToiPhoto:
2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: exportObjectsToiPhoto:

So numberOfObjectsInSlideshow and slideshowObjectAtIndex: looks like the minimum we have to implement.

If you're wondering about the "options", by the way, they're an NSDictionary. The keys (determined using the same respondsToSelector: trick as above only with objectForKey: instead) appear to be:

  • startIndex
  • debug
  • MAGIC_NUMBER
  • autoPlayDelay


To get things started, I gave NSApplication my own delegate, and made applicationDidFinishLaunching: start the slideshow. The slideshow is asynchronous, so I also had to implement applicationShouldTerminateAfterLastWindowClosed: so we can quit when it's finished.

As usual, the latest version will be in the salma-hayek library, linked to from my home page. But here's a snapshot in time:

/*
slideshow - a command-line interface to Mac OS' slideshow functionality.
Copyright (C) 2005 Elliott Hughes

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

*/
#include <Cocoa/Cocoa.h>

// Generated by class-dump 3.0.
// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004 by Steve Nygard.
@interface Slideshow : NSResponder {
id mPrivateData;
}

+ (id)sharedSlideshow;
+ (void)addImageToiPhoto:(id)fp8;
- (id)init;
- (void)dealloc;
- (void)setDataSource:(id)fp8;
- (void)loadConfigData;
- (void)runSlideshowWithDataSource:(id)fp8 options:(id)fp12;
- (void)startSlideshow:(id)fp8;
- (void)runSlideshowWithPDF:(id)fp8 options:(id)fp12;
- (void)stopSlideshow:(id)fp8;
- (void)noteNumberOfItemsChanged;
- (void)reloadData;
- (int)indexOfCurrentObject;
- (void)setAutoPlayDelay:(float)fp8;
- (void)mouseMoved:(id)fp8;
@end

@interface MyDelegate : NSObject {
NSMutableArray* mFilenames;
}
- (void) addFilename:(const char*)utf8;
@end

@implementation MyDelegate
- (id)init {
self = [super init];
if (self != nil) {
mFilenames = [[NSMutableArray alloc] init];
}
return self;
}

- (void)dealloc {
[mFilenames dealloc];
[super dealloc];
}

// Useful if you need to find out what interface you have to implement...
- (BOOL)respondsToSelector:(SEL)aSelector {
//NSLog(@"respondsToSelector: %@", NSStringFromSelector(aSelector));
return [super respondsToSelector: aSelector];
}

- (void)addFilename:(const char*)utf8 {
[mFilenames addObject:[NSString stringWithUTF8String: utf8]];
}

- (int)numberOfObjectsInSlideshow {
return [mFilenames count];
}

- (id)slideshowObjectAtIndex:(int)index {
return [mFilenames objectAtIndex: index];
}

// Start the slideshow as soon as we're running.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
(void) aNotification;

Slideshow* slideshow = [Slideshow sharedSlideshow];
[slideshow runSlideshowWithDataSource:self options:nil];
}

// Tell Cocoa that when the slideshow ends, our work here is done.
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication {
(void)theApplication;
return YES;
}

@end

int main(int argCount, char* argValues[]) {
// These are required if we want Cocoa to work.
static NSAutoreleasePool* global_pool;
global_pool = [[NSAutoreleasePool alloc] init];
static NSApplication* application;
application = [NSApplication sharedApplication];

MyDelegate* delegate = [[MyDelegate alloc] init];
(void) argCount;
while (*++argValues) {
[delegate addFilename: *argValues];
}
[application setDelegate: delegate];
[application run];

return 0;
}

Build with: gcc -o slideshow slideshow.m -framework Cocoa -F/System/Library/PrivateFrameworks -framework Slideshow

Run with: ./slideshow ~/Pictures/*

So not only can you use Mac OS' slideshow functionality from your Java programs, you can use it from your scripts, too. How cool is that? If the world isn't awash with fancy-looking pr0n viewers by Monday, I'll want to know why...

2005-05-09

Fixing the annoyingly small Mac OS popup dictionary window

I've always been a strong proponent of doing everything in code. If you do it well, and you own the code, I still think it's the best way. But when, as with Mac OS, you're just presented with a bunch of binaries and no source, it's great to be able to use Interface Builder to fix things.

Sick of having to scroll through a definition in the unusably small pop-up dictionary window? Make it larger in the nib, log out and back in, and it's fixed. (Though it seems to have pushed the part-of-speech information into the main text, but that's a price I'm more than willing to pay to not have to find the mouse, find the miniature scroll bar, and scroll.)

I can't think how to fix the stupid behavior that favors the word under the mouse over the word at the insertion point, though... I think that would need access to the source. Or more binary patching than I'm really in the mood for.

Why do Dictionary.app's preferences allow you to choose between "Open Dictionary application" and "Open Dictionary panel" for the contextual menu, but not for the control-command-D keystroke?

Why can't I choose a less ugly font than Baskerville? Editing DefaultStyle.css is awkward (it's even conveniently encoded in UTF-16, for no obvious reason). The answer, you'll find, is that they went way overboard with the number of different fonts. Stripping them all out (I left HiraMinPro-W3) and using LucidaGrande for html|body works nicely though. You can make the text a lot smaller and still read it. Serifs are so 1700's, when no-one expected to read much text very fast, and the only displays were really high resolution.

Integrating with Mac OS 10.4's dictionary from Java

Some time ago I wrote a little program to pretend to be ispell(1) but talk to NSSpellChecker so you could use Mac OS' native spelling checker. This let me imitate NSTextView's check-as-you-type in JTextArea, and in home-grown text components too.

Now Mac OS 10.4 offers us a dictionary and thesaurus. And I want them in my Java applications. There are three different interfaces, which I've already complained about here. The dashboard isn't important; I just don't see the point. The other two, the full-blown application and the little pop-up window that appears around the word in question, though: they're important and useful.

NSTextView and WebView both have new "Look Up in Dictionary" menu items, and there's a control-command-D keystroke. The menus have "Search in Spotlight" and "Search in Google" too, but they're trivial to implement in Java. "Look Up in Dictionary" isn't too hard, either, because you can use a dict: URI like this [definition; that link for Mac OS only]. The only slight bummer if you're using this is that if Dictionary.app isn't already running, your first open(1) or whatever will open a window that says "Type a word to look up in the dictionary and thesaurus... // Oxford American Dictionaries", and you'll need to open(1) again to actually look up the word. If Dictionary.app was already running, that will cause annoying flicker, because it's not smart enough to notice that it's already showing that word.

You can see Dictionary.app register that it handles these URIs if you look for CFBundleURLTypes in its Info.plist file. (John Gruber already mentioned this in Dictionary Look-Ups From BBEdit, Mailsmith, and TextWrangler, with more detail if you follow the link to his "Tiger Details report".)

In the same file, if you look for NSServices and you'll see it register a system service. Strangely, this code doesn't seem to work, though the commented-out line does:

NSArray* types = [NSArray arrayWithObject:NSStringPboardType];
NSPasteboard* pb = [NSPasteboard pasteboardWithUniqueName];
[pb declareTypes:types owner:nil];
[pb setString:@"blackberry" forType:NSStringPboardType];
BOOL success = NSPerformService(@"Look Up in Dictionary", pb);
//BOOL success = NSPerformService(@"Mail/Send To", pb);

But then I have very little Cocoa experience, sadly, so there could be some obvious idiocy there on my part.

The control-command-D keystroke is what's giving me headaches. If you're using AWT's TextArea, you're home free because it's an NSTextView, and that just works. JTextArea, though, doesn't work, and I'm not sure why not.

Here's what I've found out so far...

There are new private methods in AppKit:

93a24d20 t -[NSTextView _lookUpDefiniteRangeInDictionaryFromMenu:]
93a24d8c t -[NSTextView _lookUpIndefiniteRangeInDictionaryFromMenu:]
93a24858 t -[NSTextView _lookUpRangeInDictionary:]

But StandardKeyBinding.dict doesn't have a binding. If you run TextEdit in gdb(1) with suitable breakpoints, it looks like these methods are actually just responsible for the menu item:

(gdb) bt
#0 0x93a2486c in -[NSTextView _lookUpRangeInDictionary:] ()
#1 0x93a24d78 in -[NSTextView _lookUpDefiniteRangeInDictionaryFromMenu:] ()
#2 0x936bd274 in -[NSApplication sendAction:to:from:] ()
#3 0x93717a70 in -[NSMenu performActionForItemAtIndex:] ()
#4 0x937177f4 in -[NSCarbonMenuImpl performActionWithHighlightingForItemAtIndex:] ()
#5 0x937402fc in _NSPopUpCarbonMenu2 ()
#6 0x9373f93c in _NSPopUpCarbonMenu1 ()
#7 0x93797110 in -[NSCarbonMenuImpl _popUpContextMenu:withEvent:forView:withFont:] ()
#8 0x93796f90 in -[NSMenu _popUpContextMenu:withEvent:forView:withFont:] ()
#9 0x93677ec4 in -[NSWindow sendEvent:] ()
#10 0x9362113c in -[NSApplication sendEvent:] ()
#11 0x936185d0 in -[NSApplication run] ()
#12 0x93708e04 in NSApplicationMain ()
#13 0x000020e8 in ?? ()
#14 0x0000b298 in ?? ()

One clue was that the first time I tried a JTextArea, hitting control-command-D actually caused the program to freeze. Activity Monitor showed this:

Analysis of sampling pid 16781 every 10.000000 milliseconds
Call graph:
277 Thread_100f
277 0x1aec
277 0x1c4c
277 0x4f64
277 CFRunLoopRunSpecific
277 __CFRunLoopRun
277 __CFRunLoopDoSources0
277 __CFRunLoopPerformPerform
277 __NSFireMainThreadPerform
277 +[AWTStarter startAWT:]
277 -[NSApplication run]
277 -[NSApplication
nextEventMatchingMask:untilDate:inMode:dequeue:]
277 _DPSNextEvent
277 BlockUntilNextEventMatchingListInMode
277 ReceiveNextEventCommon
277 RunCurrentEventLoopInMode
277 CFRunLoopRunSpecific
277 __CFRunLoopRun
277 __CFRunLoopDoSource1
277 __CFMessagePortPerform
277 DSInitializeMessageReceiving
277 DSGetTextAttributes
277 DSAXGetTextAttributes
277 DSAXGetTextAttributes
277 objc_msgSend
277 objc_msgSend

Where "AX" is an Apple abbreviation (in lieu of namespaces/packages) for accessibility. I don't know what the "DS" is about.

There's a PopupDictDaemon.app tucked away inside Dictionary.app and sure enough, an nm(1) of the PopupDictDaemon shows lots of calls to accessibility functions, but also to a variety of Core Graphics' private functions (beginning CGS), including CGSSetSymbolicHotKey, which is presumably responsible for registering the keystroke. It probably then does a CGSFindWindowAndOwner and then uses accessibility functions such as AXUIElementCopyAttributeValue to find the word in question, its location, and its font. There are a bunch of calls to functions beginning DCM that appear to do the work of dictionary lookup, and then there's just the views you can see in its nib file to configure.

So why doesn't JTextArea work? I need to know that so I can make my own text components work. (And notice that Terminal doesn't work, either.) If you run Accessibility Inspector, and remember what I said about all the code in PopupDictDaemon to find the details of how the word in question looks (because the pop-up draws the word over itself with a different background, rather than just leaving the word as it is and popping up the definition below it), it starts to look clear: NSTextView offers the parameterized attributes AXRTFForRange, AXStyleRangeForIndex, and AXAttributedStringForRange in addition to those offered by JTextArea. Terminal.app also just offers JTextArea's basic style-less set. javax.accessibility.AccessibleText has a getCharacterAttribute method, which you'd imagine could be used to knock up the missing accessibility attributes, but perhaps there's some reason why this isn't happening. This is the first time I've so much as looked at the accessibility API. (I can't help but admire this as a cunning ploy by Apple to make developers and able users care about accessibility.)

By the way, if you're thinking you can fall back to handling the command-control-D keystroke with a call to open(1) with a dict: URI, you'd be wrong: PopupDictDaemon swallows the keystroke even if it does nothing with it. You never get to see it.

2005-05-07

Running the Weather widget outside dashboard

Create a new Cocoa application in Xcode, drag a WebView into your window, fight Interface Builder to create a new subclass of NSObject with an outlet "webView" connected to the WebView, and put this method in the generated .m file:

#import "WeatherController.h"

@implementation WeatherController

- (void) awakeFromNib {
[NSApp setApplicationIconImage:
[[NSImage alloc] initWithContentsOfURL:
[NSURL URLWithString:@"file:///Library/Widgets/Weather.wdgt/Icon.png"]]];
[[webView mainFrame] loadRequest:
[NSURLRequest requestWithURL:
[NSURL URLWithString:@"file:///Library/Widgets/Weather.wdgt/Weather.html"]]];
}

@end

The loadRequest: line I copied from the WebKit documentation; the other line I guessed. All use of Apple's widget (and its icon) is done by reference rather than by copy, so there's no way for Apple or AccuWeather to get upset.

To be able to run outside Xcode, you have to rebuild your project for "Deployment" rather than "Development". Weird.

2005-05-06

Differences in /usr/bin between Mac OS 10.3 and 10.4

alias(1), bg(1), cd(1), command(1), fc(1), fg(1), getopts(1), hash(1), jobs(1), read(1), type(1), ulimit(1), umask(1), unalias(1), wait(1) are all new shell scripts corresponding to the shell builtins and deferring to them:

#!/bin/sh
# $FreeBSD: src/usr.bin/alias/generic.sh,v 1.1 2002/07/16 22:16:03 wollman Exp $
# This file is in the public domain.
${0##*/} ${1+"$@"}

I don't know why.

Some CVS cruft (cvs-diff-branch, cvs-make-branch, cvs-merge-branch, cvs-revert, cvs-view-diffs) has been renamed (ocvs, ocvs-diff-branch, ocvs-make-branch, ocvs-merge-branch, ocvs-revert, ocvs-view-diffs).

No Mac OS release would be complete without a new bleeding-edge gcc. This one is no exception, the new files being c++-4.0, c++3, c++filt3 (what was c++filt in 10.3), c89, c99, cpp-4.0, cpp3, g++-4.0, g++3, gcc-4.0, gcc3, gcov-4.0, gcov3, powerpc-apple-darwin8-g++-4.0.0, powerpc-apple-darwin8-gcc-4.0.0.

Accompanying new binutils: ld64(1), otool64(1), update_prebinding_core(1).

There's an "AudioUnit validation tool": auval (no manual page).

They've renamed bspatch_apple as bspatch, but I'm still none the wiser about its purpose. There is no bsdiff (nor any man page).

Kerberos cruft: compile_et(1), krb5-config(1).

Scripting cruft: desdp(1), sdp(1).

"Directory Tool for testing Directory Services": dirt(1). "A tool for importing records in Open Directory": dsimport(1).

"Multicast DNS (mDNS) & DNS Service Discovery (DNS-SD) Test Tool": dns-sd(1).

"Convert a grap diagram into a cropped bitmap image": grap2graph(1).

SysV IPC utilities: ipcrm(1), ipcs(1).

Java: java-rmi.cgi, javaws(1).

Show/define locale settings: locale(1), localedef(1).

A link to mail(1): mailx(1). I don't know why.

Spotlight-related metadata utilities: mdcheckschema(1), mdfind(1), mdimport(1), mdls(1), mdutil(1).

An AppleShare mount utility: mnthome(1). Here's a nickel, PhotoShop boy; get yourself a proper network file system.

Yet another tty-based text editor: nano(1).

Previously missing POSIX stuff: csplit(1), getconf(1), nl(1), pathchk(1), readlink(1), stat(1), tabs(1).

Perl has gone from perl5.8.1 to perl5.8.6, and crc32, instmodsh, prove(1) and ptar(1) are new. Also wx-config and wxPerl.

Another part of samba: smbget(1).

A command-line interface to SQLite: sqlite3(1). iODBC cruft: iodbctest(1), iodbctestw(1).

A Mac OS utility for manipulating the system log: syslog(1).

"Utility to expand tabs and ensure consistent line endings": tab2space.

textutil(1) "can be used to manipulate text files of various formats, using the mechanisms provided by the Cocoa text system". Examples include converting RTF to HTML.

HTML tidy: tidy(1).

tcl cruft: wish(1), aka wish8.4.

Xgrid is installed by default: xgrid(1).

Command-line support for XSLT processing (from GNOME): xslt-config(1), xsltproc(1).

There's a bunch of extra ZIP utilities: funzip(1), unzipsfx(1), zegrep(1), zfgrep(1), zipcloak(1), zipgrep(1), zipinfo(1), zipnote(1), zipsplit(1), zless(1).

The other /usr/bin binaries in 10.3 gone in 10.4 are: CFInfoPlistConverter, ant (good riddance!), fixPrecomps, fpr, fsplit, fstat, gimpprint-config, odbctest, pawd, pdump, vmmap.

Differences in /bin between Mac OS 10.3 and 10.4

ksh(1) is new, as advertised as a 10.4 "feature" on Apple's web site. Here's a nickel, granddad; go rewrite your old scripts.

launchctl(1) is the user interface to launchd(8).

link(1) and unlink(1) are simplified versions of ln(1) and rm(1), from the Single Unix Specification version 2.

wait4path(1) waits for a "given path to show up in the namespace".

zsh-4.1.1 has been replaced by zsh-4.2.3.

2005-05-05

Dashboard

I know it's early days, but I don't like Dashboard. I noticed last night that, despite the new weather widget, I hadn't remembered to check the forecast in the morning. Why not? Because the dashboard is normally hidden.

Here are some of the things that annoy me:

  1. It's stupid that they're on a separate desktop and only there; why can't I choose? I'd rather have the weather displayed in the space currently used in the Dock by Dashboard itself, the dictionary already has more useful relatives in the Dictionary application and the pop-up, and if I need a calculator, I probably want to see it. Plus the Calculator application keeps shuffling towards being okay, and the Dashboard widget is like the bad old days. Which brings me to...

  2. Most of the widgets are useless crippled versions of full applications we already have. What's the point in that? Presumably Apple think there is some point, or they wouldn't have spent development effort on this. But I wish they'd explain it to me.

  3. Widgets should be updating themselves in preparation for me next looking at them. Surely part of the point of having quick access to them is so I don't waste time? In which case, I want to glance at them, see the latest information, and dismiss them again. I can't use them like that if I have to wait for them to update.

  4. The widgets are too US-centric. Want your local weather? May I recommend you relocate to California? Works great there. In fact, I could have left everything set to Cupertino (which is a retarded default when I've given my exact zip code as part of the registration process).

  5. Widgets look far too hard to write. There's a lot of duplicated code between them rather than a good widget-writing library. They use plenty of native code. A quick find(1) reports:

    • ./Address Book.wdgt/AddressBook.widgetplugin

    • ./Calendar.wdgt/Calendar.widgetplugin

    • ./Dictionary.wdgt/Dictionary.widgetplugin

    • ./iTunes.wdgt/iTunes.widgetplugin

    • ./Translation.wdgt/Translation.widgetplugin

    • ./Unit Converter.wdgt/ConverterPlugin.widgetplugin

    • ./World Clock.wdgt/WorldClock.widgetplugin


    And personally i think that the number of people with good Objective C and HTML and CSS skills is pretty limited. I don't believe, as some have claimed (presumably just because "it's HTML and JavaScript"), this is likely to let users write their own applications.


But, yes, the weather widget is beautiful. And accuweather was my favored source of weather data anyway. Just a pity there doesn't seem to be a link to their hourly forecasts, which are pretty good.

2005-05-03

Mac OS 10.4 / Java 1.5.0

After all the hype, it's relatively unexciting. Spotlight seems pretty cool, but it'll be a while before I properly appreciate it. At the moment, I'm just thinking it's nice that Mail won't have to re-index my mailboxes each time I log in, which always seemed a bit stupid.

Dashboard seems rather awkward. The weather thing's pretty, but I think I'd rather give it Dock space and have it pop up a window for the forecast. The other widgets seem pretty useless; the wrong way of solving the problems they address.

I've waited since the late 1980s and reviews of the NeXT cubes for a decent dictionary, and now Mac OS 10.4 gives me three at once. There's a dashboard widget, an application, and a too-small pop-up available via control-command-D. The control-command-D one could be the most useful, but I think they might have broken it by making it a keyboard command that ignores the location of the insertion point and uses the mouse location instead. Unless there's a selection, in which case it'll use that. That's just annoying. If I want the word the mouse is pointing at, I can control-click and choose "Look Up in Dictionary" (or "Search in Google", which is a good idea). This, of course, is for NSTextView. Not JTextArea or your own home-grown text component. I'll have to work out how to fix that.

make(1) is now GNU make 3.80, grep(1) is GNU grep 2.5.1, Jikes is 1.22, and Ruby is 1.8.2, so that's all good. ctags(1) is still the emacs one, not Exuberant Ctags (though you need a few patches to make that work well with Ruby or 1.5 Java source, and the maintainer's indisposed). CVS (which I still have to use for other people's projects) is up to 1.11.18, but there doesn't seem to be Subversion other than my own copy in /usr/local.

The POSIX grantpt(3), ptsname(3), and unlockpt(3) functions are now available. That will let us remove a hack from Terminator. Spotlight doesn't know about man pages, though, so it only knows which of my source files use grantpt (which is something, I suppose).

A click on Activity Viewer's Dock icon now opens the "Activity Monitor" window (it's like top(1) using a real table), which is one of the things I complained about with 10.3. Interesting that it's so obviously the right behavior that I never stopped clicking on the icon in the expectation that the window would open.

Mail still doesn't have forward as attachment. There don't appear to be an improvements to the quoting/wrapping situation. Its new UI isn't all that ugly, but the fact that it's so different to everything else is jarring. What were they thinking?

iCal's birthday calendar works (though you have to enable it in the preferences), so I can junk my Ruby script and cron(1) job. I wonder if there's an API for it, or it's just a special-case hack?

Java 1.5.0 is more sensitive to when you set system properties. I've had to move the setting of apple.laf.useScreenMenuBar into static initializers, for example. On the bright side, the annoying pause before the menu items appeared when you switched to a Java application is gone. That's now as fast as a native application. The Swing UIDefaults are still very wrong.

jps(1) works, but jstack(1) is broken (missing classes).

The main application I use seems to perform about the same as with 1.4.2, but starts up a little slower.

2005-05-01

iTunes Music Store

Tim Bray's said good things about the iTunes Music Store often enough, but it was last week before I finally gave in. I'd already used it a few times to listen to samples: it's much better than amazon, which seems to just have computer-generated samples of the first n seconds. The iTMS actually has chunks you'll recognize rather than intro. But I'd always ignored the nagging "please log in to download music" dialogs.

American radio forced me into it. They have this habit of playing the same thing at the same time every day. So on the way to tae kwon do, unless I was on the bus, I was getting to hear a lot of Semisonic's "Closing Time". I'd never heard of either band or song before, but I can see why it was so popular, even if it does mean they get called a one-hit wonder. And I'm sure it helped that their chorus seemed so apt on that journey.

Anyway, class time changed, and Semisonic's been replaced by Kelly Clarkson.

iTMS to the rescue.

It's pretty much as good as searching your own iTunes library, but it sucks that you have to hit return and get no feedback until then about how well your search is doing. If Google can do it (and not make $0.99 off it), surely Apple can? Mind you, I hated Google's implementation and never use it myself. But you know what I mean.

What's frustrating, though, is that this isn't a universal library of all music ever published. High on my list of stuff to get was stuff by Ayumi Hamasaki. I've heard a couple of trance tracks based on her songs, and I'm interested both in having copies of those and in hearing the original. (Assuming those aren't the originals.) But iTMS has never heard of her.

But it's still pretty cool. I like anything that involves instant gratification.