Solipsism Gradient

Rainer Brockerhoff’s blog

Browsing Posts tagged RBSplitView

Well, Xcode 4.1 is out of beta. Please note that the “GM” build previously posted on the developer site was NOT the released version, which is now available for free on the Mac App Store. (Open the “Welcome to Xcode” window, it should say “Version 4.1 (4B110)”.)

I’d promised to several people at WWDC to investigate how Xcode 4.1’s lack of support for IB plugins would work out for RBSplitView, and now I can finally post the results here. Unfortunately the news is not good.

Compiling RBSplitView’s “Sample App” target works well under Xcode 4.1 with just a few changes to modernize build settings sand avoid new warnings. The problem comes when trying to open the .nib file. First, the release notes say you should do this in Terminal:

	defaults write com.apple.InterfaceBuilder3 "IBKnownPluginPaths.3.2.7"
		-dict-add "net.brockerhoff.RBSplitView.IBPlugin"
		"/Users/<username>/<path-to-RBSplitView.ibplugin>"


which I did.

Opening the nib file then offers to remove the dependency on the plug-in:

and going ahead lists several warnings and issues.

The way this works is interesting. All RBSplitViews and RBSplitSubviews are converted to NSCustomViews – meaning that they’re stored as plain NSViews in the nib file, -initWithFrame: is called on unarchiving (instead of -initWithCoder:); the view’s class is reset to RBSplitView or RBSplitSubview, as the case may be, and all custom attributes are then set through key-value coding.

I suppose this would work quite well for less complex views, but it didn’t work out of the box for RBSplitView. First of all, this KVC stuff was quite new-fangled when I wrote it, and I didn’t see any need to use it until I had to update the RBSplitView.ibplugin for Xcode 3.1. At the time, I simply wrote some KVC methods for the ibplugin additions, mainly to simplify setting all those attributes from inside Interface Builder… it all worked fine.

Converting the nib file generates a lot of exceptions as the KVC methods just aren’t there in the framework code. I tried a quick fix, copying and pasting them from the plugin code, but that didn’t work out too well: RBSplitView doesn’t like being reincarnated from the nib file piecewise like that, and it seems that the attributes get set too late or in the wrong order.

I suppose some fiddling with the copied methods will fix that, but it’ll be at best a stop-gap measure. The converted nib file no longer adjusts the RBSplitSubviews properly and it’d be too easy to make a big mess of it, should you try to change anything in there. Even so, I’ll try to make some time available to get this working.

Apple says Xcode 3.2.6 is “unsupported” under Lion. If you already have it installed in a separate folder when you upgrade it will mostly continue to work, but I found that running Interface Builder 3 crashes when you have the RBSplitView plugin open. Probably the best bet, for now, is running Snow Leopard Server in a virtual machine and installing Xcode 3.2.6 in there.

RBSplitViewFixer

No comments

Just published a droplet application that fixes the spelling error in nibs produced by my previous (beta) version of the RBSplitView Interface Builder plugin. Details on the RBSplitView page.

Re: Developments

No comments

On the stopover at Hongkong; amazing place. I bought a new camera today (the Panasonic Lumix DMC-FX65) and it seems reasonably improved from my previous FX35. Tomorrow we go off again southwards, first to Manila (weather permitting).

In other news, RBSplitView 1.2 is up. I’m still not satisfied with some aspects, but it should now work OK with Snow Leopard, 64-bit, garbage collection, etc.

Re: Developments

No comments

In Taipei (Taiwan) on a very brief stop at an Internet Cafe. Everything’s fine as usual, we had much luck with the weather and Kobe/Kyoto, Tokyo and Okinawa were excellent. Japan will be a place to come back to with more leisure in the future.

RBSplitView and its Interface Builder plugin are practically finished and I’m now working on the new Quay. Stay tuned for updates; I’ll try to publish a beta version of RBSplitView for 64 bits/Snow Leopard when I stop at Hongkong tomorrow.

More soon…

