0
votes

Hiyas.

I'm wanting to know which keys the user is pressing, as they press them in my Delphi app on Android (from the soft/virtual keyboard).

I've got a bit of code to show the keyboard in the Form's OnShow event and wired up some other code in the OnKeyDown event but the OnKeyDown event is only fired when the user presses Enter not as they press each key.

Do I do this using addTextListener on the JFMXTextEditorProxy of the JFMXNativeActivity? Will that actually work, especially if I have no views/edit controls (just a form on which I'm rendering an image).

Can anyone assist?

TIA.

Daniel.

2

2 Answers

2
votes

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.

0
votes

Soft keyboards don't send key down/key up events. They generally use commitText, which ends entire strings at a time. The correct way from Java is to use a TextWatcher. I assume this would map to an addTextListener in delphi.