Making the never-ending sorry tale of FMX menus a bit happier

For me, FireMonkey at present has three main showstopping limitations: an inability to draw text that is neither wonky (XP/Vista) nor overly ‘sharp’ (W7/W8) on Windows; a TMemo that isn’t fit for purpose on any platform; and a menu implementation that is poor on Windows, and hopelessly broken on OS X. Well, perhaps not quite ‘hopelessly’ – while the only proper solution would be to untangle the source and use native menus throughout, in the meantime, I have found some workarounds for specific issues: in particular, three concerning TMainMenu (= the FMX interface for the native menu bar) and two concerning TPopupMenu.

Problem: the Mac menu bar is owned by the application, but FMX thinks it should be owned by a form

In a traditional Mac application whose raison d’être is to view or edit individual files, it is possible for the application to be running yet with no windows open, in which case you just have the application’s menu bar showing when you Cmd+Tab to it. Alas, but TMainMenu cannot fathom such a thing, since it thinks it must always be attached in spirit to a specific form even if it is not attached visually, as it would be on Windows. Put a TMainMenu on a data module, then, and nothing happens.

To get around this, define an interposer class for TMainMenu that firstly overrides RecreateOSMenu to do nothing, and secondly provides an explicit Activate method:

type
  TMainMenu = class(FMX.Menus.TMainMenu, IItemsContainer)
  protected
    procedure RecreateOSMenu; override;
  public
    procedure Activate;
  end;

//...

procedure TMainMenu.Activate;
begin
  IFMXMenuService(TPlatformServices.Current.GetPlatformService(
    IFMXMenuService)).CreateOSMenu(Application.MainForm, Self);
end;

procedure TMainMenu.RecreateOSMenu;
begin
  { do nothing - Activate method now controls this }
end;

You still need to provide a target form for Windows, but the CreateOSMenu code on OS X just ignores it (at least for now!). Anyhow, with the Activate method defined, you can now put a TMainMenu on an auto-created data module and (if you want that) only have the menu appear when running on OS X (since a menu bar will be there anyway).

Problem: when a menu item’s state is changed (e.g., its Enabled or Visible property is toggled), the FMX main menu code rebuilds the whole menu

This is just a complete WTF, but anyhow, sanity can be found in providing an alternative implementation of the IFMXMenuService interface, in harness with an interposer class for TMenuItem. For the details, see here.

Problem: if an item on a main menu has been assigned an action that controls its core properties (Enabled, Visible, Text, etc.) and the action doesn’t get updated independently, the action’s OnUpdate handler will never get called

To see this problem in action, do this:

1. Create a new FMX HD application, add a TEdit and TButton to the form, followed by a TActionList and a TMainMenu.

2. Add an action called actCopy to the action list, set its Text property to ‘Copy’, and handle its OnUpdate and OnExecute functions like this:

procedure TForm1.actCopyExecute(Sender: TObject);
begin
  (Focused.GetObject as TEdit).CopyToClipboard;
end;

procedure TForm1.actCopyUpdate(Sender: TObject);
var
  Obj: TFmxObject;
begin
  if Focused = nil then
    actCopy.Enabled := False
  else
  begin
    Obj := Focused.GetObject;
    actCopy.Enabled := (Obj is TCustomEdit) and
      (TCustomEdit(Obj).SelLength > 0);
  end;
end;

3. Add an top level item to the main menu, and a second level item under it which then gets assigned actCopy to its Action property. If you are using the TMainMenu interposer class discussed previously, add a call to MainMenu1.Activate in a handler for the form’s OnCreate event.

4. Run the application; notice that when the menu is pulled down, the Copy item is enabled regardless of whether the TEdit has the focus and has text selected.

Now, because I’m only interested in TMainMenu on OS X, the fix I’m about to suggest is Mac-specific. The principle should be similar on Windows though. Secondly, this fix presupposes the previous one, i.e. the alternative IFMXMenuService implementation and TMenuItem interposer class that doesn’t rebuild everything from scratch just because a single Enabled property has changed. If you cause the main menu to be completely recreated when you are handling one of its events, then surprise surprise, the OS doesn’t like it, and will in fact terminate the application there and then in the OS X case.

