Creating a PDF: FMX and OS X in sweet harmony

[NB: this post was written in ignorance of the interface changes involved in the soon-to-be-released XE3. Unfortunately, these changes broke the hack outlined below. However, the demo linked to has been updated to work in both XE2 and XE3 RTM. I shan’t say more than that, since who knows what any XE3 update will bring, let alone XE4…]

In a previous post I walked through how to create a simple PDF viewer in Delphi XE2 using the OS X API. Recently, I got a follow up question about using the API to create a PDF, and more particularly, create a PDF by drawing to a FMX TCanvas.

In principle this shouldn’t be too hard to do, since OS X allows you to write a PDF using exactly the same drawing functions used to draw to the screen or a printer. In the jargon of the API, those functions work against a refrence to an arbirary ‘graphics context’ (CGContextRef), just like how the drawing functions of the Windows API work against a handle to an arbitrary ‘device context’ (HDC). And, just as a VCL TCanvas is essentially a wrapper round a native HDC, so a FMX/Mac TCanvas is essentially a wrapper round a native CGContextRef. All we need do, then, is create a standalone TCanvas and assign its Handle property appropriately. Simples! Continue reading

Advertisements

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.

Associating a file type with a FMX/OS X application (2) – making the associations

In my previous post, I gave a conceptual overview of what it takes to associate a file type on OS X. The key points were the following:

  • File registrations are made locally in the application’s Info.plist file, not to a central registry. (A central registry – the ‘launch services’ database – still exists of course, it’s just that you don’t write to it directly.)
  • A Mac GUI application, whether written using FMX or something else, must adopt some sort of non-SDI approach. This means when the user opens two or more files in the same application, they all share the same instance of the program. In an SDI model, in contrast, each open file has its own instance.
  • The previous point rules out receiving file names on the command line. This is no different to Windows to the extent a ParamStr(1) approach assumes an SDI model. However, whereas on Windows a non-SDI approach means manually implementing single instancing and forwarding ‘open file’ messages to the original instance, on the Mac, these two things are done for you by the system.
  • This still leaves actually receiving ‘open file’ messages. To do that, you must provide a ‘delegate object’ for NSApplication that includes an ‘application:openFile:’ method.

In the present post, I will put these points into practice in the context of an XE2/FMX application. The result will be a simple text file viewer that lists opened files down the side, TextWrangler 4.x-style: Continue reading

Associating a file type with a FMX/OS X application (1) – background

[This is part 1 of a three part series. Part 2 is here and part 3 here.]

Say you want to associate a file type with a Windows application you are writing. Doing so takes two basic steps:

  1. Adding registration keys to the Windows Registry.
  2. When the application starts up, having it check to see whether a file name has been passed on the command line, and then take appropriate action when one has – if ParamCount > 0 then …

The first step is a bit tedious, though not difficult once you know what keys to write. Moreover, if an installation program is being used, then the installer should be able do it for you. As for the second step, the level of difficulty depends on whether the program is implemented using an SDI (Single Document Interface) model or not – an ‘SDI’ application being one in which each file the user opens has its own instance of the application.

If SDI is used, then you just need to read off the value of ParamStr(1) at start up and get on with it. If SDI isn’t used though, you will need to employ (a) some sort of mechanism (typically a mutex) to maintain only one running instance of the application and (b) another mechanism (e.g. the Windows messaging system) to notify the existing instance when the user attempts to open another file. If talk of mutexes and Windows messages sounds gibberish to you, it is something I cover in my book – check out the accompanying demo if you just want to see some code.

That said, the main topic of this post and its sequel is associating a file type on OS X, and on the Mac, things are a little different:

  1. OS X does not have a central registry like Windows that applications (or application installers) explicitly write to. Instead, each GUI program has a ‘plist’ (property list) file, roughly equivalent to a manifest on Windows, that (optionally) includes file type association information. The OS then periodically parses the plist files of applications on the hard drive, saving relevant settings in a private cache.
  2. A non-SDI approach of some sort is basically compulsory for a Mac GUI application, be it using Mac-style MDI (Multiple Document Interface, like TextEdit) or some for of TDI (Tab Document Interface, like a web browser). Nonetheless, compared to non-SDI scenarios on Windows, on the Mac the system implements for you the two mechanisms I mentioned earlier, of ensuring only a single instance of the application is ever running, and sending an ‘open file’ message to the existing instance when the user tries to open another file.

You may notice that I’ve used the term ‘message’ in both Windows and Mac contexts. Some may object that this elides how ‘messages’ in a Windows API sense and ‘messages’ in an OS X one are fundamentally different things. On one level that objection has merit – in particular, Windows messages involve a straight-C API directly callable by Delphi code, whereas messages on OS X concern method calls against Objective-C objects that aren’t directly controllable by Delphi code. However, the fact the fact the word ‘message’ is used in both worlds is no accident – ‘messages’ in the OS X sense are just a higher level realisation of the ‘messaging’ concept that is also implemented by the Windows API.

