CCR.VirtualKeying – virtual keystroke interface for Windows and OS X

I’ve just added to GitHub a little virtual keystroke interface for Windows and OS X – on the former it wraps the SendInput API, on the latter the CGEvent functions:

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

Virtually typing some text into the active window is a one-liner:

TVirtualKeySequence.Execute('¡Hola!');

Alternatively, you can build up a sequence of keystrokes to perform by working with an explicit IVirtualKeySequence instance:

var
  Sequence: IVirtualKeySequence;
begin
  Sequence := TVirtualKeySequence.Create;
  Sequence.AddKeyPresses('Привет!');
  {$IFDEF MACOS}
  Sequence.Add(vkA, [ssCommand], [keDown, keUp]);
  {$ELSE}
  Sequence.Add(vkA, [ssCtrl], [keDown, keUp]);
  {$ENDIF}
  Sequence.Execute;

For more on the available methods, check out the readme at the above link. The repository also includes a simple demo in FMX and VCL forms (code is essentially the same).

Sending virtual keystrokes on OS X

Going through my comments backlog I found a question asking how to send virtual keystrokes on OS X. Short answer is to use the CGEvent functions in MacApi.CoreGraphics – in a nutshell,

  1. Call CGEventSourceCreate to allocate a CGEventSourceRef.
  2. For each keystroke, create a CGEventRef by calling CGEventCreateKeyboardEvent; this takes the CGEventSourceRef just allocated plus an OS X virtual key code. For possible values of the latter, see the MacApi.KeyCodes unit, assuming you have a recent Delphi version, otherwise scan the source for KEY_xxx values. Also, note that you will need separate events for first pressing a key down, then unpressing it.
  3. If required, call CGEventSetFlags on a given CGEventRef to assign a modifier key (alt/shift/ctrl/command); in the case of a character, you’ll typically want to call CGEventKeyboardSetUnicodeString as well rather than messing about trying to figure out the correct key code.
  4. Actually perform the event by passing the CGEventRef to CGEventPost, using kCGHIDEventTap for the first parameter.
  5. Clean up by passing each CGEventRef then the CGEventSourceRef to CFRelease.

The following follows these steps to generate key down and key up events for a Cmd+L shortcut:

uses
  System.SysUtils, MacApi.CocoaTypes, MacApi.CoreFoundation, MacApi.CoreGraphics, MacApi.KeyCodes;

procedure PressCmdLShortcut;
var
  EventSourceRef: CGEventSourceRef;
  KeyDownRef, KeyUpRef: CGEventRef;
begin
  KeyDownRef := nil;
  KeyUpRef := nil;
  EventSourceRef := CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
  if EventSourceRef = nil then RaiseLastOSError;
  try
    KeyDownRef := CGEventCreateKeyboardEvent(EventSourceRef, KEY_L, 1); //1 = key down
    if KeyDownRef = nil then RaiseLastOSError;
    CGEventSetFlags(KeyDownRef, kCGEventFlagMaskCommand);
    KeyUpRef := CGEventCreateKeyboardEvent(EventSourceRef, KEY_L, 0);  //0 = key up
    if KeyUpRef = nil then RaiseLastOSError;
    CGEventSetFlags(KeyUpRef, kCGEventFlagMaskCommand);
    CGEventPost(kCGHIDEventTap, KeyDownRef);
    CGEventPost(kCGHIDEventTap, KeyUpRef);
  finally
    if KeyDownRef <> nil then CFRelease(KeyDownRef);
    if KeyUpRef <> nil then CFRelease(KeyUpRef);
    CFRelease(EventSourceRef);
  end;
end;

Alternatively, you can use a little cross platform (Mac/Windows) wrapper interface I’ve written… more on which in my next post!

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…

 

Working around an XE5/Mac menu issue

In XE5, if you try running the FMX ‘ActionsDemo’ sample project on OS X, you’ll quickly find an issue – the menu bar isn’t set up correctly:

ActionsDemo menu wrong
ActionsDemo menu wrong 2

ActionsDemo menu wrong 3
If you open up the menu designer in the IDE, you’ll find what should be happening is that the application menu gets four items: Hide ActionsDemo, Hide Others, a separator, and Quit ActionsDemo, with all but the separator having an appropriate shortcut assigned:

ActionsDemo menu designer
Each of the three substantive items uses a standard FMX action, and to be honest, I have always found them poorly coded since they were introduced in XE3. However, the menu bar not being built correctly at all didn’t happen in XE3.

Comparing the FMX Mac menu bar source code between XE3 and XE5 doesn’t come up with much though – the code for building the default menu bar has been split out into its own method, and that’s about it. However, after adding a couple of breakpoints and running the ActionsDemo sample through the debugger, I discovered the key method – TPlatformCocoa.CreateOSMenu – being called several times when the application was starting up. Partly this was due to style notifications, and partly due to the code being re-entrant in a somewhat sloppy way – TPlatformCocoa.CreateOSMenu first gets rid of what was there before, however the clean-up code for a FMX menu item on OS X religiously calls CreateOSMenu to rebuild the menubar regardless of whether the item is being destroyed singularly, or as part of a general ‘get rid of everything’ call. Since the CreateOSMenu code in itself doesn’t take account of possible re-entrancy (i.e., being called again when it hasn’t yet finished the time before)… the end result is not well determined.