Anyhow, those caveats aside, take a copy of FMX.Platform.Mac.pas, add the copy to the project, and make the following edits to it:

  • Find the NewNSMenu method declaration, and change the parameter from Text: string to Source: TMenuItem.
  • Amend all calls to NewNSMenu accordingly (i.e., pass in the menu item itself, not the menu item’s text). There’s one call you won’t be able to adjust for, so comment it out and use this code instead:
//LNewMenu := NewNsMenu(FMXAppName);
LNewMenu := TNSMenu.Wrap(TNSMenu.Alloc.initWithTitle(NSSTR(FMXAppName)));
  • Where FMXOSMenuItem and TFMXOSMenuItem are defined, add the following interface and class on similar lines:
  FMXOSMenu = interface(NSMenu)
  ['{E0653081-A641-4220-B7D3-2F1CF87E071E}']
    procedure menuNeedsUpdate(menu: Pointer); cdecl;
  end;

  TFMXOSMenu = class(TOCLocal, NSMenuDelegate)
  strict private
    FSource: TMenuItem;
  public
    constructor Create(const ASource: TMenuItem);
    destructor Destroy; override;
    function GetObjectiveCClass: PTypeInfo; override;
    procedure menuNeedsUpdate(menu: Pointer); cdecl;
  end;

constructor TFMXOSMenu.Create(const ASource: TMenuItem);
begin
  inherited Create;
  FSource := ASource;
  UpdateObjectID(NSMenu(Super).initWithTitle(NSSTR(DelAmp(ASource.Text))));
  NSMenu(Super).setDelegate(Self);
end;

destructor TFMXOSMenu.Destroy;
begin
  NSMenuItem(Super).Release;
  inherited;
end;

function TFMXOSMenu.GetObjectiveCClass: PTypeInfo;
begin
  Result := TypeInfo(FMXOSMenu);
end;

procedure TFMXOSMenu.menuNeedsUpdate(menu: Pointer); cdecl;
var
  I: Integer;
  Item: TMenuItem;
begin
  try
    for I := FSource.ItemsCount - 1 downto 0 do
    begin
      Item := FSource.Items[I];
      if Item.Action <> nil then
        Item.Action.Update;
    end;
  except //an unhandled exception here will kill the program
    Application.HandleException(ExceptObject);
  end;
end;
  • Amend the implementation of NewNSMenu itself to look like this:
function TPlatformCocoa.NewNSMenu(const Source: TMenuItem): NSMenu;
var
  AutoReleasePool: NSAutoreleasePool;
begin
  AutoReleasePool := TNSAutoreleasePool.Create;
  try
    Result := NSMenu(TFMXOSMenu.Create(Source).Super);
    FNSHandles.Add(AllocHandle(Result));
  finally
    AutoReleasePool.release;
  end;
end;
  • After ensuring OS X is the target platform, re-run the application: the ‘Copy’ menu item should now disable itself as appropriate.

The background to this code is that every top level menu item will be a NSMenu, and likewise every originating item in a multi-level menu setup, e.g. the ‘Recent’ item under ‘File’ that pops out a list of the recently-opened files. Any NSMenu can then be assigned a delegate object to handle certain events, one of which – menuNeedsUpdate – is called to allow you to update sub-items’ state as necessary. In our case, we have the delegate be NSMenu (or rather, a subclass of NSMenu) itself. In this capacity, the class then handles the menuNeedsUpdate message by asking sub-items’ actions to update themselves.

Now, there is a slight flaw in this fix, and in particular the TFMXOSMenu class, since in short, its destructor will never get called. This is an occupational hazzard of the fact that when you define an Objective-C class (as we do here) using the Delphi to Objective-C bridge, the reference counting of the core Objective-C/Cocoa part of instantiated objects and the reference counting of the Delphi part are completely separate from one another. Alas, but the TFMXOSMenuItem class of the original unit suffers from this same flaw. However, since the menu bar isn’t something you should be recreating at will – rather, it should be created at start up and left there until the application terminates – this isn’t a big deal in practice given OS X, like Windows, will reclaim all allocated resources when the process terminates anyhow.