Re: Developments

No comments

The rest of the Shanghai tours were very impressive, visited an old town on the river near Shanghai – an Asian Venice. Then the Jade Buddha Temple, the Planning Exhibition for next year’s World Expo, and a visit to the town’s tallest building; nearly 500m!

Boarding the Costa Classica went well and today we visited Nagasaki, a beautiful and historic town. Next, Kobe/Kyoto and Tokyo…

In other news, I’ve got RBSplitView working in 64-bit mode, also (as far as I can tell) with Garbage Collection enabled. Now I’m working on the updated IBPlugin, which is a little more complex than I thought. Stay tuned.

Cocoa musings pt.1

No comments

One of the reasons for my taking a week or two off to work mostly on the just-released Klicko was that I like to rework, and group together, code snippets that worked well for me in earlier applications, and see if I can update them to conform to my slowly growing experience. I’m also prone to digress; one such digression (several months!) resulted in the release of the unexpectedly popular RBSplitView.

Both Klicko and Quay use code that, like RBSplitView, were destined for XRay II, the supposed successor of XRay, which sadly is not reliable under Leopard. Alas, at this writing, it sounds like XRay II will remain in the freezer, its mummy mined for code snippets and general philosophical experience… but the basic idea persists, and something quite equivalent (but also quite different in detail) is already being conceived.

Both Quay and Klicko do part of their seeming magic with a technology called “Quarz Event Taps” (PDF file). This was introduced in Tiger, and perfected in Leopard. Briefly, an event tap is a C callback routine that is called to filter low-level user input events at some points in the system’s event processing, which is actually quite complex. Events can be generated, examined, modified or even suppressed before they’re delivered to an application. Since user input events are usually routed to the foreground window (that is, to the foreground application, even if it has no window), this makes event taps quite powerful.

You can make a global event tap, or a per-process tap. Quay sets up a tap on the Dock process to intercept clicks on Dock icons. Klicko uses a global tap to check for clicks on background windows.

Tapping one application is, in principle, easy: you locate the application to be tapped by its PSN (Process Serial Number), set up the tap, tie it to your application’s main run loop, and that’s it. Here’s what a bare-bones implementation would look like:

// This is the callback routine, called for every tapped event.

CGEventRef ProcessEvent(CGEventTapProxy tapProxy, CGEventType type, CGEventRef event, void *refcon) {
   switch (type) {
      case kCGEventLeftMouseDown:
         // process a mouse down
         break;
      case kCGEventLeftMouseUp:
         // process a mouse down
         break;
   }
   return event;   // return the tapped event (might have been modified, or set to NULL)
               // returning NULL means the event isn't passed forward
}

// Here's how you set up the tap: we're catching mouse-down and mouse-up

...
   ProcessSerialNumber psn;
   // get the PSN for the app to be tapped; usually with the routines in <Processes.h>
...
   CFMachPortRef tapg = CGEventTapCreateForPSN(&psn, kCGTailAppendEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);
   if (!tapg) {   // bail out if the tap couldn't be created
      NSLog(@"application tap failed");
      [NSApp terminate:nil];
   }
   CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tapg, 0);
   if (!source) {   // bail out if the run loop source couldn't be created
      NSLog(@"runloop source failed");
      [NSApp terminate:nil];
   }
   CFRelease(tapg);   // can release the tap here as the source will retain it; see below, however
   CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
   CFRelease(source);  // can release the source here as the run loop will retain it

After that, all should work – in principle. The devil is in the details. Here’s how you locate a running application by its application ID and return its PSN:

BOOL GetPSNForApplicationID(NSString* appid, ProcessSerialNumber* outPSN) {
   outPSN.highLongOfPSN = outPSN.lowLongOfPSN = kNoProcess;
   while (GetNextProcess(outPSN)==noErr) {
      NSDictionary* pdict = [(NSDictionary*)ProcessInformationCopyDictionary(&psn,
         kProcessDictionaryIncludeAllInformationMask) autorelease];
      if ([[pdict stringForKey:(id)kCFBundleIdentifierKey] isEqualToString:appid]) {
         return YES;
      }
   }
   return NO;
}

