Tag Archives: objc

Adding a badge to the unified toolbar on Mac OS X

Mac OS X and the applications running on it are known for being sometimes unusual in the look and feel. For some reason developer on Mac OS X seems to be more creative than on other platforms. For an application developer which deals with user input it is important to make any user interaction as useful as possible and present this information in a way which is on the one side as much less annoying as possible but also attractive and informative on the other side. Lastly I was thinking about how to show the users they are using a beta version, which should remind them that this is not production ready software, but on the same time make it easy to respond to bugs they find in this pre-release. The solution, I came up with, was a badge in the toolbar which shows this is a release which is definitely in a testing phase, but also allows the user to double-click to give a respond to this particular version. In the following post I will show how to do this.

Getting the superview

In Cocoa all user displayed content is a NSView (most of the time; forget about the Dock). This is really nice, cause you could manipulate them. Although there are no public methods for getting the NSView of the toolbar or even the titlebar, they exist as an NSView. The NSView of the toolbar could be accessed by a private method. As always in Objective C you could ask for a particular method by using the respondsToSelector statement like in the following:

NSToolbar *tb = [pWindow toolbar];
if ([tb respondsToSelector:@selector(_toolbarView)])
{
 NSView *tbv = [tb performSelector:@selector(_toolbarView)];
 if (tbv)
 {
  /* do something with tbv */
 }
}

This return the NSView of the toolbar at least until 10.6. But be warned this is a private method and of course this could be changed in a future version of Mac OS X. At least the shown method will correctly fail in the case Apple change his mind which will result in doing nothing. Anyway this will not include the area of the titlebar. To get the NSView which covers both the title bar and the toolbar you need another trick. The titlebar usual has a close, minimize or maximize button. These buttons are NSButton’s and added by the system depending of the window type. As a NSButton is also a NSView it also has a parent. This dependency allows us to access the view which is responsible for displaying the unified titlebar and the toolbar. The following code shows how to get the responsible view and how to add an additional NSImageView to it:

NSView *wv = [[pWindow standardWindowButton:NSWindowCloseButton] superview];
if (wv)
{
 /* We have to calculate the size of the title bar for the center case. */
 NSSize s = [pImage size];
 NSSize s1 = [wv frame].size;
 NSSize s2 = [[pWindow contentView] frame].size;
 /* Correctly position the label. */
 NSImageView *iv = [[NSImageView alloc] initWithFrame:
        NSMakeRect(s1.width - s.width - (fCenter ? 10 : 0),
                   fCenter ? s2.height + (s1.height - s2.height - s.height) / 2 : s1.height - s.height - 1,
  	           s.width, s.height)];
 /* Configure the NSImageView for auto moving. */
 [iv setImage:pImage];
 [iv setAutoresizesSubviews:true];
 [iv setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin];
 /* Add it to the parent of the close button. */
 [wv addSubview:iv positioned:NSWindowBelow relativeTo:nil];
}

This code needs a NSWindow, a NSImage as label and the flag fCenter for deciding if the image is vertically centered within the titlebar. Also the badge is pinned on the right side by setting the AutoresizingMask. The following shows how this could be look like:

Adding functionality to the badge

First we should add some information about this release by using the setToolTip method of an NSView. Simply add this call to the code:

 [iv setToolTip:@"Some info about the beta version."];

To make this beta hint more useful, we should make it clickable. Getting mouse down events in Cocoa is only possible by handling the mouseDown event of the NSResponder sub-class. Also the view must accept first responder events. How to do this is shown in the following code:

@interface BetaImageView: NSImageView
{}
- (BOOL)acceptsFirstResponder;
- (void)mouseDown:(NSEvent *)pEvent;
@end
@implementation BetaImageView
- (BOOL)acceptsFirstResponder
{
 return YES;
}
- (void)mouseDown:(NSEvent *)pEvent
{
 if ([pEvent clickCount] > 1)
  [[NSWorkspace sharedWorkspace] openURL:
        [NSURL URLWithString:@"http://www.virtualbox.org/"]];
 else
  [super mouseDown:pEvent];
}
@end

Replace NSImageView with BetaViewImage in the allocation call in listing 2 and you are done. Change the URL to one where the user can respond to the beta questions, like a bug tracker or forum.