As such, a quick fix for the re-entrancy issue is to add a flag to avoid CreateOSMenu doing its stuff when it hasn’t finished doing it before:

  • Take a copy of FMX.Platform.Mac.pas
  • In the copy, add the following private field to TPlatformCocoa:
FCreatingOSMenu: Boolean;
  • Amend the top of TPlatformCocoa.CreateOSMenu to look like this:
if FCreatingOSMenu then Exit; //!!!added
AutoReleasePool := TNSAutoreleasePool.Create;
try
  FCreatingOSMenu := True;    //!!added
  • Amend the bottom of TPlatformCocoa.CreateOSMenu to look like this:
finally
  FCreatingOSMenu := False;    //!!!added
  AutoReleasePool.release;
end;
  • Save the unit, add it to the project, and rebuild.

While this fix gets rid of the MacOSMenuItem top level item that shouldn’t be there, things still aren’t OK though:

ActionsDemo menu still wrong

Notice the application menu is still missing Hide ActionsDemo and Hide Others. This is because the action classes used – TFileHideApp and TFileHideAppOthers – are poorly coded. Happily, they can be avoided easily enough though, and without requiring platform specific code as such:

  • Add the following private field to the form:
FHideAppService: IFMXHideAppService;
  • In the form’s OnCreate event handler, retrieve the platform service in the normal way:
FHideAppService := TPlatformServices.Current.GetPlatformService(
  IFMXHideAppService) as IFMXHideAppService;
  • Bring up the form’s designer surface, right click on it and select View as Text.
  • Find TFileHideApp and TFileHideAppOthers at the end, and change the types to plain TAction.
  • Right click and choose View as Form.
  • Double click the action list, and in the action list editor, find and put back the two actions’ standard Text and ShortCut properties – the Text for FileHideApp1 should be ‘Hide ActionsDemo’ and the ShortCut Cmd+H; the Text for FileHideAppOthers1 should be ‘Hide Others’ and the ShortCut Cmd+Alt+H.
  • Handle FileHideAppOthers1’s OnExecute event like this:
procedure TMainForm.FileHideAppOthers1Execute(Sender: TObject);
begin
  FHideAppService.HideOthers;
end;
  • Handle FileHideApp1’s OnUpdate event like this (in practice the code isn’t necessary for how the demo is set up – SDI not Mac-style MDI – but we’ll add it for completeness):
procedure TMainForm.FileHideApp1Update(Sender: TObject);
begin
  (Sender as TCustomAction).Enabled := not FHideAppService.Hidden;
end;
  • Handle FileHideApp1’s OnExecute event like this:
procedure TMainForm.FileHideApp1Update(Sender: TObject);
begin
  FHideAppService.Hidden := True;
end;
  • On saving (Ctrl+S), you’ll be prompted to update two type declarations; answer Yes to both. Re-run the demo, and the menu bar should now be set up correctly:

ActionsDemo menu fixed

PS (and changing the subject) – for those who contacted me a bit ago about my Exif code, sorry, I will get round to replying, but I’ve just been busy with other things recently.

Programmatically shutting down, restarting, sleeping or logging off on OS X

Browsing StackOverflow, I came across a question asking how to programmatically shut down the computer in Delphi when targeting OS X. Mysteriously, the question has been met with four downvotes as I write this, leaving it ‘on hold as off-topic’ until the darstardly questioner stops thinking a programmer’s Q&A site is a proper place for programming questions or something.

Anyhow, with respect to the question, one easy way to do the deed is to use a Cocoa NSAppleScript object to run the following piece of AppleScript:

tell application "Finder" to shut down

As desired, ‘to shut down’ can be replaced with ‘to restart’, ‘to sleep’ or ‘to log out’.

Now in Delphi, NSAppleScript (or more exactly, a wrapper interface type for NSAppleScript) is declared in the Macapi.Foundation unit. Alas, but this is misdeclared, or at least was when I last looked (see here – if someone wants to confirm this is still the case in the latest and greatest, please do in the comments). As such, you need to fix the declaration before using it. On the other hand, fixing it is easy:

uses
  Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation;

type
  NSAppleScript = interface(NSObject)
    ['{0AB1D902-25CE-4F0B-A3BE-C4ABEDEB88BC}']
    function compileAndReturnError(errorInfo: Pointer): Boolean; cdecl;
    function executeAndReturnError(errorInfo: Pointer): Pointer; cdecl;
    function executeAppleEvent(event: NSAppleEventDescriptor; error: Pointer): Pointer; cdecl;
    function initWithContentsOfURL(url: NSURL; error: Pointer): Pointer; cdecl;
    function initWithSource(source: NSString): Pointer; cdecl;
    function isCompiled: Boolean; cdecl;
    function source: NSString; cdecl;
  end;
  TNSAppleScript = class(TOCGenericImport<NSAppleScriptClass, NSAppleScript>)  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Script: NSAppleScript;
  Error: Pointer;
begin
  Error := nil;
  Script := TNSAppleScript.Wrap(TNSAppleScript.Alloc.initWithSource(
    NSSTR('tell application "Finder" to shut down')));
  try
    if Script.executeAndReturnError(Error) = nil then
      raise EOSError.Create('AppleScript macro failed');
  finally
    Script.release;
  end;
end;

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.