That said, at a practical level, OS X routes the ‘open file’ message through the NSApplication object. NSApplication is the Cocoa equivalent to the TApplication class in the VCL or FMX, and just like TApplication, you neither subclass nor explicitly instantiate it – instead, the Cocoa framework creates a singleton instance of NSApplication for you. Because of that, the ‘open file’ message effectively becomes an event of NSApplication, and handling this message means handling an event. Sounds easy!

Unlike Delphi however, events in Cocoa tend to be surfaced not as things any old object can handle, but as potential methods on a dedicated ‘delegate’ object. The practical problem of how to handle the ‘open file’ (or more exactly, application:openFile:) message therefore comes to this: we need to create a delegate object that implements a method for the application:openFile: message. As to how we actually go about doing that however, I’ve save until next time

A FMX TClipboard and TMacPreferencesIniFile implementation

On first experimenting with FMX and OS X back last September/October, I wrote both a FMX TClipboard implementation and a TCustomIniFile descendant that delegates to the Mac Preferences API. The latter serves as a rough analogue of TRegistryIniFile on Windows; as for the former, I wrote it because the stock FMX clipboard support is crap. If anyone is interested, the source is available on Google Code here (the SVN URL is http://delphi-foundations.googlecode.com/svn/trunk/FMX%20Utilities/). Suffice to say, I don’t claim any particular originality (another FMX TClipboard implementation is floating about here for example), however both classes were written without reference to alternative implementations.

TMacPreferencesIniFile (CCR.MacPrefsIniFile.pas)

In terms of approach, my Mac Preferences wrapper uses the native typing as appropriate when writing, but string-based auto-conversions as required on read. To that extent, it behaves similarly to TRegistryIniFile.

In itself, the Mac Preferences API only directly supports a single level of settings – in other words, a preferences file (‘plist’) is just a flat list of keys and values. This is a problem for any TCustomIniFile descendant that wraps it, since the TCustomIniFile interface assumes at least two levels (‘sections’, then keys-and-values). In order to create a hierarchy, the Apple-promoted workaround is to write a CFDictionary (or NSDictionary, if using the Objective-C interface) for a ‘value’. However, this is a rather impractical solution due to dubious API decisions on Apple’s part – in order to change a single value more than one level down, a CFDictionary-based approach requires you to load the whole branch, copy it to a ‘mutable’ CFDictionary, make the amendment, then rewrite the whole branch again.

Instead of that, I have adopted the approach taken by both TextWrangler and Microsoft Word, which is to fake a preference hierarchy by using a delimiter character embedded in the ‘real’ key names. By default a colon is used, like TextWrangler (Word uses a backslash), though you can change this by setting the Delimiter property. If you write a value for the ‘Width’ key under the ‘General’ section, then, what actually happens is that a plist key called ‘General:Width’ is set.

Other than that, all of the TCustomIniFile interface is supported, with native implementations of ReadBinaryStream and WriteBinaryStream too. Lastly, since the Preferences API requires writes to be explicitly flushed, so does my wrapper, in the form of a call to the UpdateFile method. This is just like TMemIniFile, which requires you call UpdateFile to affect any changes too.

For more info about TMacPreferencesIniFile, check out either the source and/or a small demo I’ve written for it: Continue reading

Minor update to the eBook version of Delphi XE2 Foundations

Just a quick ‘heads up’ to say a (very) minor revision of the Kindle edition of my book, Delphi XE2 Foundations, is now available. This corrects some typos in the original version, but as nothing substantive has changed, it isn’t necessary to update. Further, if you have bought one of the eBook parts this month (July), it is probable you already have the corrected version. This is because it has actually been out for a few weeks – I’ve just been waiting for Amazon to make it available to people who bought a part previously.

If at the start of the eBook the copyright page says ‘Updated 30 June 2012’, then you have the most recent version (the printed version won’t, since it had the corrections in question at the outset). Otherwise, you can upgrade by going to the ‘Manage Your Kindle’ page on Amazon’s website; an ‘update available’ link should then be visible:

Having chosen the option to update, the book should re-download when you next open your app or device. For myself, I found the book ‘disappeared’ in the Mac app after it had finished being re-downloaded, however simply closing and reopening the app fixed that. Also, please be aware that a side effect of upgrading will may be the loss of any annotations or notes made in the original version.

As an aside, various Kindle apps and devices have themselves had software or firmware updates from Amazon in the past couple of months. I would recommend upgrading to anyone affected. Of the Kindle apps for Windows and OS X, v1.9.x was better than v1.8.x, and v.10.x (current at my time of writing) finally supports the entire CSS subset supported by the Kindle Fire (under the hood, an eBook is composed of HTML and CSS, like a webpage). For example, here’s a page from my book in v1.8.1 of the desktop app (OS X, but Windows was similar):

And here’s the exact same page in v1.10.0:

The desktop apps supposedly auto-update, but I haven’t found that to be the case – I’ve needed to manually download and reinstall.

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. Continue reading