To make a global tap, you don’t need a PSN. Just use the following tap creation call instead:

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

More details. If you’re tapping an application, it may not be running; CGEventTapCreateForPSN will return NULL in that case. Or it may quit while you have the tap set up. You probably want to monitor that process and either quit, or rerun the application, or wait for it to come back up. In the latter cases, you’ll have to back out of the now-dead tap carefully:

   CFMachPortInvalidate(tapg);
   CFRunLoopRemoveSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
   CFRelease(tapg);   // this CFRelease has been moved from before the CFRunLoopAddSource

supposing, of course, that you have held on to those two variables. Note how the CFRelease(tapg) should, in such a case, happen only after the source has been removed from the run loop; otherwise invalidating the tap will cause the run loop to crash. You can use the same technique to close a global event tap, though usually there’s no need; if your app crashes or quits, the tap will be closed automatically.

However, there’s a serious problem while debugging an event tap. If you’re tapping a single application, and set a breakpoint inside yours (or break into the debugger anywhere because of a crash or exception), both applications will stop. If the same happens while a global tap is active, the entire system stops accepting user input! The only way to recover is to ssh/telnet in from another machine, and kill Xcode. So even if you prefer NSLog/printf calls to breakpoints, this will be very inconvenient for all but the simplest callback code.

The solution I found was to always use an application tap while debugging. An easy way is to define, as I always do, a special macro inside the main project build configuration panel (but for the debug configuration only): inside the “Preprocessor Macros Not Used In Precompiled Headers” (aka GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS) write “DEBUG”, and then, instead of the global tap, compile in an application tap on some always-present application (like the Finder) by using #ifdef DEBUG/#else/#endif statements.

Even that isn’t always sufficient, as Xcode 3 notoriously may invoke the debugger (even on your release build!) if your app crashes. You must either get used to never clicking on “Build & Go” for your release build, or you must make a runtime check for the debugger. The latter will prevent inadvertent freezes, but if you forget to take it out before deployment, your application will behave oddly if a curious user runs it under a debugger.

This post is already too long, so I’ll talk only briefly about what you can do inside the event tap callback itself. Every possible execution path should be short and contain no long loops or wait points. If you’re just watching events, always return the same event passed in as parameter. Return NULL if you want to suppress an event; however, be careful to suppress entire event groups. For instance, if you decide to suppress a mouse-down, store the event number and also suppress subsequent mouse-dragged and mouse-up events with the same number; otherwise the destination application may behave oddly. Some apps may behave oddly when tapped, by the way.

Update: I previously said here that to intercept or generate keyboard events, your application must run with setgid 0 (the “wheel” group). I was mistaken; my apologies. Your application must run setuid root to make an event tap at the third tap point (which I didn’t mention here), which is where the events enter the window server (kCGHIDEventTap).

After several months of tinkering and getting used to the new IB, I just published a first beta of the IB3 plugin for RBSplitView 1.1.4.

Some things haven’t been tested (copy&paste), others don’t work fully (undo), but it seems to work mostly. Please post bug reports in the source forum, or e-mail me. To install, close IB3, unzip the plugin in a convenient location (I don’t think there’s a standard one for IB3), and double-click it.

A full Leopard version will be out around the end of this month, along with source code for the plugin.

We’re back from a 10-day vacation in São Paulo and surroundings. Good to get away, better to get back. I was offline most of the time and am still catching up on news and e-mail.

Had less time to work underway than I anticipated but I made progress on the Leopard version of RBSplitView – including the Interface Builder plugin – and had some new ideas about other things too.

The new iMacs look very nice. Wonder if the lower prices will be reflected here, though.

The iPhone update makes me almost sure Apple isn’t using the TrustZone at all. Deliberately, of course. Makes one wonder what is behind that…

…more as I catch up.

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