Using the Cocoa toolbar (NSToolbar) in XE2

Following my previous post that demonstrated how to write a simple Mac PDF viewer, I got a request for showing how to use the Cocoa toolbar control in a FireMonkey application. When used, the Cocoa toolbar is the button bar that is fixed to the top of a window, and in recent versions of OS X, is visually (and functionally) integrated with the title bar.

Unlike the PDF case, which involved calling a straight C API, creating and using a standard toolbar in OS X means negotiating with an Objective-C interface. As such, the necessary code is a bit less straightforward, since you need to go through the Delphi to Objective-C bridge, a feature whose official documentation is a few comments in the source code! Nonetheless, the bridge is something I cover in my book, partly via the example of using the NSAlert Objective-C class in its ‘modal sheet’ mode. As the case of the Cocoa toolbar (NSToolbar) is similar, I’ll try not to repeat myself – please check out the book if you need more background information.

To cut to the chase, I’ve put up a small demo project here (the SVN URL is http://delphi-foundations.googlecode.com/svn/trunk/XE2 book/13. Native APIs/Mac specific/Cocoa delegate example (NSToolbar)). When you run it, you will be presented with an application that looks like this:

This shows a standard Cocoa toolbar with a button for the Open command, a button for the Copy command, a ‘flexible’ spacer, and a button for an Info command – a flexible spacer in this context causes the remaining item(s) to be right-aligned. Each of the buttons has an image, caption and tooltip; the Copy button also enables and disables itself as text becomes selected and deselected in the memo control underneath.

In terms of actual code, setting up a Cocoa toolbar takes three steps (see here for more details):

  1. A delegate object must be designed and created.
  2. An NSToolbar instance itself must be created, and assigned the delegate.
  3. The NSToolbar must be assigned to the form.

Step 1 is obviously the biggest one, particularly so in XE2 given the object in question must be an Objective-C object that receives Objective-C ‘messages’. To implement it, you need to derive a class from TOCLocal, which is itself declared in the Macapi.ObjectiveC unit. The derived class must then implement a Delphi interface descended from NSObject that represents the interface of the Objective-C class. Here’s how the Delphi interface and class looks like in my demo:

{$M+}
type
  INSToolbarDelegate = interface(NSObject)
    ['{17797346-6D03-46C1-982D-6840549F78BD}']
    //required methods
    function toolbarAllowedItemIdentifiers(toolbar: Pointer): NSArray; cdecl;
    function toolbarDefaultItemIdentifiers(toolbar: Pointer): NSArray; cdecl;
    function toolbar(toolbar: Pointer; itemForItemIdentifier: CFStringRef;
      willBeInsertedIntoToolbar: Boolean): NSToolbarItem; cdecl;
    //optional method
    function validateToolbarItem(theItem: NSToolbarItem): Boolean; cdecl;
    //custom method
    procedure ItemClicked(Sender: NSToolbarItem); cdecl;
  end;

  TNSToolbarDelegate = class(TOCLocal, NSToolbarDelegate)
  private
    FClickEventSelector: SEL;
    FOwner: TNSToolbarHelper;
  protected
    function GetObjectiveCClass: PTypeInfo; override;
  public
    constructor Create(AOwner: TNSToolbarHelper); reintroduce;
    destructor Destroy; override;
    function toolbarAllowedItemIdentifiers(toolbar: Pointer): NSArray; cdecl;
    function toolbarDefaultItemIdentifiers(toolbar: Pointer): NSArray; cdecl;
    function toolbar(toolbar: Pointer; itemForItemIdentifier: CFStringRef;
      willBeInsertedIntoToolbar: Boolean): NSToolbarItem; cdecl;
    function validateToolbarItem(theItem: NSToolbarItem): Boolean; cdecl;
    procedure ItemClicked(Sender: NSToolbarItem); cdecl;
  end;
{$M-}

Since I declare the two types in the implementation section of the unit, the $M+ ensures RTTI is generated, which is a requirement for the Delphi to Objective-C bridge to work. Notice the TNSToolbarDelegate class doesn’t formally implement the INSToolbarDelegate interface however (in case you’re wondering, there isn’t a typo – NSToolbarDelegate is just a dummy interface defined in Macapi.AppKit). This is because any interface formally implemented by a TOCLocal descendant represents an Objective-C ‘protocol’ implemented by the Objective-C class. INSToolbarDelegate isn’t a protocol however – rather, it is the class, in a sense.

Of the methods themselves, the first three must be implemented by any application that uses an NSToolbar not set up in InterfaceBuilder/Xcode. Here, toolbarAllowedItemIdentifiers specifies a set of possible buttons in the form of an NSArray of textual identifiers, toolbarDefaulttemIdentifiers a set of standard items in another NSArray, and toolbar (actually toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar: in Objective-C speak) is called when an NSToolbarItem needs to be created for a given button.

Of the remaining methods our delegate implements, validateToolbarItem is a standard one for changing a button’s enabled state dynamically (in VCL/TAction terms, it is equivalent to handling the OnUpdate event), and ItemClicked is a custom method that we will set up to handle the ‘action’ message (equivalent to handling the OnExecute or OnClick event in the VCL).

Unlike the others, the action message is something more like a Delphi event, in that anything can handle it using any method of the appropriate signature. Instead of a pointer to a method though, a ‘selector’ (pointer to a special string that identifies the method) must be used. This makes it prudent for the delegate object to handle the action message, since you can’t just pass an ordinary method or procedural pointer for a selector. Instead, we must get a reference to our custom ItemClicked method by calling the Objective-C runtime’s sel_getUid function. Further, to correctly pass a reference to the Objective-C object our TOCClass embodies, we must pass not Self (which is a reference to the Delphi object), but the result of GetObjectID:

uses Macapi.ObjCRuntime;

function TNSToolbarDelegate.toolbar(toolbar: Pointer;
  itemForItemIdentifier: CFStringRef; willBeInsertedIntoToolbar: Boolean): NSToolbarItem;
begin
  //...
  Result.setTarget(GetObjectID);
  Result.setAction(sel_getUid('ItemClicked:'));
end;

Note the all-important closing colon in the sel_getUid string!

Alongside the delegate, my demo also implements a wrapper for the other bits and bobs associated with setting up an NSToolbar – check out the source for details. In use, it works like this:

procedure TfrmNSToolbarDemo.FormCreate(Sender: TObject);
var
  Item: TNSToolbarHelperItem;
begin
  itmFileOpen.ShortCut := scCommand or Ord('O');
  { Both toolbar and toolbar items have textual identifiers in the Cocoa 
    system, and my wrapper code reflects that. }
  FHelper := TNSToolbarHelper.Create('DocumentToolbar');
  //open command - use the corresponding menu item's OnClick handler
  Item := FHelper.AddItem('FileOpen');
  Item.SetCaptionImageAndHint('Open', 'Open.png', 'Open file');
  Item.OnClick := itmFileOpenClick;
  //copy command - ensure the button is enabled and disabled as appropriate
  Item := FHelper.AddItem('EditCopy');
  Item.SetCaptionImageAndHint('Copy', 'Copy.png', 'Copy selected text');
  Item.OnClick :=
    procedure (Sender: TObject)
    begin
      memEditor.CopyToClipboard;
    end;
  Item.OnUpdate :=
    procedure (Sender: TNSToolbarHelperItem; var EnableItem: Boolean)
    begin
      EnableItem := (memEditor.SelLength > 0);
    end;
  //add a flexible spacer to right align the remaining button
  FHelper.AddFlexibleSpaceItem;
  //add a button onto the end of the toolbar
  Item := FHelper.AddItem('Info');
  Item.SetCaptionImageAndHint('Info', 'Info.png', 'App information');
  Item.OnClick :=
    procedure (Sender: TObject)
    begin
      MessageDlg('FireMonkey NSToolbar Demo',
        TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0);
    end;
  //configure the toolbar appropriately, and attach it to the form
  FHelper.Toolbar.setSizeMode(NSToolbarSizeModeSmall);
  FHelper.Attach(Self);
end;

The button glyphs are simply PNG files added to the application ‘bundle’ via Project|Deployment in the IDE (when adding new items in the Deployment tab, you will need to select ‘All configurations – OS X platform’ from the combo box at the top first, and having added a file, change its output folder from Contents\MacOS to Contents\Resources). The implementation of the SetCaptionImageAndHint method itself is very trivial, since the Cocoa framework looks up the image file for you:

procedure TNSToolbarHelperItem.SetCaptionImageAndHint(const ACaption,
  AImageFileName, AHint: string);
var
  NSCaption: NSString;
  Obj: NSToolbarItem;
begin
  NSCaption := NSSTR(ACaption);
  Obj := GetCocoaObject;
  Obj.setLabel(NSCaption);
  Obj.setPaletteLabel(NSCaption);
  Obj.setImage(TNSImage.Wrap(TNSImage.OCClass.imageNamed(NSSTR(AImageFileName))));
  Obj.setToolTip(NSSTR(AHint));
end;

Unfortunately, one thing I have not been able to get working is the built-in toolbar customisation dialog. More exactly, imagine adding these two lines just before FHelper.Attach(Self) in the code above:

  FHelper.Toolbar.setAllowsUserCustomization(True);
  FHelper.Toolbar.setAutosavesConfiguration(True);

Re-run the demo, and you now get a popup menu for the toolbar with items for changing the size of its images, switching to a text- or image-only style, hiding the toolbar, and customising the buttons shown. All but the last option work as expected, with changes auto-saved by the Cocoa framework. Alas, but any attempt to show the customisation ‘palette’ causes the application to hang! Everything else works fine though.

Advertisements

12 thoughts on “Using the Cocoa toolbar (NSToolbar) in XE2

  1. Hi!
    Quite interesting your series of “how to use the MAC APIs” in Delphi XE2. But I’m still facing two problems, which both of them has been avoided to talk in the Embarcadero forums and in the blogs I usually follow.
    One of them is ¿how call a method which is named like a reserved word in Delphi? I.e. look at the NSNetService interface and you will find that the method “function type: NSString; cdecl;” is missing.
    The other one is ¿how implement methods with the same name and parameters, but they differ only in the parameter’s names? I.e. look at the NSNetServiceBrowserDelegate. Four methods have the same name, and, by pairs, they have the same parameters’ types changing only the names of them, so it is not possible (I think) from Delphi to implement ALL of them.

    Thank you, and continue with your very good job giving a lot of missing documentation!!!

    Best regards,
    Ruben

    • Hi Ruben

      With respect to your first question, does not using the normal escape character (i.e., an ampersand) work?

      function &type: NSString; cdecl;

      The fact the standard import types don’t make use of this feature doesn’t say much – nor does the COM type library importer, and that’s been around for donkey’s years!

      As for your second question, that is a topic I briefly cover in my book ;-). The workaround, in a nutshell, is to declare overloads using a pointer type (e.g. CFStringRef or even just Pointer for NSString). This is because the Delphi to Objective-C bridge only creates wrapper interfaces if you tell it too – use a pointer type for an Objective-C object parameter, and the value will just be received ‘raw’. If you need the parameter wrapped in order to call a method, just wrap it manually using TNSSomeType.Wrap.

      • Ahh ok!! I didn’t even know that the & had a special function (or even any function…) in Delphi.
        I tried both of your solutions, and they worked awesome!!
        Thank you very much!

        Best regards,
        Ruben

  2. Thanks for the demo Chris. I think it’s not possible to insert Firemonkey objects in the toolbar. Or is it? I use a design time toolbar for Windows compilation, and in OSx side, I kill it and recreate an NSToolbar with your code and add corresponding items manually.
    And a second note: It would be really great if you can show us how to insert search bar and segmented control to the toolbar.

    • As you may have read elsewhere, an NSToolbarItem can host an NSView. Unfortunately, because a FMX control is not an NSView under the surface, the problem is the general one of how to allow a FMX control to be hosted by an NSView instead of a FireMonkey form.

  3. Hi, i tried to use the NSToolbar as Tabsheet replacement. As far as i understood is that i have to set the item which has been selected via setSelectedItemIdentifier(NSSTR(‘itemName’)); It does not work 😦

    • The delegate object needs to implement toolbarSelectableItemIdentifiers as well. If you are using my helper code, add this method to both NSToolbarDelegate and TNSToolbarDelegate, modelling it on toolbarAllowedItemIdentifiers.

  4. Hi!
    I’ve used your example – thanx a lot!.
    But I’m a bit confused about image loading: You load an image via filename. Unfortunatelly, deployment an image file and loading it as “image.png” or “Contents\MacOS\image.png” doesn’t work. What’s the correct way to load image (as resource) into the toolbar ?

      • Yes, it works! Thank You.

        Unfortunately, there is no way to add images etc. to Contents\Resources except of manually editing of *.dproj file, am I right? Project-Deployment does not allow me to do this.

        • Oh dear – what version of Delphi are you using? I confess I don’t have the latest at present to check, though it was certainly OK in earlier versions.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s