Problem: TPopupMenu in FMX has no OnPopup event

With the VCL TPopupMenu, I frequently make use of its OnPopup event. Unfortunately no such event exists in the FMX equivalent. Nonetheless, an interposer class can fix that easily enough:

type
  TPopupMenu = class(FMX.Menus.TPopupMenu)
  strict private
    FOnPopup: TProc<TObject>; //or TNotifyEvent
  public
    procedure Popup(X: Single; Y: Single); override;
    property OnPopup: TProc<TObject> read FOnPopup write FOnPopup;
  end;

procedure TPopupMenu.Popup(X, Y: Single);
begin
  if Assigned(FOnPopup) then
    FOnPopup(Self);
  inherited;
end;

When the interposer class is in scope, you can now assign an OnPopup handler at runtime.

Problem: TPopupMenu forgets the order of hidden items

Say you have a popup menu whose items you conditionally hide (i.e., set their Visible properties to False) at various times. In such a situation, after every time the menu is shown, it will have pushed the hidden items to the top of the list, so that when you unhide them later, they will appear in a different position to what their were originally.

The underlying cause is that when a TPopupMenu is shown, it reassigns visible items’ Parent property to the control that will actually be ‘poping up’, before reassinging the properties back to itself once the popup closes. My preferred workaround is once more an interposer class, adding code to explicitly maintain the items’ order. Continuing with the previous example, the overridden Popup method should be rewritten to look something like this:

//TPopupMenu declaration as before...

type
  THiddenRec = record
    Item: TControl;
    Index: Integer;
  end;

procedure TPopupMenu.Popup(X, Y: Single);
var
  HiddenList: TList<THiddenRec>;
  Rec: THiddenRec;
  I: Integer;
begin
  if Assigned(FOnPopup) then
    FOnPopup(Self);
  //fix stock code not maintaining order of hidden items
  HiddenList := nil;
  try
    for I := ItemsCount - 1 downto 0 do
    begin
      Rec.Item := Items[I];
      if not Rec.Item.Visible then
      begin
        Rec.Index := Rec.Item.Index;
        if HiddenList = nil then
          HiddenList := TList<THiddenRec>.Create;
        HiddenList.Add(Rec);
        Rec.Item.Parent := nil;
      end;
    end;
    inherited;
    if HiddenList <> nil then
      for I := HiddenList.Count - 1 downto 0 do
      begin
        Rec := HiddenList[I];
        InsertObject(Rec.Index, Rec.Item);
      end;
  finally
    HiddenList.Free;
  end;
end;

That’s it for now. If nothing else, my patience is getting rather frayed…

Advertisements

FMX/OS X tip: enabling compiler optimisations to avoid weird startup crashes

I wasn’t able to trace the precise change that did it, but at a certain point I had the peculiar problem of an OS X debug build crashing at startup, and hanging the IDE if I attempted to run it in the debugger. Switching to the ‘release’ build configuration allowed it to run, but when you’re working with a framework that is so, er, work-in-progress as FireMonkey, not being able to run an application in the debugger for a certain target platform is rather debilitating.

Seeking to investigate the problem, I created a copy of the project and progressively removed units until it ran properly again. This led to things getting even weirder though, since use of generic types in unit initialization clauses or class constructors was seemingly the straw that broke the camel’s back! However, I finally hit upon a solution, which was to enable compiler optimisations (the default is ‘off’ in the stock debug configurations). To do this, I went to Project|Options, chose the second node down (Delphi Compiler -> Compiling), ensured ‘Debug configuration – OS X platform’ was selected in the combo box at the top, then ticked the ‘Optimization’ box and finally clicked OK.

Notwithstanding this issue, overall I’d say remote debugging in XE2 and XE3 from a Windows guest to an OS X host is pretty smooth (if a bit bumpy once exceptions get raised), so don’t let this put you off…