Conclusion

This post showed how to add an nice looking badge to the unified toolbar on Mac OS X. The user is reminded he is using some pre-release software all the time, but at the same time has an easy way to report problems with this release. Of course could such a badge used for anything else.

Creating file shortcuts on three different operation systems

As you may know, developing for multiple platforms is one of my strengths. Strictly speaking, it’s a basic requirement if you are involved in such a product like VirtualBox, which runs on every major (and several minor) platform available today. Beside the GUI, which uses Qt and therewith is portable without any additional cost (which isn’t fully true if you want real native look and feel on every platform, especially on Mac OS X), all the rest of VirtualBox is written in a portable way. This is done by using only C/C++ and Assembler when necessary. Everything which needs a different approach, because of the design of the OS (and the API’s which are available there), is implemented in a platform dependent way. In the history of VirtualBox, several modules are created and grown by the time, which makes it really easy to deal with this differences. For stuff like file handling, paths, strings, semaphores or any other basic functionality, you can just use the modules which are available. On the other side it might be necessary, for a new feature we implement, to write it from the ground. In the following post I will show how to create a file shortcut for the three major operation systems available today.

Why do you want to use file shortcuts

On the classical UNIX systems you have hard and soft links. These are implemented by the filesystem and make it possible to link to another file or folder without any trouble. Most of the time soft links are used, but it really depends on the use case. Unfortunately these kind of links are not available on Windows (yes, I know there are also hard links and junctions on NTFS, but they are not common and difficult to handle), these links doesn’t allow any additional attributes. For example one like to add a different icon to the link or provide more information through a comment field. Beside on Mac OS X, shortcuts can also be work as an application launcher, where the link contain the information what application should be started and how. In contrast to filesystem links which are handled by the operation system, these shortcuts are handled by the window system (or shell) running on the host (which doesn’t mean there is no filesystem support for it). On Windows this is the Explorer, on Mac OS X the Finder and on Linux a freedesktop.org conforming file manager.

Creating a Desktop file on Linux

Desktop files on Linux (or any other UNIX system which conforms to freedesktop.org) is easy. It’s a simple text file which implement the Desktop Entry Specification. In version 1.0 there are 18 possible entries, where not all of them are mandatory. In the following example I use Qt to write these files, but it should be no problem to use any other toolkit or plain C.

bool createShortcut(const QString &strSrcFile,
                    const QString &strDstPath,
                    const QString &strName)
{
 QFile link(strDstPath + QDir::separator() + strName + ".desktop");
 if (link.open(QFile::WriteOnly | QFile::Truncate))
 {
  QTextStream out(&link);
  out.setCodec("UTF-8");
  out << "[Desktop Entry]" << endl
      << "Encoding=UTF-8" << endl
      << "Version=1.0" << endl
      << "Type=Link" << endl
      << "Name=" << strName << endl
      << "URL=" << strSrcFile << endl
      << "Icon=icon-name" << endl;
  return true;
 }
 return false;
}

Replace icon-name by a registered icon on the system and you are done.

Creating a Shell link on Windows

Windows provides an interface for IShellLink since Windows XP. The following example shows how to use it:

