Extended TClipboard implementation for FMX and VCL (CCR.Clipboard)

I’ve just pushed to GitHub (first time, so be gentle…) an extended, multi-platform TClipboard implementation for newer Delphi versions:

https://github.com/chrisrolliston/CCR.Clipboard

Where the platform allows, supports delayed rendering, virtual files, change notifications, and inter-process TClipboard-based drag and drop. The code originates from the FMX TClipboard I published a few years back, though is much extended, and was refactored to support the VCL too (XE2+). For more info, check out the readme first…

https://github.com/chrisrolliston/CCR.Clipboard/blob/master/Readme.md

… followed by the wiki pages for discussion of individual features, together with known issues and limitations:

https://github.com/chrisrolliston/CCR.Clipboard/wiki

Disclaimer: supporting multiple FMX versions ain’t no fun, so if you come to try it in XE4 or whatever and have an issue, I may not be able to help you. Also, if you’re interested in drag and drop on OS X, consider using my code with any version lower than XE8 a ‘proof of concept’ only…

 

Advertisements

FMX TClipboard now supports iOS

I’ve just checked in a revision of my open source FMX TClipboard implementation that has an iOS backend. This supports the current rather than the FPC-based version of ‘Delphi for iOS’, however the Windows and OS X backends still compile with XE2 and above.

In essence, the new code wraps the native iOS clipboard API (UIPasteboard) and presents it in a fashion that closely follows the VCL clipboard interface, just like the existing Windows and OS X support did the same for desktop platforms. For some reason Apple in their wisdom decided to make the iOS clipboard API similar yet randomly different to the OS X one, so even though there’s not masses of code, it was a bit fiddly to implement. Anyhow, I’ve also knocked out a little demo similar to the previous desktop one:

TiOSClipboard demo

Using (say) Photos, you can copy an image to the clipboard and paste it into the demo. Conversely, you can from the demo itself copy either just the text entered, just the image, both the text and the image as two representations of the same clipboard item, or both the text and image as a custom clipboard format. The ‘List Formats on Clipboard’ button, as its name implies, then lists the formats currently on the clipboard. This is what I get after copying an image from Photos on the iOS simulator:

Format List

Technically, the iOS clipboard, like the OS X one, can have multiple items, each with multiple representations. Since the multiple-item concept doesn’t exist on Windows (indeed, it didn’t exist on OS X originally either), my class is only concerned with the first item, which is what most applications only bother with anyhow.

If you want the code, the SVN URL for it and a few other pieces is the following:

http://delphi-foundations.googlecode.com/svn/trunk/FMX%20Utilities/

The core files are now CCR.FMXClipboard.pas, CCR.FMXClipboard.Apple.pasCCR.FMXClipboard.iOS.pas, CCR.FMXClipboard.Mac.pas and CCR.FMXClipboard.Win.pas, and together they stand alone.

FMX anti-pattern: returning nil rather than raising an exception on an invalid state or arguments

In the Delphi RTL and VCL, a method called LoadFromFile or the like will raise an exception if the file doesn’t exist or is invalid. Similarly, if an object property getter implements a lazy-loading pattern (meaning, the value of the property will only be initialised when first needed), it will raise an exception if the situation is such that nothing valid can be loaded or initialised. This is pretty basic exception programming – when an error arises, the flow of the calling code is forcibly interupted with a clear account of what call was invalid. Imagine if (say) TBitmap.LoadFromFile didn’t raise an exception when the file name passed to it didn’t refer to a valid bitmap file – calling code could go happily on its way assuming a graphic has been loaded when it hasn’t. Likewise, what if the Items property getter for TList<T> just returned nil (or equivalent) with an out of bounds index? If T is a class type, then off-by-one errors in the calling code will end up with cryptic access violations later on that might be nowhere near where the erroneous call was actually made!

