Solipsism Gradient

Rainer Brockerhoff’s blog

Browsing Posts published in December, 2008

Klicko 1.0.1 (123)

No comments

Well… let’s hope this one will last more than a few hours…? icon_smile.gif

Cocoa musings pt.2

No comments

One of the nice things Objective-C inherited from C is the switch statement. If you have set different tags in all your menu items, you often find you can do things like:

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   switch ([menuItem action]) {
   case 1:
      ...
   case 2:
      ...
   }
   return YES;
}

Those tags have to be set in Interface Builder, and you probably will have an enum somewhere to use symbolic constants instead. However, the standard C switch() is restricted to integer values; you can’t switch on strings or selectors. So, if you don’t want tags and need to switch on the menu item’s selectors, you’ll have to do:


- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   SEL action = [menuItem action];
   if (action==@selector(firstAction:)) {
      ...
   } else if (action==@selector(secondAction:)) {
      ...
   } else...
   return YES;
}

Bearable if you have two, or even four or five cases, but what if there are dozens? Or suppose you have to compare strings instead of selectors:

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   NSString* title = [menuItem title];
   if ([title isEqualToString:@"firstTitle"]) {
      ...
   } else if ([title isEqualToString:@"secondTitle"]) {
      ...
   } else...
   return YES;
}

Not that it’s recommendable, in practice, to compare menu item titles, but it’s a good example.
Well, there are other ways to make this more readable, or even more efficient. But here’s one neat way to convert a group of strings into integers for use in a switch(). First, let’s write an utility C function to do so:

NSUInteger CaseCodeForString(NSString* string) {
   static NSArray* array = nil;
   if (!array) {
      array = [[NSArray alloc] initWithObjects:
               @"zeroth string",
               @"first string",
               @"second string",
               @"third string",
               ...
            nil];
   }
   return [array indexOfObject:string];
}

Note the standard lazy allocate-once trick of declaring array static, initialize it to nil, and test before using. Anyway, this function will return 0 if you feed it @”zeroth string”, 1 for @”first string” and so forth… and return NSNotFound if the string isn’t in the array. So you could, in our last example, do:

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   switch (CaseCodeForString([menuItem title])) {
   case 0:
      ...
   case 1:
      ...
   }
   return YES;
}

If there are many strings, this will be faster than a series of isEqualToString: calls; this is because NSArray uses a hash table to find the index of a particular object, and only goes into the actual string comparison if the two string’s -hash methods return the same value.

Pushed out 119, then literally a second later got a bug report, had to do 120. Sorry about that.

Re: Cocoa musings pt.1

No comments

In my post on event taps, I mentioned the following code to get a global event tap:

   CFMachPortRef tapg = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

This taps the event stream at the “annotated session” point (hence the kCGAnnotatedSessionEventTap parameter). Basically, this means that the event has already been analyzed as to destination application, and you can ask the passed event directly for that application’s process ID. This is was I was using in Klicko.

Unfortunately the docs don’t mention another step that takes place while “annotating” a mouse-down event: apparently, if the click was on a window’s title bar (not elsewhere in the window), the owning process is brought to the front before the tap sees the event! Klicko’s processing varied depending on the clicked-on window’s and the owning process’ state, so I was seeing different results depending on the click location – title bar or inside the window. Worse, a workaround I needed to do to bring non-main windows to the front for background processes wouldn’t be applied at all if the user clicked on a title bar!

A more subtle consequence was that I was intercepting (and discarding) mouse clicks, while the system was expecting the click that brought the process to the front to actually arrive at the process… the result was that, while the menu bar changed to the process, its windows would remain in the back.

After beating my head against this wall for several days, I was rereading the CGEvent docs again for ideas and noticed that I hadn’t tried applying a global tap before event annotation:

   CFMachPortRef tapg = CGEventTapCreate(kCGSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

Note the kCGSessionEventTap parameter. This meant that I had to discover the owning application’s process ID myself (instead of asking the mouse-down event directly). Premature optimization strikes again; I’d selected the annotated tap just to avoid doing these steps.

At any rate, I immediately discovered that I now could properly intercept clicks anywhere, discard them, and do process and window activation as I wanted to. Well, almost; there was still some redundant window activation to do, as Carbon and Cocoa apps seem to still have some residual differences in that regard. I’ll be filing bugs at Apple about some of these things.

This latest build of Klicko fixes all known bugs and edge cases, and some memory leaks. Hopefully this will be the last build for a long time.

In my next post I’ll explain some technical details.

Travel updates

No comments

Oops. I just realized I forgot to update my travel maps. Here’s the current world map (44 countries visited):

and here’s zooming in on Europe (28 countries visited):

Just saw Rob Keniger’s Wrapster plug-in for Coda, an interesting use of event taps; it’s a plug-in that intercepts key events for its master application.

The version I looked at (1.2) uses a global tap and checks if its master application is active:

   if(![NSApp isActive]) {
      return event;
   }

Of course, it would be conceptually more reliable to obtain the event’s destination process PSN and test against that; but the best way would be to tap the current process like this:

   ProcessSerialNumber psn = {kNoProcess, kCurrentProcess};
   CFMachPortRef tapg = CGEventTapCreateForPSN(&psn, kCGTailAppendEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), ProcessEvent, NULL);

Global event taps are a great tool, but they’re also easy to abuse, and I suppose that in a year or two we might have dozens of utilities or plug-ins tapping events; the actual results might be dependent on the order they run or load, for instance, and overall responsiveness might suffer. Therefore, please make absolutely sure that you need a global tap to do whatever you want to do, that it takes as little time as possible, and that it does careful parameter checking.

One great application to test event taps (and to check their presence) is Event Tap Testbench. Its author, Bill Cheeseman, has helped me a lot with subtle details of both event taps and the Accessibility APIs; thanks Bill! Do also check out their other products.

Rob tests if Accessibility is enabled by trying to create a tap and seeing if it succeeds. While this works, I think it’s easier to do it like this:

   if (!AXAPIEnabled()&&!AXIsProcessTrusted()) {
      // error dialog here
   }

The first call checks if Accessibility is enabled in System Preferences; the second checks if your process is allowed to set a key tap even with Accessibility disabled. To achieve this, you must (from a process running as root) call AXMakeProcessTrusted() on the tapping application’s executable – this will take effect on its next run.

In the case of Klicko, it taps only mouse button events, for which Accessibility doesn’t need to be on; however, it uses the Accessibility APIs to check out details about the clicked-on windows. Some people dislike turning Accessibility on in System Preferences, so a future version of Klicko might use AXMakeProcessTrusted() to avoid this; this means asking the user for an Administrator password and running a separate tool, like Quay does. I’m still evaluating the trade-offs for that: it precludes running Klicko from ~/Applications or from the disk image, adds to the application size, and the user has to navigate an extra dialog on first run.

Update:Klicko now uses AXMakeProcessTrusted().

Found an error in the code signature for both Klicko and Quay, induced by the presence of a newer signing certificate for iPhone apps.

So I pushed out new builds for Quay 1.1.1 (285) and Klicko 1.0.1 (103). Klicko also has other small improvements; see the release notes.

Photos licensed by Creative Commons license. Unless otherwise noted, content © 2002-2024 by Rainer Brockerhoff. Iravan child theme by Rainer Brockerhoff, based on Arjuna-X, a WordPress Theme by SRS Solutions. jQuery UI based on Aristo.