Search code examples
macosfiremonkey

Associate my app with its custom file type in OSX and open file on double click


My FMX app (MyApp, say) uses its own custom file type to store data files. Let's say these files have extension *.myext.

I have managed to set up the info.plist so that OSX registers MyApp as the owner of file type *.myext.

If MyApp is not yet open, it opens when double-clicking a file with that extension. Of course, the file does not open because I haven't written any code to handle the event because I don't know how to detect in OSX that the event has occurred.

If MyApp is already open, I get, I get a message 'The document “xxxx.myext” could not be opened. [MyApp] cannot open files in the “[MyApp file]” format.'

So my question is how MyApp can know that a file has been double-clicked so that it can launch a file open procedure?


Solution

  • I answered my own question eventually. Three authors had each given fairly similar units containing most of the necessary code.

    Chris Rolliston: https://delphihaven.wordpress.com/2012/08/14/associating-a-file-type-on-osx-part3/

    Victor Fedorenkov: https://pastebin.com/r4y6KmWz

    Remy Lebeau: https://forums.embarcadero.com/thread.jspa?messageID=934522

    None of the three units could be used without modification in Delphi Tokyo 10.2, no doubt due to changes that have taken place in Firemonkey since the posts were written.

    I took parts of each of the three versions and made various edits to arrive at the unit below, which works in Delphi 10.2 Update 3:

    unit NSApplicationOpenFileDelegateUnit.Mac;
    
    interface
    
    type
    
      TOpenFileEvent = reference to procedure(const AFileName: string);
    
    procedure InstallApplicationDelegate2(const AOnOpenFile: TOpenFileEvent);
    
    implementation
    
    uses
      System.SysUtils, System.RTLConsts, System.Messaging, System.Classes,
      Macapi.ObjectiveC, Macapi.CoreFoundation, Macapi.CocoaTypes, Macapi.AppKit,
      Macapi.Foundation, FMX.Forms,
      Macapi.ObjCRuntime,
      FMX.Platform, FMX.Platform.Mac, FMX.Helpers.Mac;
    
    type
      NSApplicationDelegate2 = interface(NSApplicationDelegate)
        ['{BE9AEDB7-80AC-49B1-8921-F226CC9310F4}']
        function application(theApplication: Pointer; openFile: CFStringRef)
          : Boolean; cdecl;
      end;
    
      TNSApplicationDelegate2 = class(TOCLocal, NSApplicationDelegate2)
      private
        FOnOpenFile: TOpenFileEvent;
      public
        constructor Create(const AOnOpenFile: TOpenFileEvent);
        procedure applicationDidFinishLaunching(Notification
          : NSNotification); cdecl;
        procedure applicationDidHide(Notification: NSNotification); cdecl;
        procedure applicationDidUnhide(Notification: NSNotification); cdecl;
        function applicationShouldTerminate(Notification: NSNotification)
          : NSInteger; cdecl;
        function applicationDockMenu(sender: NSApplication): NSMenu; cdecl;
        procedure applicationWillTerminate(Notification: NSNotification); cdecl;
        function application(theApplication: Pointer; openFile: CFStringRef)
          : Boolean; cdecl;
      end;
    
    constructor TNSApplicationDelegate2.Create(const AOnOpenFile: TOpenFileEvent);
    begin
      inherited Create;
      FOnOpenFile := AOnOpenFile;
    end;
    
    procedure TNSApplicationDelegate2.applicationDidFinishLaunching
      (Notification: NSNotification); cdecl;
    begin
      // Seems we have to have this method even though it is empty.
    end;
    
    procedure TNSApplicationDelegate2.applicationDidHide(Notification
      : NSNotification); cdecl;
    begin
      // Seems we have to have this method even though it is empty.
    end;
    
    procedure TNSApplicationDelegate2.applicationDidUnhide
      (Notification: NSNotification); cdecl;
    begin
      // Seems we have to have this method even though it is empty.
    end;
    
    function TNSApplicationDelegate2.applicationShouldTerminate
      (Notification: NSNotification): NSInteger; cdecl;
    begin
      Result := NSTerminateNow;
    end;
    
    function TNSApplicationDelegate2.applicationDockMenu(sender: NSApplication)
      : NSMenu; cdecl;
    begin
      Result := nil;
    end;
    
    procedure TNSApplicationDelegate2.applicationWillTerminate
      (Notification: NSNotification); cdecl;
    begin
      FreeAndNil(FMX.Forms.application);
    end;
    
    function TNSApplicationDelegate2.application(theApplication: Pointer;
      openFile: CFStringRef): Boolean; cdecl;
    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
        on E: Exception do
        begin
          FMX.Forms.application.HandleException(E);
          Result := False;
        end;
      end;
    end;
    
    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;
    
    end.
    

    To use the unit, include this line in the main form's FormCreate:

    InstallApplicationDelegate2(OpenFile);
    

    and also include a file open procedure

    procedure TMyMainForm.OpenFile(const AFileName: String);
    begin
    // Application-specific code to open the file
    end;
    

    Double-clicking a file in Finder now opens the file up in the application.

    The above presupposes that your app and your proprietary file extension have already been associated via the info.plist file. The following is an info.plist to associate file extension ".myext" with an application MyApp.

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
    http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>CFBundleName</key>
        <string>My App Full Name</string>
        <key>CFBundleDisplayName</key>
        <string>My App Full Name</string>
        <key>CFBundleIdentifier</key>
        <string>com.mycompany.MyAppName</string>
        <key>CFBundleVersion</key>
        <string>1.0.6662</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleAllowMixedLocalizations</key>
        <string>YES</string>
        <key>CFBundleExecutable</key>
        <string>MyAppName</string>
        <key>NSHighResolutionCapable</key>
        <string>true</string>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.productivity</string>
        <key>NSLocationAlwaysUsageDescription</key>
        <string>The reason for accessing the location information of the 
        user</string>
        <key>NSLocationWhenInUseUsageDescription</key>
        <string>The reason for accessing the location information of the 
        user</string>
        <key>NSContactsUsageDescription</key>
        <string>The reason for accessing the contacts</string>
        <key>CFBundleShortVersionString</key>
        <string>1.0.0</string>
    
        <key>CFBundleIconFile</key>
        <string>MyAppName.icns</string>
        <key>CFBundleSupportedPlatforms</key>
        <array>
            <string>MacOSX</string>
        </array>
    
    <key>CFBundleDocumentTypes</key>
        <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>LSItemContentTypes</key>
            <array>
                <string>com.mycompany.MyAppName.myext</string>
            </array>
            <key>LSHandlerRank</key>
            <string>Owner</string>
            <key>CFBundleTypeIconFile</key>
            <string>MyAppName.icns</string>
        </dict>
        </array>
        <key>UTExportedTypeDeclarations</key>
        <array>
        <dict>
            <key>UTTypeIdentifier</key>
            <string>com.mycompany.MyAppName.myext</string>
            <key>UTTypeTagSpecification</key>
            <dict>
                <key>public.filename-extension</key>
                <array>
                    <string>myext</string>
                </array>
            </dict>
            <key>UTTypeConformsTo</key>
            <array>
                <string>public.data</string>
            </array>
            <key>UTTypeDescription</key>
            <string>My App Full Name document</string>
            <key>UTTypeIconFile</key>
            <string>MyAppName.icns</string>
        </dict>
        </array>
    </dict>
    </plist>