unit uCEFBrowserWindow; {$mode objfpc}{$H+} {$i cef.inc} interface uses {$IFDEF FPC} LResources, PropEdits, {$ENDIF} uCEFApplication, uCEFChromiumWindow, uCEFTypes, uCEFInterfaces, uCEFChromium, uCEFLinkedWinControlBase, uCEFBrowserViewComponent, uCEFChromiumEvents, Forms, ExtCtrls, Controls, Classes, sysutils; type (* On cocoa closing a browser does not work, while the application is in any other event. I.e. if the App is in a button-press event, then the browser will only close once that event was finished. *) { TEmbeddedChromium 1) TEmbeddedChromium keeps track of the browser while it is created. This allows for CloseBrowser to function, even if the Browser object is not yet known. Also calls to "LoadUrl" are cached until the browser object is created. 2) TEmbeddedChromium adds InternalEvents that can be hooked by the component that owns the TEmbeddedChromium. This means the default published events are available to the end user. Published events that should not be available are hidden via THiddenPropertyEditor * Hidden event properties must not be assigned by any end user code. * } TEmbeddedChromium = class(TChromium) private type TChromiumBrowserState = (csNoBrowser, csCreatingBrowser, csHasBrowser, csClosingBrowser, csCloseAfterCreate); private FInternalOnGotFocus: TOnGotFocus; FState : TChromiumBrowserState; FInternalOnBrowserClosed : TNotifyEvent; FInternalOnBrowserCreated : TNotifyEvent; FLoadUrl, FFrameName : ustring; function GetIsClosing: Boolean; procedure SetInternalOnClose(AValue: TOnClose); protected function GetHasBrowser : boolean; reintroduce; procedure doOnBeforeClose(const ABrowser: ICefBrowser); override; procedure doOnAfterCreated(const ABrowser: ICefBrowser); override; procedure doOnGotFocus(const Abrowser: ICefBrowser); override; function MustCreateFocusHandler: boolean; override; procedure DoCreated(Data: PtrInt); procedure DoOnClosed(Data: PtrInt); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; function CreateBrowser(const aBrowserParent: TWinControl = nil; const aWindowName: ustring = ''; const aContext: ICefRequestContext = nil; const aExtraInfo: ICefDictionaryValue = nil): boolean; overload; override; function CreateBrowser(aParentHandle: TCefWindowHandle; aParentRect: TRect; const aWindowName: ustring = ''; const aContext: ICefRequestContext = nil; const aExtraInfo: ICefDictionaryValue = nil; aForceAsPopup : boolean = False): boolean; overload; override; function CreateBrowser(const aURL: ustring; const aBrowserViewComp: TCEFBrowserViewComponent; const aContext: ICefRequestContext = nil; const aExtraInfo: ICefDictionaryValue = nil): boolean; overload; override; // CloseBrowser will work, even if the browser is still in creation, and Initialized is still false procedure CloseBrowser(aForceClose: boolean); reintroduce; // LoadURL will work, even if the browser is still in creation, and Initialized is still false procedure LoadURL(const aURL: ustring; const aFrameName: ustring = ''); overload; property HasBrowser: Boolean read GetHasBrowser; // Includes browser in creation property IsClosing : Boolean read GetIsClosing; (* - Events for use by the Owning component ONLY - Events are called in main thread - OnBrowserCreated: the parent event may be called when procedure Initialized is still false. - OnBrowserCreated: may not be called, if the CloseBrowser has already been called *) property InternalOnBrowserCreated : TNotifyEvent read FInternalOnBrowserCreated write FInternalOnBrowserCreated; property InternalOnBrowserClosed : TNotifyEvent read FInternalOnBrowserClosed write FInternalOnBrowserClosed; property InternalOnGotFocus : TOnGotFocus read FInternalOnGotFocus write FInternalOnGotFocus; end; TBrowserWindow = class; { TChromiumWrapper } TChromiumWrapper = class protected type TWrapperState = (wsNone, wsWaitingForClose, wsSentCloseEventAfterWait, wsDestroyAfterWait); protected FChromium : TEmbeddedChromium; FWrapperState : TWrapperState; FBrowserWindow : TBrowserWindow; procedure DoOnAfterCreated(Sender: TObject); procedure DoOnBeforeClose(Sender: TObject); procedure BrowserThread_OnClose(Sender: TObject; const browser: ICefBrowser; var aAction : TCefCloseBrowserAction); {$IFDEF FPC} procedure BrowserThread_OnGotFocus(Sender: TObject; const browser: ICefBrowser); {$ENDIF} procedure MaybeDestroy; public constructor Create(AOwner: TBrowserWindow); reintroduce; destructor Destroy; override; function CreateBrowser: boolean; procedure LoadURL(aURL: ustring); procedure CloseBrowser(aForceClose: boolean); function IsClosed: boolean; (* WaitForBrowserClosed calls ProcessMessages. It therefore is possible that the TBrowserWindow will be destroyed when this method returns. It is the callers responsibility to take any necessary precaution. *) procedure WaitForBrowserClosed; published property Chromium: TEmbeddedChromium read FChromium; end; { TBrowserWindow A simple "drop on the Form" component for an full embedded browser. The component handles most events required by CEF. The only additions needed to be made by the User in their code are: * Implement TForm.OnCloseQuery CEF must be able to destroy the browser, before the main form is closed. (That is while the Form still has a Handle, and the event loop is still running) It is adviced to do the same for any other form (other than the main form). TForm.OnCloseQuery should call (for each TBrowserWindow) TBrowserWindow.CloseBrowser(True); The Form can be allowed to close by setting (checking for all BrowserWindows) CanClose := BrowserWindow.IsClosed; On Windows and Linux it is also possible to Destroy the TBrowserWindow. This will wait for the browser to close, and after that the form can be closed. - However, this must be done in OnCloseQuery (or before). - Once TForm.Destroy is called, it is to late. By that time the event loop no longer runs. *** IMPORTANT: (MacOS) *** On MacOs CloseBrowser() must be called, and the *event* must be awaited. Neither destroying the component, nor waiting with App.ProcessMessages will work. On MacOS, CEF will not finish until the OnCloseQuery event returned to the main event loop. (Hence ProcessMessage does not work). The same is true for any action taken in OnClick or other event. CEF always waits for any event to return to the main event loop. See also the BrowserWindowEX example how that affect modal forms. * Implement TBrowserWindow.OnBrowserClosed If TForm.OnCloseQuery called CloseBrowser, this callback can be used to call Form.Close again (the callback should check if the browser was closed by OnCloseQuery. * On Windows: handle the WM_ENTERMENULOOP and WM_EXITMENULOOP, as shown in examples * Optional prevent pop-up windows by implementing Chromium.BeforePopup Chromium.OpenUrlFromTab } TBrowserWindow = class(TCEFLinkedWinControlBase) private FChromiumWrapper : TChromiumWrapper; FOnBrowserClosed : TNotifyEvent; FOnBrowserCreated : TNotifyEvent; FTimer : TTimer; procedure DoCreateBrowser(Sender: TObject); procedure DoCreateBrowserAfterContext(Sender: TObject); function GetEmbeddedChromium: TEmbeddedChromium; protected function GetChromium: TChromium; override; procedure DestroyHandle; override; procedure RealizeBounds; override; procedure DoEnter; override; procedure DoExit; override; procedure DoOnCreated; procedure DoOnClosed(Data: PtrInt); procedure DoOnFocus(Data: PtrInt); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure CreateHandle; override; procedure CloseBrowser(aForceClose: boolean); procedure WaitForBrowserClosed; function IsClosed: boolean; procedure LoadURL(aURL: ustring); published property Chromium: TEmbeddedChromium read GetEmbeddedChromium; property OnBrowserCreated : TNotifyEvent read FOnBrowserCreated write FOnBrowserCreated; (* OnBrowserClosed will not be called, if the TBrowserWindow is destroyed/destroying before the browser is closed. *) property OnBrowserClosed : TNotifyEvent read FOnBrowserClosed write FOnBrowserClosed; end; {$IFDEF FPC} procedure Register; {$ENDIF} implementation { TEmbeddedChromium } function TEmbeddedChromium.GetIsClosing: Boolean; begin Result := FState in [csCloseAfterCreate, csClosingBrowser]; end; procedure TEmbeddedChromium.SetInternalOnClose(AValue: TOnClose); begin inherited OnClose := AValue; end; function TEmbeddedChromium.GetHasBrowser: boolean; begin Result := (FState <> csNoBrowser) or (inherited GetHasBrowser); end; procedure TEmbeddedChromium.doOnBeforeClose(const ABrowser: ICefBrowser); begin inherited doOnBeforeClose(ABrowser); FState := csNoBrowser; Application.QueueAsyncCall(@DoOnClosed, 0); end; procedure TEmbeddedChromium.doOnAfterCreated(const ABrowser: ICefBrowser); begin inherited doOnAfterCreated(ABrowser); (* We may still be in Chromium.CreateBrowserSync In that case initialization will happen after this event, but before the call to CreateBrowser returns *) Application.QueueAsyncCall(@DoCreated, 0); end; procedure TEmbeddedChromium.doOnGotFocus(const Abrowser: ICefBrowser); begin inherited doOnGotFocus(Abrowser); if Assigned(FInternalOnGotFocus) then FInternalOnGotFocus(Self, Abrowser); end; function TEmbeddedChromium.MustCreateFocusHandler: boolean; begin Result := assigned(FInternalOnGotFocus) or inherited MustCreateFocusHandler; end; procedure TEmbeddedChromium.DoCreated(Data: PtrInt); var u, f: ustring; begin // Any other state, means this is a late async call case FState of csCreatingBrowser: begin FState := csHasBrowser; if FLoadUrl <> '' then begin u := FLoadUrl; f := FFrameName; LoadURL(u, f); end; if (FInternalOnBrowserCreated <> nil) then FInternalOnBrowserCreated(Self); end; csCloseAfterCreate: begin FState := csHasBrowser; CloseBrowser(True); end; end; end; procedure TEmbeddedChromium.DoOnClosed(Data: PtrInt); begin if (FInternalOnBrowserClosed <> nil) then FInternalOnBrowserClosed(Self); end; constructor TEmbeddedChromium.Create(AOwner: TComponent); begin FState := csNoBrowser; inherited Create(AOwner); SetSubComponent(True); Name := 'Chromium'; end; destructor TEmbeddedChromium.Destroy; begin inherited Destroy; Application.RemoveAsyncCalls(Self); end; function TEmbeddedChromium.CreateBrowser(const aBrowserParent: TWinControl; const aWindowName: ustring; const aContext: ICefRequestContext; const aExtraInfo: ICefDictionaryValue): boolean; begin FState := csCreatingBrowser; Result := inherited CreateBrowser(aBrowserParent, aWindowName, aContext, aExtraInfo); if Initialized then DoCreated(0); end; function TEmbeddedChromium.CreateBrowser(aParentHandle: TCefWindowHandle; aParentRect: TRect; const aWindowName: ustring; const aContext: ICefRequestContext; const aExtraInfo: ICefDictionaryValue; aForceAsPopup : boolean): boolean; begin FState := csCreatingBrowser; Result := inherited CreateBrowser(aParentHandle, aParentRect, aWindowName, aContext, aExtraInfo, aForceAsPopup); if Initialized then DoCreated(0); end; function TEmbeddedChromium.CreateBrowser(const aURL: ustring; const aBrowserViewComp: TCEFBrowserViewComponent; const aContext: ICefRequestContext; const aExtraInfo: ICefDictionaryValue ): boolean; begin FState := csCreatingBrowser; Result := inherited CreateBrowser(aURL, aBrowserViewComp, aContext, aExtraInfo); if Initialized then DoCreated(0); end; procedure TEmbeddedChromium.CloseBrowser(aForceClose: boolean); begin if FState = csCreatingBrowser then begin FState := csCloseAfterCreate; exit; end else if FState in [csHasBrowser] then begin FState := csClosingBrowser; inherited CloseBrowser(aForceClose); end; end; procedure TEmbeddedChromium.LoadURL(const aURL: ustring; const aFrameName: ustring); begin FLoadUrl := ''; FFrameName := ''; if FState = csHasBrowser then begin inherited LoadURL(aURL, aFrameName); end else begin FLoadUrl := aURL; FFrameName := aFrameName; end; end; { TChromiumWrapper } procedure TChromiumWrapper.DoOnAfterCreated(Sender: TObject); begin if (FBrowserWindow <> nil) then FBrowserWindow.DoOnCreated; end; procedure TChromiumWrapper.BrowserThread_OnClose(Sender: TObject; const browser: ICefBrowser; var aAction: TCefCloseBrowserAction); begin (* FBrowserWindow should always be <> nil If FBrowserWindow is nil (MacOS) then the FBrowserWindow.Handle is destroyed too, and CEF should call BeforeClose, without calling DoClose *) if (FBrowserWindow <> nil) and FBrowserWindow.DestroyChildWindow then aAction := cbaDelay else aAction := cbaClose; end; procedure TChromiumWrapper.DoOnBeforeClose(Sender: TObject); begin if (FBrowserWindow <> nil) then begin if FWrapperState = wsWaitingForClose then FWrapperState := wsSentCloseEventAfterWait else FBrowserWindow.DoOnClosed(0); end; end; procedure TChromiumWrapper.BrowserThread_OnGotFocus(Sender: TObject; const browser: ICefBrowser); begin if (FBrowserWindow <> nil) then Application.QueueAsyncCall(@FBrowserWindow.DoOnFocus, 0); end; procedure TChromiumWrapper.MaybeDestroy; begin if FChromium.Owner <> nil then FBrowserWindow.RemoveComponent(FChromium); CloseBrowser(True); FBrowserWindow := nil; if FWrapperState in [wsWaitingForClose, wsSentCloseEventAfterWait] then FWrapperState := wsDestroyAfterWait; if not FChromium.HasBrowser then Destroy; end; constructor TChromiumWrapper.Create(AOwner: TBrowserWindow); begin FBrowserWindow := AOwner; FWrapperState := wsNone; FChromium := TEmbeddedChromium.Create(AOwner); if not(csDesigning in AOwner.ComponentState) then begin FChromium.OnClose := {$IFDEF FPC}@{$ENDIF}BrowserThread_OnClose; FChromium.InternalOnBrowserClosed := {$IFDEF FPC}@{$ENDIF}DoOnBeforeClose; FChromium.InternalOnBrowserCreated := {$IFDEF FPC}@{$ENDIF}DoOnAfterCreated; {$IFDEF LINUX} // This is a workaround for the CEF issue #2026. Read below for more info. FChromium.InternalOnGotFocus := {$IFDEF FPC}@{$ENDIF}BrowserThread_OnGotFocus; {$ENDIF} end; inherited Create; end; destructor TChromiumWrapper.Destroy; begin if FChromium.HasBrowser then WaitForBrowserClosed; inherited Destroy; FChromium.Destroy; Application.RemoveAsyncCalls(Self); end; function TChromiumWrapper.CreateBrowser: boolean; begin if FChromium.HasBrowser then exit(False); Result := FChromium.CreateBrowser(FBrowserWindow, ''); end; procedure TChromiumWrapper.LoadURL(aURL: ustring); begin FChromium.LoadURL(aURL); end; procedure TChromiumWrapper.CloseBrowser(aForceClose: boolean); begin FChromium.CloseBrowser(aForceClose); end; function TChromiumWrapper.IsClosed: boolean; begin Result := not FChromium.HasBrowser; end; procedure TChromiumWrapper.WaitForBrowserClosed; begin if not FChromium.HasBrowser then exit; FChromium.CloseBrowser(True); FWrapperState := wsWaitingForClose; while FChromium.HasBrowser do begin Application.ProcessMessages; if GlobalCEFApp.ExternalMessagePump then GlobalCEFApp.DoMessageLoopWork; sleep(5); end; if (FBrowserWindow <> nil) and (FWrapperState = wsSentCloseEventAfterWait) then Application.QueueAsyncCall(@FBrowserWindow.DoOnClosed, 0); if FWrapperState = wsDestroyAfterWait then Destroy else FWrapperState := wsNone; end; { TBrowserWindow } procedure TBrowserWindow.DoCreateBrowser(Sender: TObject); begin if FTimer <> nil then FTimer.Enabled := False; if FChromiumWrapper.Chromium.HasBrowser then begin if not FChromiumWrapper.Chromium.IsClosing then begin FreeAndNil(FTimer); exit; end else begin FChromiumWrapper.MaybeDestroy; FChromiumWrapper := TChromiumWrapper.Create(Self); end; end; if FChromiumWrapper.CreateBrowser then begin FreeAndNil(FTimer); end else begin if GlobalCEFApp.ExternalMessagePump then GlobalCEFApp.DoMessageLoopWork; if FTimer = nil then FTimer := TTimer.Create(Self); FTimer.OnTimer := @DoCreateBrowser; FTimer.Interval := 100; FTimer.Enabled := True; end; end; procedure TBrowserWindow.DoCreateBrowserAfterContext(Sender: TObject); begin {$IFnDEF WINDOWS} FTimer := TTimer.Create(Self); FTimer.Interval := 20; FTimer.OnTimer := @DoCreateBrowser; FTimer.Enabled := True; {$ELSE} DoCreateBrowser(nil); {$ENDIF} end; function TBrowserWindow.GetEmbeddedChromium: TEmbeddedChromium; begin Result := FChromiumWrapper.Chromium; end; function TBrowserWindow.GetChromium: TChromium; begin Result := FChromiumWrapper.FChromium; end; procedure TBrowserWindow.CreateHandle; begin inherited CreateHandle; if not (csDesigning in ComponentState) then begin (* On Windows we can create the browser immediately. But at least on Linux, we need to wait *) GlobalCEFApp.AddContextInitializedHandler(@DoCreateBrowserAfterContext); end; end; procedure TBrowserWindow.DestroyHandle; begin if FTimer <> nil then FreeAndNil(FTimer); if (GlobalCEFApp = nil) or (not FChromiumWrapper.Chromium.HasBrowser) or (csDesigning in ComponentState) then begin inherited DestroyHandle; exit; end; {$IFDEF MACOSX} inherited DestroyHandle; FChromiumWrapper.CloseBrowser(True); {$ELSE} FChromiumWrapper.WaitForBrowserClosed; inherited DestroyHandle; {$ENDIF} end; procedure TBrowserWindow.RealizeBounds; begin inherited RealizeBounds; if not (csDesigning in ComponentState) and HandleAllocated then Chromium.NotifyMoveOrResizeStarted; end; procedure TBrowserWindow.DoEnter; begin inherited DoEnter; If not(csDesigning in ComponentState) then Chromium.SetFocus(True); end; procedure TBrowserWindow.DoExit; begin inherited DoExit; if not(csDesigning in ComponentState) then Chromium.SendCaptureLostEvent; end; procedure TBrowserWindow.DoOnCreated; begin {$IFDEF FPC}{$IFDEF LINUX} Chromium.UpdateXWindowVisibility(Visible); Chromium.UpdateBrowserSize(Left, Top, Width, Height); {$ENDIF}{$ENDIF} if Assigned(FOnBrowserCreated) then FOnBrowserCreated(Self); end; procedure TBrowserWindow.DoOnClosed(Data: PtrInt); begin if (not(csDestroying in ComponentState)) and Assigned(FOnBrowserClosed) then FOnBrowserClosed(Self); end; procedure TBrowserWindow.DoOnFocus(Data: PtrInt); begin SetFocus; end; constructor TBrowserWindow.Create(AOwner: TComponent); begin FChromiumWrapper := TChromiumWrapper.Create(Self); inherited Create(AOwner); ControlStyle := ControlStyle + [csOwnedChildrenNotSelectable]; end; destructor TBrowserWindow.Destroy; begin RemoveComponent(FChromiumWrapper.FChromium); inherited Destroy; FChromiumWrapper.MaybeDestroy; Application.RemoveAsyncCalls(Self); end; procedure TBrowserWindow.CloseBrowser(aForceClose: boolean); begin FChromiumWrapper.CloseBrowser(aForceClose); end; procedure TBrowserWindow.WaitForBrowserClosed; begin FChromiumWrapper.WaitForBrowserClosed; end; function TBrowserWindow.IsClosed: boolean; begin Result := FChromiumWrapper.IsClosed; end; procedure TBrowserWindow.LoadURL(aURL: ustring); begin FChromiumWrapper.LoadURL(aURL); end; {$IFDEF FPC} procedure Register; begin {$I res/TBrowserWindow.lrs} RegisterComponents('Chromium', [TBrowserWindow]); RegisterClass(TEmbeddedChromium); RegisterPropertyEditor(TypeInfo(TOnClose), TEmbeddedChromium, 'OnClose', THiddenPropertyEditor); end; {$ENDIF} end.