Oh dear, what a long haul.
I have some code that is working now. There may still be bugs in odd corner cases but I've done my best to test them.
In this example code, I simply place keys as they are pressed (including enter and delete key presses) into a TQueue in a form and display them in a label (one by one). You could easily adapt this code to produce events instead. Beware of threading issues.
To get this code working, you'll need a blank FireMonkey application targeting Android. Place three labels on the form and name it "TestKeyDownMainForm". Name the unit "FormTestKeyDownMain". Place also a Timer.
Make Label1 a caption, Label2 a display area and Label3 a header. Set the Timer to every 30ms or so (more if you want). Paste in the code (replacing everything that's there) and then connect up the OnCreate, OnShow, OnDestroy and OnKeyDown events for the Form and the OnTimer event for the Timer. You should be able to figure this out if I haven't been entirely specific.
unit FormTestKeyDownMain;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
System.SyncObjs, System.Generics.Collections,
Androidapi.AppGlue,
Androidapi.NativeActivity,
Androidapi.JNIBridge,
Androidapi.JNI.JavaTypes,
Androidapi.JNI.GraphicsContentViewText,
Androidapi.JNI.Embarcadero,
FMX.Helpers.Android,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
FMX.Controls.Presentation, FMX.StdCtrls;
type
TTextListener = class(TJavaLocal, JFMXTextListener)
private
FLastLen,
FLastEnter,
FLastAddSkip,
FAddSkip: Integer;
FWasEndWord,
FWasDelete,
FWasEnter,
FGotSpace,
FComposing: Boolean;
FLastChar: Char;
FHistory: TStack<Char>;
FLock: TCriticalSection;
public
constructor Create;
destructor Destroy; override;
procedure onComposingText(beginPosition: Integer; endPosition: Integer); cdecl;
procedure onSkipKeyEvent(event: JKeyEvent); cdecl;
procedure onTextUpdated(text: JCharSequence; position: Integer); cdecl;
end;
TTestKeyDownMainForm = class(TForm)
Label1: TLabel;
Label2: TLabel;
Timer1: TTimer;
Label3: TLabel;
procedure FormShow(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FFMXAct: JFMXNativeActivity;
FFMXTxp: JFMXTextEditorProxy;
FTxtLsn: TTextListener;
FCharCS: TCriticalSection;
FCharIn: TQueue<Char>;
public
{ Public declarations }
end;
var
TestKeyDownMainForm: TTestKeyDownMainForm;
implementation
{$R *.fmx}
{ TTextListener }
constructor TTextListener.Create;
begin
inherited;
FLock:= TCriticalSection.Create;
FLastEnter:= -1;
FLastLen:= 0;
FLastAddSkip:= 0;
FAddSkip:= 0;
FWasEndWord:= False;
FGotSpace:= False;
FComposing:= False;
FHistory:= TStack<Char>.Create;
end;
destructor TTextListener.Destroy;
begin
FHistory.Clear;
FHistory.Free;
FLock.Free;
inherited;
end;
procedure TTextListener.onComposingText(beginPosition, endPosition: Integer);
begin
FLock.Acquire;
try
FComposing:= True;
if beginPosition > 0 then
FAddSkip:= beginPosition;
finally
FLock.Release;
end;
end;
procedure TTextListener.onSkipKeyEvent(event: JKeyEvent);
begin
end;
procedure TTextListener.onTextUpdated(text: JCharSequence; position: Integer);
var
i,
skip,
l,
t: Integer;
s: string;
begin
FLock.Acquire;
try
l:= text.length;
s:= string(text.toString);
skip:= 3;
i:= l - 3;
while i >= 10 do
begin
i:= i div 10;
Inc(skip);
end;
if FAddSkip > (l - 3) then
FAddSkip:= 0;
Inc(skip, FAddSkip);
t:= l - skip;
FWasDelete:= (not FWasEndWord) and (not FGotSpace) and
((t = 0) or (t < FLastLen) or ((FLastLen <= 0) and
(FLastAddSkip > FAddSkip)));
if (not FWasDelete)
and FGotSpace
and (FAddSkip = FLastAddSkip)
and (t <= FLastLen) then
FWasDelete:= True;
FWasEndWord:= (not FWasDelete) and (not FGotSpace) and (FLastLen = t) and
(skip < l);
if (not FWasEndWord)
and (not FComposing)
and FWasEnter then
FWasEndWord:= True
else if FWasEndWord
and (FHistory.Count > 0)
and (FHistory.Peek = #13) then
begin
FWasDelete:= True;
FWasEndWord:= False;
end;
FWasEnter:= False;
FComposing:= False;
FLastLen:= t;
FLastAddSkip:= FAddSkip;
if FWasEndWord then
Exit
else if FWasDelete then
FLastChar:= #07
else
FLastChar:= text.charAt(l - 1);
FGotSpace:= FLastChar = ' ';
if FWasDelete then
begin
if FHistory.Count > 0 then
FHistory.Pop
end
else
FHistory.Push(FLastChar);
TestKeyDownMainForm.FCharCS.Acquire;
try
TestKeyDownMainForm.FCharIn.Enqueue(FLastChar);
finally
TestKeyDownMainForm.FCharCS.Release;
end;
finally
FLock.Release;
end;
end;
{ TForm1 }
procedure TTestKeyDownMainForm.FormCreate(Sender: TObject);
begin
FFMXAct:= TJFMXNativeActivity.Wrap(Pandroid_app(PANativeActivity(DelphiActivity)^.instance)^.activity.clazz);
FFMXTxp:= FFMXAct.getTextEditorProxy;
FCharCS:= TCriticalSection.Create;
FCharIn:= TQueue<Char>.Create;
FTxtLsn:= TTextListener.Create;
FFMXTxp.addTextListener(FTxtLsn);
end;
procedure TTestKeyDownMainForm.FormDestroy(Sender: TObject);
begin
FFMXTxp.removeTextListener(FTxtLsn);
FTxtLsn.Free;
FCharIn.Free;
FCharCS.Free;
end;
procedure TTestKeyDownMainForm.FormKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
begin
FTxtLsn.FLock.Acquire;
try
if not FTxtLsn.FWasDelete then
begin
FTxtLsn.FWasEnter:= True;
FTxtLsn.FWasEndWord:= False;
FTxtLsn.FHistory.Push(#13);
FTxtLsn.FLastChar:= #13;
FTxtLsn.FLastEnter:= FTxtLsn.FLastLen;
FCharCS.Acquire;
try
FCharIn.Enqueue(FTxtLsn.FLastChar);
finally
FCharCS.Release;
end;
end;
FTxtLsn.FWasDelete:= False;
FTxtLsn.FComposing:= False;
finally
FTxtLsn.FLock.Release;
end;
end;
procedure TTestKeyDownMainForm.FormShow(Sender: TObject);
begin
Label2.Text:= '';
CallInUIThread(procedure
begin
FFMXTxp.setFocusable(True);
FFMXTxp.setFocusableInTouchMode(True);
FFMXTxp.requestFocus;
FFMXTxp.showSoftInput(True);
end);
end;
procedure TTestKeyDownMainForm.Timer1Timer(Sender: TObject);
var
c: Char;
begin
FCharCS.Acquire;
try
while FCharIn.Count > 0 do
begin
c:= FCharIn.Dequeue;
if c = #13 then
begin
Label3.Text:= 'Got Enter';
Label2.Text:= Label2.Text + #13;
end
else if c = #07 then
begin
Label3.Text:= 'Got Delete';
Label2.Text:= Copy(Label2.Text, Low(Label2.Text), Length(Label2.Text) - 1);
end
else
begin
if c = ' ' then
Label3.Text:= 'Got Space'
else
Label3.Text:= 'Got Regular';
Label2.Text:= Label2.Text + c;
end;
end;
finally
FCharCS.Release;
end;
end;
end.
If you look at the code, its quite hideous. Not only because I haven't commented it but also because of all the hoops I had to go through to get a key press sequence (please excuse my ancient - ALGOL - strict block style). If anyone can come up with a better way, I'd sure like to see it! What is with the '[nn]' tags in the sequence, anyway??
I haven't tried to test it using a real keyboard, only the soft/virtual keyboard. I'm pretty sure that shift, alt and control keys would kill this logic. However, if you need to support it then you should be able to start by examining the event parameter of onSkipKeyEvent.
It should be possible to use this code without the FMX framework as well, so long as you can show the soft keyboard (by replacing CallInUIThread). I believe you'll need to hook up the logic in the OnKeyDown event to the onInputEvent of the application instead. I'll be trying this next.
Enjoy.
Daniel.
Edit:
My apologies but everywhere I say "Delete" I should probably say "BackSpace". I hope this doesn't cause any confusion.
Unfortunately, I have found more cases where this code doesn't work. An example is pressing enter after backspace. I have a fix for that case but there are more. I am trying to find code that does work but I'm concerned that I won't be able to come up with code that functions reliably.