Search code examples
delphifiremonkeydelphi-12-athens

Create popup that handles focus nicely


I'm working on a tagging feature in my FMX application similar to Microsoft Teams.

Here's how it looks in Teams:

enter image description here

Now in my version I also want to add an edit field and other stuff to the popup.

I started off trying to use a TPopup, but the issue with this is that if you click on a TEdit inside the popup the popup will close. Due to this code in WndProc in FMX.Platform.Win:

WM_ACTIVATE:
  begin
    if not ((TFmxFormState.Recreating in LForm.FormState) or (LForm.FormStyle = TFormStyle.Popup) or
      WindowHandleToPlatform(LForm.Handle).FDisableDeactivate) then
    begin
      if LoWord(wParam) <> 0 then
      begin
        if HiWord(wParam) = 0 then
          LForm.Activate;
        // If the window is minimized, then do nothing.
      end
      else
      begin
        PrepareClosePopups;
        LForm.Deactivate;
        ClosePopupList; // This is called when clicking on edit on popup
      end;
    end;
    Result := 0;
  end;

ClosePopupList is called when clicking on the TEdit inside the popup and this function closes the popup.

So I switched to using a TForm and that fixes that issue, but after showing the form with Show my memo doesn't have focus anymore.

I would like to keep my focus on the memo except when we click on the edit inside the popup, in that case the edit should get focus and the popup should remain open. If I click outside the popup then the popup should close.

Is there a way to fix this?


Solution

  • This should work.

    MainFormUnit

    var MainForm:TMainForm;
    
    implementation
    {$R *.fmx}
    uses PopupFormUnit;
    
    procedure TMainForm.FormMouseDown(Sender:TObject; Button:TMouseButton; Shift:TShiftState; X,Y:Single);
    begin
    //If you click anywhere on the main form that doesn't take focus, this will hide the popup.
      PopupForm.Hide;
    end;
    
    procedure TMainForm.MemoClick(Sender:TObject);
    //My trigger for testing purposes.  You do what you need to to trigger the popup
    begin
      OpenPopup;
    end;
    
    procedure TMainForm.MemoExit(Sender:TObject);
    begin
    //If you click on any other focusable control on the main form, you want the popup to hide.
    //See the note about the timer.
      PopupTimer.Enabled:=True;
    end;
    
    procedure TMainForm.OpenPopup;
    begin
      PopupForm.Show;  //Show the popup
      Active:=True;    //Return the focus to the main form.
    end;
    
    procedure TMainForm.PopupTimerTimer(Sender:TObject);
    begin
      PopupForm.Hide;            //Hide the popup
      PopupTimer.Enabled:=False; //And turn the timer off.
    end;
    
    end.
    

    PopupFormUnit

    NOTE: Set PopupForm.FormStyle to StayOnTop.

    var
      PopupForm: TPopupForm;
    
    implementation
    
    {$R *.fmx}
    
    uses MainFormUnit;
    
    procedure TPopupForm.FormActivate(Sender:TObject);
    begin
      //We want to keep the popup from showing, so cancel the timer.
      MainForm.PopupTimer.Enabled:=False;
    end;
    
    end.
    

    Timer

    When you click on the popup, it becomes activated, but the main form's memo loses focus first. So if TMainForm.MemoExit directly hid the popup, the popup would be hidden. So instead, we start a timer which will hide the popup when it's finished. To avoid hiding the popup when we've just clicked on it, we have TPopupForm.FormActivate cancel the timer, so that in this case only, the popup remains showing. In other cases, the popup closes.

    The popup activation happens immediately after the memo loses focus, so you don't need the timer's interval to be very long. I set it to 10 (milliseconds) and that worked okay for me.

    I put the timer on the main form, but it should be okay to put it on the popup form, which would avoid the need for the popup form unit to reference the main form unit.