FMX OnCloseQuery and OnClose woes on OS X – fixing QC 111362 (+ another bug)

If you’ve attempted to use FireMonkey to write an application targeting OS X, you may well have come across CloseQuery bugs – for me it’s generally been any dialog shown by a form’s OnCloseQuery handler having a tendancy to show twice, though you can also get the opposite problem, of the OnCloseQuery handler not being shown at all. QC 111362 (not by me) provides a clear test case of both: in the first instance, the user answering ‘yes’ to the save changes prompt leads to the prompt showing again, and in the second, assigning the ModalResult property of a button leads to OnCloseQuery not being raised at all when the button is clicked.

Having been bugging me for a while, I finally decided to investigate the problems properly. First up was the showing-twice issue. At first I thought it must be dialog-related, but it wasn’t:

  • As in the VCL, the FMX TForm has CloseQuery and Close methods, with the implementation of the latter calling the former. (Unlike in the VCL, FMX does not abstract out calling the OnClose event into a DoClose method. Instead, it is called directly inside Close. But I digress)
  • At the platform-specific level, FMX.Platform.pas calls CloseQuery in response to the windowShouldClose: message, which is good, and Close in response to windowWillClose:… which is bad. Why? Because as its name implies, by the time windowWillClose: is sent, the window is closing, not asking whether it should close. When the user clicks the red traffic light, the message order is therefore windowShouldClose:, and if that returns True, windowWillClose:. When OnCloseQuery has been handled without setting CanClose to False, this means it gets called a second time. Doh!
  • Instead of calling Close, the windowWillClose: handler should just invoke any assigned OnClose handler, if that. If a DoClose method had existed, perhaps the author would have at least thought whether Close rather DoClose is really appropriate…

Now, I say that windowWillClose: should call OnClose ‘if that’ because OnClose itself acts as a ‘are you really, really sure you want the form to close?’ event. This is because if a handler sets the Action var parameter to TCloseAction.caNone, in principle, the form should not now close:

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if MessageDlg('Are you SURE you want the form to close?',
    TMsgDlgType.mtConfirmation, mbYesNo) <> mrYes then
    Action := TCloseAction.caNone;
end;

Predictably enough though, such code doesn’t work when targeting OS X, assuming Close is called because of the user attempting to close the form rather than the form being closed programmatically. More exactly, setting Action to anything beyond the default (caHide) doesn’t have any affect, because Cocoa has already decided what will happen. To make even OnClose work correctly, then, almost the entire logic needs to be placed in the windowShouldClose: handler, and the windowWillClose: handler left relatively bare.

As for the bug of OnCloseQuery not being raised at all, the problem there is specific to a modal form being closed due to its ModalResult property being set, either directly or via a button. Looking at the source, the CloseModal call inside TPlatformCocoa.ShowWindowModal looked suspicious, and sure enough, it was the problem:

  • As before, the FMX TForm mimics its VCL forebear in having a CloseModal method. In the VCL (and FMX/Windows), this is is called inside the modal message loop once a change in the form’s ModalResult property has been detected. CloseModal itself then calls CloseQuery to give the client code the chance to cancel the closure; if CloseQuery returns False, the ModalResult property is reset to mrNone (0). Back in the modal message loop, the ModalResult property is then checked a second time to allow any reset to take effect.
  • The author of TPlatformCocoa.ShowWindowModal misunderstood the purpose of CloseModal, apparently assuming it is some sort of notification method rather than a ‘last chance to cancel’ one. As such, CloseModal is called in the wrong place (too late), and ModalResult not rechecked.

Here’s how to fix these various bugs:

  • Take a copy of FMX.Platform.Mac.pas, and put it in your application’s main source directory (you can also explicitly add it to the project if you want).
  • Open the copy and head to the TFMXWindow.windowShouldClose method implementation. Change it look like this:
function TFMXWindow.windowShouldClose(Sender: Pointer): Boolean;
var
  Action: TCloseAction;                                  //!!!CCR