Alas, but such basic understanding of exception programming is not grasped by at least one developer on the FMX team. Repeatedly across the XE2, XE3 and now (I learn) XE4 releases, methods that an experienced Delphi programmer would expect to raise an exception when nothing valid can be returned do not. As soon as one case is fixed – e.g., the TFmxObject.Children property getter was done so in XE3, albeit implicitly – another one or two are added, and frankly, it needs to stop now. The latest cases I learn are the HScrollBar and VScrollBar property getters on TScrollBox – done properly in XE3, the anti-pattern has however been introduced in XE4, causing ‘random’ access violations in TMemo (the FMX TMemo inherits from TScrollBox).

FireMonkey’s BoundsRect bug, and working around it

Just a quick one, but the BoundsRect property getter in the FireMonkey TControl is incorrectly implemented, returning (0, 0, Width, Height) rather than (Position.X, Position.Y, Position.X + Width, Position.Y + Height). At first you might be tempted to say the property just has different semantics to the VCL version, however the implementation of the property setter gives the lie to that. Steps:

  1. Create a new FireMonkey HD application.
  2. Add a button to the form.
  3. Handle the button’s OnClick event like this:
procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.BoundsRect := Button1.BoundsRect;
end;

Expected: nothing to happen. Actual: the button moves to the top-left corner of the form.

To work around this bug, either don’t read BoundsRect in the first place (it is after all just a small utility property), or put the following class helper in scope:

type
  TControlHelper = class helper for TControl
  strict private
    function GetBoundsRect: TRectF;
    procedure SetBoundsRect(const Value: TRectF);
  public
    property BoundsRect: TRectF read GetBoundsRect
      write SetBoundsRect;
  end;

//...

function TControlHelper.GetBoundsRect: TRectF;
begin
  Result.Left := Position.X;
  Result.Top := Position.Y;
  Result.Right := Result.Left + Width;
  Result.Bottom := Result.Top + Height;
end;

procedure TControlHelper.SetBoundsRect(const Value: TRectF);
begin
  SetBounds(Value.Left, Value.Top, Value.Width, Value.Height);
end;

Update 20/12/13: as pointed out in the comments, while GetBoundsRect remains as miscoded as ever in XE5, there is also a ParentedRect read-only property that does what the BoundsRect property getter should do. What a mess!

Fixing a TForm.BorderIcons bug in FMX/OS X

If you try removing biMaximize from a FireMonkey form’s BorderIcons property, you’ll find the ‘maximise’ (zoom) button on OS X (i.e., the green ‘traffic light’) resolutely staying both visible and enabled. The reason for the bug is the following code in FMX.Platform.Mac.pas, and it exists in both XE2 and XE3:

        if TBorderIcon.biMaximize in AForm.BorderIcons then
          Style := Style or NSWindowZoomButton;              

The problem here is that NSWindowZoomButton is not a valid member of a Cocoa window style ‘set’, a fact the compiler wasn’t able to pick up given Objective-C does not have strongly typed sets like Pascal. Further, you cannot in fact hide or disable the zoom button via the window style – instead, you get a reference to the actual button and hide or disable it ‘directly’.

As Apple’s UI guidelines say to disable an unwanted zoom button rather than hide it, that’s what we’ll do for our fix. So, having taken a copy of FMX.Platform.Mac.pas (or if you’ve been following this blog before, with your latest version of it open), head to TCocoaPlatform.CreateWindow. Next, comment out the lines quoted above (i.e. ‘if TBorderIcon…’). Then, immediately after the NSWin.initWithContentRect call, add the following:

    if (Style and not NSResizableWindowMask <> 0) and
      not (TBorderIcon.biMaximize in AForm.BorderIcons) then
    begin
      NSWin.standardWindowButton(NSWindowZoomButton).setEnabled(False);
      NSWin.standardWindowButton(NSWindowZoomButton).setTarget(nil);
    end;

Setting the ‘target’ to nil is necessary, otherwise the Enabled property will get automatically re-enabled – roughly, you can think of the button as having had an action assigned to it when created, and setting the Target to nil as like setting the Action property to nil again.

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…

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.