bool createShortcut(LPCSTR lpszSrcFile,
                    LPCSTR lpszDstPath,
                    LPCSTR lpszName)
{
 IShellLink *pShl = NULL;
 IPersistFile *pPPF = NULL;
 HRESULT rc = CoCreateInstance(CLSID_ShellLink,
                               NULL,
                               CLSCTX_INPROC_SERVER,
                               IID_IShellLink,
                               (void**)(&pShl));
 if (FAILED(rc))
  return false;
 do
 {
  rc = pShl->SetPath(lpszSrcFile);
  if (FAILED(rc))
   break;
  rc = pShl->QueryInterface(IID_IPersistFile, (void**)&pPPF);
  if (FAILED(rc))
   break;
  WORD wsz[MAX_PATH];
  TCHAR path[MAX_PATH] = { 0 };
  lstrcat(path, lpszDstPath);
  lstrcat(path, "\");
  lstrcat(path, lpszName);
  lstrcat(path, ".lnk");
  MultiByteToWideChar(CP_ACP, 0, buf, -1, wsz, MAX_PATH);
  rc = pPPF->Save(wsz, TRUE);
 } while(0);
 if (pPPF)
  pPPF->Release();
 if (pShl)
  pShl->Release();
 return SUCCEEDED(rc);
}

As you may noticed this uses COM. Many API’s on Windows using the COM interface to communicate between processes. If you don’t use COM in your application you have to initialize it first. This is achieved by adding the following call to the front of the function:

 if (FAILED(CoInitialize(NULL))
  return false;

Depending on your application it might be worth to unitialize COM after usage by appending the following to the function:

 CoUninitialize();

The function itself isn’t any magic. It gets a COM interface to the IShellLink interface and then work with it, by setting the source path and adding a target path by using the IPersistFile interface. As I wrote before you could do much more. Providing a path to a specific application or adding your own parameters is no problem. Have a look at the documentation.

Creating an Alias file on Mac OS X

Shortcut files on Mac OS X are a little bit different. At first, they aren’t one. There are the classical filesystem links and Alias files. Alias files are links which targeting a specific file, but they haven’t all the possibilities of shortcuts like on Windows or Linux. As the name suggest they are really only an alias for another file or directory. So specifying an application to start or things like that aren’t possible. Anyway they allow changing the icon and they are more persistent than on Window or Linux cause they are working with several attributes of the target file. Even if you rename or move the target, an Alias file will resolve the target correctly (if it is possible). On the other side, being such special means also being hard to create. In principle there are two possibilities. The first one is, creating a file which is no file at all, but has several resources forks attached. Therefor you need to know exactly how Alias files are built of and make sure with every release of Mac OS X you are following the development. There is a free project which does exactly that: NDAlias. If you are like me and a little bit more lazy, you ask someone who should know how to create Alias files. This is Finder. Although writing the files itself isn’t easy, asking the Finder to do the job is not really easier, cause the information about doing exactly that are really rare. The following code shows how to achieve it:

bool createShortcut(NSString *pstrSrcFile,
                    NSString *pstrDstPath,
                    NSString *pstrName)
{
 /* First of all we need to figure out which process Id the Finder
  * currently has. */
 NSWorkspace *pWS = [NSWorkspace sharedWorkspace];
 NSArray *pApps = [pWS launchedApplications];
 bool fFFound = false;
 ProcessSerialNumber psn;
 for (NSDictionary *pDict in pApps)
 {
  if ([[pDict valueForKey:@"NSApplicationBundleIdentifier"]
         isEqualToString:@"com.apple.finder"])
  {
   psn.highLongOfPSN = [[pDict
                          valueForKey:@"NSApplicationProcessSerialNumberHigh"] intValue];
   psn.lowLongOfPSN  = [[pDict
                          valueForKey:@"NSApplicationProcessSerialNumberLow"] intValue];
   fFFound = true;
   break;
  }
 }
 if (!fFFound)
  return false;
 /* Now the event fun begins. */
 OSErr err = noErr;
 AliasHandle hSrcAlias = 0;
 AliasHandle hDstAlias = 0;
 do
 {
  /* Create a descriptor which contains the target psn. */
  NSAppleEventDescriptor *finderPSNDesc = [NSAppleEventDescriptor
                                            descriptorWithDescriptorType:typeProcessSerialNumber
                                            bytes:&psn
                                            length:sizeof(psn)];
  if (!finderPSNDesc)
   break;
  /* Create the Apple event descriptor which points to the Finder
   * target already. */
  NSAppleEventDescriptor *finderEventDesc = [NSAppleEventDescriptor
                                              appleEventWithEventClass:kAECoreSuite
                                              eventID:kAECreateElement
                                              argetDescriptor:finderPSNDesc
                                              returnID:kAutoGenerateReturnID
                                              transactionID:kAnyTransactionID];
  if (!finderEventDesc)
   break;
  /* Create and add an event type descriptor: Alias */
  NSAppleEventDescriptor *osTypeDesc = [NSAppleEventDescriptor descriptorWithTypeCode:typeAlias];
  if (!osTypeDesc)
   break;
  [finderEventDesc setParamDescriptor:osTypeDesc forKeyword:keyAEObjectClass];
  /* Now create the source Alias, which will be attached to the event. */
  err = FSNewAliasFromPath(nil, [pstrSrcFile fileSystemRepresentation], 0, &hSrcAlias, 0);
  if (err != noErr)
   break;
  char handleState;
  handleState = HGetState((Handle)hSrcAlias);
  HLock((Handle)hSrcAlias);
  NSAppleEventDescriptor *srcAliasDesc = [NSAppleEventDescriptor
                                           descriptorWithDescriptorType:typeAlias
                                           bytes:*hSrcAlias
                                           length:GetAliasSize(hSrcAlias)];
  if (!srcAliasDesc)
   break;
  [finderEventDesc setParamDescriptor:srcAliasDesc
    forKeyword:keyASPrepositionTo];
  HSetState((Handle)hSrcAlias, handleState);
  /* Next create the target Alias and attach it to the event. */
  err = FSNewAliasFromPath(nil, [pstrDstPath fileSystemRepresentation], 0, &hDstAlias, 0);
  if (err != noErr)
   break;
  handleState = HGetState((Handle)hDstAlias);
  HLock((Handle)hDstAlias);
  NSAppleEventDescriptor *dstAliasDesc = [NSAppleEventDescriptor
                                           descriptorWithDescriptorType:t ypeAlias
                                           bytes:*hDstAlias
                                           length:GetAliasSize(hDstAlias)];
  if (!dstAliasDesc)
   break;
  [finderEventDesc setParamDescriptor:dstAliasDesc
    forKeyword:keyAEInsertHere];
  HSetState((Handle)hDstAlias, handleState);
  /* Finally a property descriptor containing the target
   * Alias name. */
  NSAppleEventDescriptor *finderPropDesc = [NSAppleEventDescriptor recordDescriptor];
  if (!finderPropDesc)
   break;
  [finderPropDesc setDescriptor:[NSAppleEventDescriptor descriptorWithString:pstrName]
    forKeyword:keyAEName];
  [finderEventDesc setParamDescriptor:finderPropDesc forKeyword:keyAEPropData];
  /* Now send the event to the Finder. */
  err = AESend([finderEventDesc aeDesc],
               NULL,
               kAENoReply,
               kAENormalPriority,
               kNoTimeOut,
               0,
               nil);
 } while(0);
 /* Cleanup */
 if (hSrcAlias)
  DisposeHandle((Handle)hSrcAlias);
 if (hDstAlias)
  DisposeHandle((Handle)hDstAlias);
 return err == noErr ? true : false;
}

Although the code above looks a little bit scary, it does not much. It fetch the process serial number of the current Finder process, creates an Application event for creating an Alias file and send this event to the Finder.

Conclusion

Beside showing how to create file shortcuts on different platforms, this article also shows which work is necessary to create platform independent code. It’s a simple example. But it also makes clear that one simple solution for platform one, not necessarily mean it’s such simple on platform two.

Making this easy accessible to any developer is the next step. I will leave this exercise to the reader, but have a look at the platform code of the VirtualBox GUI and the corresponding Makefile.

Changing the default behavior of built-in Cocoa controls

Apple has many task specific controls built into Cocoa. They are all well designed and have most of the functionality a user and a developer expect. One of this controls is the NSSearchField. This control has a special design which allows the user to recognize the provided functionality with ease. It is so well-known that Apple uses the design even on there website. It has support for menus (e.g. for recent search items), auto completion, a cancel button, and so one. Although this is mostly feature complete, there are sometimes cases where you like to extend it. In this post, I will show how to add another visual hint to this control when a search term isn’t found. The aim is to change the background of the underlying text edit to become light red to visual mark the failed search.

Understanding how Cocoa works

The NSSearchField class inherits from an NSControl. NSControls are responsible for the interaction with the user. This implies displaying the content in a NSView, reacting to user input like mouse or keyboard events and sending actions to other objects in the case the status of the control has changed. Usually a control delegate the first two tasks to a NSCell. The main reasons for this are to persist on good performance even if there are many cells of the same type (like in the case of a table) and to be able to exchange the behavior of the control easily (like in the case of a combobox which also allow typing in a text field). With this information in mind we know that we need to overwrite the drawing routine of the cell (NSSearchFieldCell) to achieve our goal. The implementation is straight forward and shown in the following:

@interface MySearchFieldCell: NSSearchFieldCell
{
  NSColor *m_pBGColor;
}
- (void)setBackgroundColor:(NSColor*)pBGColor;
@end

@implementation MySearchFieldCell
-(id)init
{
  if (self = [super init])
    m_pBGColor = Nil;
  return self;
}
- (void)setBackgroundColor:(NSColor*)pBGColor
{
  if (m_pBGColor != pBGColor)
  {
    [m_pBGColor release];
    m_pBGColor = [pBGColor retain];
  }
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
  if (m_pBGColor != Nil)
  {
    [m_pBGColor setFill];
    NSRect frame = cellFrame;
    double radius = MIN(NSWidth(frame), NSHeight(frame)) / 2.0;
    [[NSBezierPath bezierPathWithRoundedRect:frame
          xRadius:radius yRadius:radius] fill];
  }
  [super drawInteriorWithFrame:cellFrame inView:controlView];
}
@end

The user can set a custom background color by using setBackgroundColor. Also it is possible to reset the background color by passing Nil to this method. The method drawInteriorWithFrame draws a rounded rectangle on the background and forwards the call to the super class afterward.

Replacing the cell class of a control

The next step is tell the control to use our own cell class and not the default one. Although there is a setCell method defined in NSControl it is not as easy as one might think. Creating an instance of MySearchFieldCell and passing it to setCell after the NSSearchField is created will not have the expected effect. The reason for this comes from the fact that the cell is initialized when the control is created. This includes setting all properties and targets for the actions. If one replaces the cell afterward these setting will be get lost. Later on, I will show a method how to keep this configuration, but for now we will start with an easier approach.

When the control creates a cell object it asks a static method for the class name to use. This method is called cellClass. If we overriding this method with our own one, we are able to return MySearchFieldCell. The following code demonstrates this:

@interface MySearchField: NSSearchField
{}
@end

@implementation MySearchField
+ (Class)cellClass
{
  return [MySearchFieldCell class];
}
@end

Now, if you use MySearchField instead of NSSearchField when creating search fields, you are done. Unfortunately this isn’t always possible. First you may not be able to inherit from NSSearchField for whatever reason and second this will not work when you are use the Interface Builder (IB) from Xcode. There you can’t easily use your own version of NSSearchField, but you have to stick with the original one. Before we proceed the obligatory screenshot:

The power of Archives

What we need is a method of setting our own cell class even when the control is already instantiated. In Cocoa it is possible to Archive and Serialize any object which implements the NSCoding protocol. Archiving means that the whole class hierarchy, with all properties and connections, is saved into a stream. Xcode makes heavy use of this in the nib file format where all the project data of the IB is written in. This alone doesn’t help us much, but additional to the archiving and unarchiving work, it is possible to replace classes in the decoding step. The relevant classes are NSKeyedArchiver and NSKeyedUnarchiver. NSKeyedUnarchiver has a method setClass:forClassName which allow this inline replacement and the following code shows how to use it:

NSSearchField *pSearch = [[NSSearchField alloc] init];
/* Replace the cell class used for the NSSearchField */
[NSKeyedArchiver setClassName:@"MySearchFieldCell"
      forClass:[NSSearchFieldCell class]];
[pSearch setCell:[NSKeyedUnarchiver
      unarchiveObjectWithData: [NSKeyedArchiver
        archivedDataWithRootObject:[pSearch cell]]]];
/* Get the original behavior back */
[NSKeyedArchiver setClassName:@"NSSearchFieldCell"
      forClass:[NSSearchFieldCell class]];

Basically this creates an archive of the current NSSearchFieldCell of the NSSearchField, which is instantly unarchived, but with the difference that the NSSearchFieldCell class is replaced by MySearchFieldCell. Because the archiving preserve all settings the new created class will have the same settings like the old one. The last call to NSKeyedArchiver will restore the default behavior.

Conclusion

This post should have removed some of the mysteries of the control and cell relationship in Cocoa. Additional to a simple derivation approach, a much more advanced way for setting the cell of a control was shown. This allows the replacement of any class hierarchy without loosing any runtime settings. If you need such replacements much more often or working with the IB, you should have a look at Mike’s post which shows a more generic way of the archive/unarchive trick.