begin
  Result := False;
  if Application = nil then
    Exit;
  if Application.Terminated then
    Exit;
  try
    Result := Wnd.CloseQuery;
    if Result and Assigned(Wnd.OnClose) then             //!!!CCR
    begin                                                //!!!CCR
      Action := TCloseAction.caHide;                     //!!!CCR
      Wnd.OnClose(Wnd, Action);                          //!!!CCR
      if Action = TCloseAction.caMinimize then           //!!!CCR
      begin                                              //!!!CCR
        Result := False;                                 //!!!CCR
        Wnd.WindowState := TWindowState.wsMinimized;     //!!!CCR
      end                                                //!!!CCR
      else                                               //!!!CCR
        if (Application <> nil) and                      //!!!CCR
           (Wnd = Application.MainForm) then             //!!!CCR
          Result := (Action in [TCloseAction.caHide,     //!!!CCR
            TCloseAction.caFree])                        //!!!CCR
        else                                             //!!!CCR
        begin                                            //!!!CCR
          Result := (Action = TCloseAction.caHide);      //!!!CCR
          if Action = TCloseAction.caFree then Wnd.Release;
        end;                                             //!!!CCR
    end;
  except
    Application.HandleException(Self);
  end;
end;

The next method down is probably TFMXWindow.windowWillClose; in it, delete everything, before adding the following in its place:

procedure TFMXWindow.windowWillClose(
  notification: NSNotification);
begin
  if (Application <> nil) and (Application.MainForm = Wnd) then
    Application.Terminate;
end;

The addition preserves the ‘main form’ semantics we implicitly remove by not calling Close (which would otherwise enforce them).

The revised windowShouldClose and windowWillClose methods fix the double shows and OnClose not working properly; to fix the no-shows, head next to the implementation of TPlatformCocoa.ShowWindowModal. Find the call the AForm.CloseModal, and comment it out (it’s in the wrong place). Finally, find the check for AForm.ModalResult not being 0, and amend the code to look like this:

          if AForm.ModalResult <> 0 then
          begin
            AForm.CloseModal;                     //!!!CCR
            if AForm.ModalResult <> 0 then        //!!!CCR
            begin                                 //!!!CCR
              NSApp.stopModal;
              Continue;
            end;                                  //!!!CCR
          end;

To take effect, you may need to disable debug DCUs to ensure the revised unit gets picked up (Project|Options, Delphi Compiler > Compiling, uncheck ‘Use debug DCUs’ – you’ll only need to do this for the OS X debug configuration).

As a final point, please keep in mind I do not encourage invoking event handlers directly, as the code above does – the fact the FMX source doesn’t bother with dedicated event-calling methods half the time says nothing about the validity of such practice. The only reason I do it above is because I wish to avoid interface changes, and therfore, DCU incompatibilities.

Retrieving the application’s version string

If you need to retrieve at runtime your application’s version number, you’re targeting Windows, and you don’t mind just having the major and minor numbers, then SysUtils has a handy GetFileVersion function for the task:

function GetAppVersionStr: string;
var
  Rec: LongRec;
begin
  Rec := LongRec(GetFileVersion(ParamStr(0)));
  Result := Format('%d.%d', [Rec.Hi, Rec.Lo])
end;

If you want the ‘release’ and ‘build’ numbers as well, you’ll need to drop down to the Windows API:

function GetAppVersionStr: string;
var
  Exe: string;
  Size, Handle: DWORD;
  Buffer: TBytes;
  FixedPtr: PVSFixedFileInfo;
