Associating a file type with a FMX/OS X application (3) – implementing the delegate

[NB: this is the third and final post of a short series – see here for part 1 and here for part 2.]

Having made the required file type associations in the Info.plist file, the final thing to do is implement a custom delegate for NSApplication. Since NSApplication is an Objective-C class, a delegate for it needs to be an Objective-C object, and as such, we need to use XE2’s Delphi to Objective-C bridge, and more specifically, derive a class from TOCLocal. If you have been following this blog, you will know I’ve covered the latter before (it is also covered in my book).

So, starting from where we left off last time, add a new unit to the TextFileViewer project, and save it as CustomNSApplicationDelegate.pas. Then, add the following to the interface section of the new unit:

uses
  Macapi.ObjectiveC, Macapi.CoreFoundation, Macapi.CocoaTypes, Macapi.AppKit;

type
  NSApplicationDelegate2 = interface(NSApplicationDelegate)
    ['{BE9AEDB7-80AC-49B1-8921-F226CC9310F4}']
    function application(theApplication: Pointer;
      openFile: CFStringRef): Boolean; cdecl;
  end;

  TOpenFileEvent = reference to procedure (
    const AFileName: string);

  TNSApplicationDelegate2 = class(TOCLocal,
    NSApplicationDelegate2)
  private
    FDefaultDelegate:
    FOnOpenFile: TOpenFileEvent;
  public
    constructor Create(const AOnOpenFile: TOpenFileEvent);
    procedure applicationDidFinishLaunching(
      Notification: Pointer); cdecl;
    procedure applicationWillTerminate(
      Notification: Pointer); cdecl;
    function application(theApplication: Pointer;
      openFile: CFStringRef): Boolean; cdecl;
  end;

procedure InstallApplicationDelegate2(
  const AOnOpenFile: TOpenFileEvent);

Here, we firstly extend the stock interface mapping of the NSApplicationDelegate protocol (a protocol being the Objective-C equivalent of a Delphi interface type), before declaring a class to implement it. That we need to do the former is because a protocol can have one or more members marked as ‘optional’ in Objective-C. Since Delphi interfaces have no equivalent feature, the stock protocol to interface mappings in XE2 (and presumably, XE3) declare the minimum number of methods possible (in many cases, that means no methods at all!). In order to implement more, you therefore need to derive a custom interface with those additional methods included.

That said, writing the implementing code is not too tricky:

uses FMX.Forms;

var
  Delegate: NSApplicationDelegate2;

procedure InstallApplicationDelegate2(
  const AOnOpenFile: TOpenFileEvent);
var
  NSApp: NSApplication;
begin
  NSApp := TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication);
  Delegate := TNSApplicationDelegate2.Create(AOnOpenFile);
  NSApp.setDelegate(Delegate);
end;

constructor TNSApplicationDelegate2.Create(const AOnOpenFile: TOpenFileEvent);
begin
  inherited Create;
  FOnOpenFile := AOnOpenFile;
end;

function TNSApplicationDelegate2.application(
  theApplication: Pointer; openFile: CFStringRef): Boolean;
var
  Range: CFRange;
  S: string;
begin
  Result := Assigned(FOnOpenFile);
  if not Result then Exit;
  Range.location := 0;
  Range.length := CFStringGetLength(openFile);
  SetLength(S, Range.length);
  CFStringGetCharacters(openFile, Range, PChar(S));
  try
    FOnOpenFile(S);
  except
    FMX.Forms.Application.HandleException(ExceptObject);
    Result := False;
  end;
end;

procedure TNSApplicationDelegate2.applicationDidFinishLaunching(
  Notification: Pointer);
begin
  { The default FMX delegate doesn't do anything
    here, so nor will we. }
end;

procedure TNSApplicationDelegate2.applicationWillTerminate(
  Notification: Pointer);
begin
  FMX.Forms.Application.Free;
  FMX.Forms.Application := nil;
end;

The implementation of the applicationDidFinishLaunching and applicationWillTerminate methods just copies what the default FMX delegate (TNSApplicationDelegate) does. Ideally we would be able to derive our TNSApplicationDelegate2 from it, but alas, TNSApplicationDelegate is buried inside the implementation section of its unit, like almost all platform-specific classes in FMX, at least in XE2.

That said, to put the code to work, add CustomNSApplicationDelegate to the uses clause of the main form’s unit, before inserting the following line at the top of the form’s OnCreate handler:

  InstallApplicationDelegate2(OpenFile);

Rerun the application, and you should now be able to open *.tfvdoc files by double clicking them in Finder, and moreover, open plain text files of any description by dragging and dropping them onto the application’s Dock icon.

For the full project source, check out here to view the code (including the all-important custom Info.plist, which I’ve commented), or copy and paste this URL to download it via SVN.

Advertisements

2 thoughts on “Associating a file type with a FMX/OS X application (3) – implementing the delegate

  1. Hello, Just wanted to let you know that the NSApplicationDelegate listed in this post is not complete. This code does not compile under Delphi XE7.

    • Hi Matthew – that’s a bit of a pain. The underlying reason is that the Objective-C protocol that the NSApplicationDelegate interface in Delphi represents contains a number of only optionally-implemented methods. Since a Delphi interface can only declare mandatory methods, Embarcadero typically start by declaring such an interface with nothing (or next to nothing), then as new FMX versions come along, add in methods as they come to be implemented by the default FMX implementation. The actual Objective-C type never changes however – it’s just how it’s being represented in Delphi.

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