begin
  Exe := ParamStr(0);
  Size := GetFileVersionInfoSize(PChar(Exe), Handle);
  if Size = 0 then
    RaiseLastOSError;
  SetLength(Buffer, Size);
  if not GetFileVersionInfo(PChar(Exe), Handle, Size, Buffer) then
    RaiseLastOSError;
  if not VerQueryValue(Buffer, '\', Pointer(FixedPtr), Size) then
    RaiseLastOSError;
  Result := Format('%d.%d.%d.%d',
    [LongRec(FixedPtr.dwFileVersionMS).Hi,  //major
     LongRec(FixedPtr.dwFileVersionMS).Lo,  //minor
     LongRec(FixedPtr.dwFileVersionLS).Hi,  //release
     LongRec(FixedPtr.dwFileVersionLS).Lo]) //build
end;

If targeting OS X then GetFileVersion isn’t available, so you need to drop down to the OS API again. This time the code is much simpler however:

uses Macapi.CoreFoundation;

function GetAppVersionStr: string;
var
  CFStr: CFStringRef;
  Range: CFRange;
begin
  CFStr := CFBundleGetValueForInfoDictionaryKey(
    CFBundleGetMainBundle, kCFBundleVersionKey);
  Range.location := 0;
  Range.length := CFStringGetLength(CFStr);
  SetLength(Result, Range.length);
  CFStringGetCharacters(CFStr, Range, PChar(Result));
end;

This uses Core Foundation; you could use Cocoa instead, but when an equivalent ‘Core’ API is available, it’s usually the better choice in Delphi given it can be called directly, and with code almost identical to the Objective-C samples available online and elsewhere.

Cross platform FMX/XE3 demos for CCR Exif

I’ve just added a set of FMX demos in the SVN trunk for my image metadata reading/writing library, CCR Exif. These are ports of the existing VCL demos (minus the resaving tester). In alphabetical order, they number the following:

  • ExifList displays the Exif tags from an image, Exif being the metadata format most cameras use.
  • ExifTimeShifter shifts the Exif date/times for one or more images by a specified number of minutes.
  • IPTCEditor edits the IPTC data held in an image. IPTC is the metadata format Photoshop used back in the 1990s, and makes for a slightly lower-tech alternative to Exif.
  • JpegDump displays information from a JPEG file’s header, providing a lower-level view of its metadata tags than the other demos.
  • Screenshooter takes a screenshot, applies a few tags, and saves to a new JPEG file.
  • XMPBrowser displays XMP metadata in a tree view, XMP being a newer, XML-based metadata format.

While the FMX demos (which require XE3 by the way) mostly hew to the line set by their VCL forebears, I began with a blank slate in each case. This I did because I wanted certain UI details to follow the target platform (i.e., Windows or OS X), and in so doing, see what FMX did to help, or at least, see how far it wouldn’t get in the way.

Screenshooter and ExifTimeShift

The simplest demo to port was Screenshooter, since the FMX version has almost exactly the same UI for both platforms. The only difference is the fact the Mac version has the regular Mac menu bar; however, the menu items on it are just the standard ones that FMX in XE3 set up for you automatically:

Similar between platforms too is ExifTimeShift:

In this case there are some subtle differences though:

  • I hide a few toolbar buttons on OS X because toolbars on a Mac just tend to have less items than their Windows equivalents.
  • The Preferences/Options dialog is non-modal on OS X, with no explicit OK/Cancel buttons and changes applied immediately. (I leave aside whether this makes for a better UI – in fact, I think I prefer the Windows model myself – but that is the contemporary Apple style.)
  • On both platforms, double clicking a listed file displays the image. On Windows the image window gets its own taskbar button; on OS X it is full screen-enabled – you can see this in the screenshot with the double arrow icon on the title bar – and listed under the application’s Window menu. While I had to implement these things manually, it wasn’t hard, though I’d admit the Windows part requires more work (the FMX developers are unfortunately obsessed with a Win 3.x/VCL-style ‘main form’ concept that isn’t technically native even on Windows).
  • The file date/time preference is saved to the Registry on Windows and the application’s preferences file on OS X. Since I didn’t want to introduce dependencies, the latter is done by direct API calls, though in practice I would use my own Mac Preferences API wrapper I blogged about previously.

ExifList, IPTCEditor and XMPBrowser

The main platform difference between the remaining demos is much more ‘in your face’: whereas on Windows the applications are SDI, on OS X they follow the basics of the Mac document model. As such, if you run ExifList, IPTCEdit or XMPBrowser on OS X without passing the application a file first, it opens with no window showing. This follows what Preview does:

Further, whereas the same instance of the Mac version can have multiple images open, each with their own window (and enumerable using the system-standard Cmd+` shortcut), you need to open the Windows version several times to have it load several images at the same time:

Implementation

Each demo is essentially single source, and structured around my preferred VCL UI model. In this, the document form’s OnClick code is all centralised into action lists located on a data module separate from the form itself, with any necessary communication between the actions and the form being performed via interface types. A unit shared between all the demos (CCR.FMX.Demos.pas) then contains all the platform-specific UI-related code, and manages the differences between the different document models (no form is auto-created in the traditional Delphi fashion, and the document form class itself is looked up using RTTI). Alas, but that unit also implements certain crucial bug fixes for FMX itself, though at least the new IFMXxxxService system (see my previous-but-one post) made that bareable. I’ll probably blog about it in more detail later.

Creating a PDF redux: FMX and OS X in slightly sour disharmony

Not so long ago I posted about how to create a PDF file using the OS X API and a FireMonkey TCanvas. Alas, but little did I know but the hack I used to get a canvas to accept an arbitrary CGContextRef (= HDC for OS X’ers) no longer worked in the soon-to-be released XE3. Oh dear!

Nonetheless, I have now got it to (mostly) work again, and with a couple of IFDEF’s the revised demo should now compile in both XE2 and XE3 (the code is in the same place if you’re interested). I say ‘mostly’, because page 1 of the output (which results from simply asking a TLayout to draw itself and its children) loses the subtle glow effect I added round the title. If you peer closely, you should be able to see what I mean in this screenshot (on the left is the FMX/XE3 application, on the right the outputted PDF open in Preview):

Not a big deal, but still sliiighly disappointing.

As for the hot and bothered simian itself… well, it’s a lot better than its XE2 precursor. If you had the patience to stick with FMX in XE2, you will find upgrading quite a chore though because there has been lots of little interface changes, like the one that bit my PDF demo. Put another way, pretty much *any* hack you felt forced to make in XE2 will need to be either rewritten (because no longer possible) or removed (because the hack is no longer necessary). I really don’t think people should complain about that though – in fact, if anything, *more* interface changes were needed.

By that I mean the refactoring effort for FMX/XE3 hasn’t been completed even on its own terms. For example, the walking-up-the-list disease I blogged about before has been mostly fixed, but there is still the odd example left unfixed – TMainMenu, I’m looking at you! The fact the latter appears in the final part of a unit where cases above it *have* been fixed suggests the development team just ran out of time, though as I said, they did make genuine progress even so.

FindFirst on OS X – two small tips

If using FindFirst/FindNext/FindClose when targeting OS X , there’s two things to keep in mind:

  1. To search for items with any name, use a mask of ‘*’ not ‘*.*’.
  2. If enumerating sub-directories and sub-directories of sub-directories, remember to check faSymLink or you may find yourself in an endless loop.

The following code demonstrates getting both things right, assuming you just want to ignore directory links like you would a folder shortcut on Windows (thanks to Primož Gabrijelčič in the comments for making the caveat clear):

procedure DoSearch(const Path, FileSpec: string);
const
  SAnyMask = {$IFDEF MSWINDOWS}'*.*'{$ELSE}'*'{$ENDIF};
var
  Info: TSearchRec;
begin
  //find files in the immediate directory...
  if FindFirst(Path + FileSpec, faAnyFile and not faDirectory,
    Info) = 0 then
  try
    repeat
      //work with found file info...
    until FindNext(Info) <> 0;
  finally
    FindClose(Info);
  end;
  //find sub-directories and recurse
  if FindFirst(Path + SAnyMask, faDirectory, Info) = 0 then
  try
    repeat
      if (Info.Attr and faDirectory <> 0) and
         (Info.Name <> '.') and  (Info.Name <> '..') and
         (Info.Attr and faSymLink = 0) then
        DoSearch(Path + Info.Name + PathDelim, FileSpec);
    until FindNext(Info) <> 0;
  finally
    FindClose(Info);
  end;
end;

As an aside, both these things I previously forgot in the public version of one of my book demos (ahem), but the source is now updated in the SVN.