// Copyright © 2021 The CefSharp Authors. All rights reserved. // // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. using System; using System.Collections.Generic; using System.Windows.Forms; using CefSharp.WinForms.Host; using CefSharp.WinForms.Internals; namespace CefSharp.WinForms.Handler { /// /// Called beforethe popup is created, can be used to cancel popup creation if required /// or modify . /// It's important to note that the methods of this interface are called on a CEF UI thread, /// which by default is not the same as your application UI thread. /// /// the ChromiumWebBrowser control /// The browser instance that launched this popup. /// The HTML frame that launched this popup. /// The URL of the popup content. (This may be empty/null) /// The name of the popup. (This may be empty/null) /// The value indicates where the user intended to /// open the popup (e.g. current tab, new tab, etc) /// The value will be true if the popup was opened via explicit user gesture /// (e.g. clicking a link) or false if the popup opened automatically (e.g. via the DomContentLoaded event). /// browser settings, defaults to source browsers /// To cancel creation of the popup return true otherwise return false. public delegate PopupCreation OnBeforePopupCreatedDelegate(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IBrowserSettings browserSettings); /// /// Called when the has been created. /// When called you must add the control to it's intended parent /// so the can be calculated to set the initial /// size correctly. /// /// popup host control /// url public delegate void OnPopupCreatedDelegate(ChromiumHostControl control, string url); /// /// Called when the instance has been created. /// The reference will be valid until is called /// /// popup host control, maybe null if Browser is hosted in a native Popup window. /// DevTools by default will be hosted in a native popup window. /// browser public delegate void OnPopupBrowserCreatedDelegate(ChromiumHostControl control, IBrowser browser); /// /// Called when the is to be removed from it's parent. /// When called you must remove/dispose of the . /// /// popup host control /// browser public delegate void OnPopupDestroyedDelegate(ChromiumHostControl control, IBrowser browser); /// /// Called to create a new instance of . Allows creation of a derived /// implementation of . /// /// A custom instance of . public delegate ChromiumHostControl CreatePopupChromiumHostControl(); /// /// A WinForms Specific implementation that simplifies /// the process of hosting a Popup as a Control/Tab. /// This implementation returns true in /// so no WM_CLOSE message is sent, this differs from the default CEF behaviour. /// public class LifeSpanHandler : CefSharp.Handler.LifeSpanHandler { private readonly Dictionary popupParentFormMessageInterceptors = new Dictionary(); private OnBeforePopupCreatedDelegate onBeforePopupCreated; private OnPopupDestroyedDelegate onPopupDestroyed; private OnPopupBrowserCreatedDelegate onPopupBrowserCreated; private OnPopupCreatedDelegate onPopupCreated; private CreatePopupChromiumHostControl chromiumHostControlCreatedDelegate; /// /// Default constructor /// /// Optional delegate used to create custom instances. public LifeSpanHandler(CreatePopupChromiumHostControl chromiumHostControlCreatedDelegate = null) { this.chromiumHostControlCreatedDelegate = chromiumHostControlCreatedDelegate; } /// protected override bool DoClose(IWebBrowser chromiumWebBrowser, IBrowser browser) { if (browser.IsPopup) { var control = ChromiumHostControl.FromBrowser(browser); //We don't have a parent control so we allow the default behaviour, required to close //default popups e.g. DevTools if (control == null) { return false; } //If the main browser is disposed or the handle has been released then we don't //need to remove the popup (likely removed from menu) if (!control.IsDisposed && control.IsHandleCreated) { try { //We need to invoke in a sync fashion so our IBrowser object is still in scope //Calling in an async fashion leads to the IBrowser being disposed before we //can access it. control.InvokeSyncOnUiThreadIfRequired(new Action(() => { onPopupDestroyed?.Invoke(control, browser); control.Dispose(); })); } catch (ObjectDisposedException) { // If the popup is being hosted on a Form that is being // Closed/Disposed as we attempt to call Control.Invoke // we can end up with an ObjectDisposedException // return false (Default behaviour). return false; } } } //No WM_CLOSE message will be sent, manually handle closing return true; } /// protected override void OnAfterCreated(IWebBrowser chromiumWebBrowser, IBrowser browser) { if (browser.IsPopup) { //WinForms will kindly lookup the child control from it's handle //If no parentControl then likely it's a native popup created by CEF //(Devtools by default will open as a popup, at this point the Url hasn't been set, so // we're going with this assumption as it fits the use case currently) var control = ChromiumHostControl.FromBrowser(browser); //If control is null then we'll treat as a native popup (do nothing) //If control is disposed there's nothing for us to do either. if (control != null && !control.IsDisposed) { control.BrowserHwnd = browser.GetHost().GetWindowHandle(); control.InvokeOnUiThreadIfRequired(() => { var interceptor = new ParentFormMessageInterceptor(control); interceptor.Moving += (sender, args) => { if (!browser.IsDisposed) { browser?.GetHost()?.NotifyMoveOrResizeStarted(); } }; popupParentFormMessageInterceptors.Add(browser.Identifier, interceptor); }); control.BrowserCore = browser; control.RaiseIsBrowserInitializedChangedEvent(); } onPopupBrowserCreated?.Invoke(control, browser); } } /// protected override void OnBeforeClose(IWebBrowser chromiumWebBrowser, IBrowser browser) { if (!browser.IsDisposed && browser.IsPopup) { ParentFormMessageInterceptor interceptor; if (popupParentFormMessageInterceptors.TryGetValue(browser.Identifier, out interceptor)) { popupParentFormMessageInterceptors.Remove(browser.Identifier); interceptor?.Dispose(); } } } /// /// /// NOTE: DevTools popups DO NOT trigger OnBeforePopup. /// protected override bool OnBeforePopup(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) { newBrowser = null; PopupCreation userAction = onBeforePopupCreated?.Invoke(chromiumWebBrowser, browser, frame, targetUrl, targetFrameName, targetDisposition, userGesture, browserSettings) ?? PopupCreation.Continue; //Cancel popup creation if(userAction == PopupCreation.Cancel) { return true; } if(userAction == PopupCreation.ContinueWithJavascriptDisabled) { noJavascriptAccess = true; } //No action so we'll go with the default behaviour. if (onPopupCreated == null) { return false; } var webBrowser = (ChromiumWebBrowser)chromiumWebBrowser; //Load and Display Handlers are used to trigger the relevant events. //If they are already assigned we'll leave the user preference in place if (webBrowser.LoadHandler == null) { webBrowser.LoadHandler = new LoadHandler(); } if (webBrowser.DisplayHandler == null) { webBrowser.DisplayHandler = new DisplayHandler(); } //We need to execute sync here so IWindowInfo.SetAsChild is called before we return false; webBrowser.InvokeSyncOnUiThreadIfRequired(new Action(() => { ChromiumHostControl control = chromiumHostControlCreatedDelegate?.Invoke(); if (control == null) { control = new ChromiumHostControl { Dock = DockStyle.Fill }; } control.CreateControl(); onPopupCreated?.Invoke(control, targetUrl); var rect = control.ClientRectangle; var windowBounds = new CefSharp.Structs.Rect(rect.X, rect.Y, rect.Width, rect.Height); windowInfo.SetAsChild(control.Handle, windowBounds); })); return false; } /// /// The will be called before the popup has been created and /// can be used to cancel popup creation if required or modify . /// /// Action to be invoked before popup is created. /// instance allowing you to chain method calls together public LifeSpanHandler OnBeforePopupCreated(OnBeforePopupCreatedDelegate onBeforePopupCreated) { this.onBeforePopupCreated = onBeforePopupCreated; return this; } /// /// The will be called when the has been /// created. When the is called you must add the control to it's intended parent /// so the can be calculated to set the initial /// size correctly. /// /// Action to be invoked when the Popup host has been created and is ready to be attached to it's parent. /// instance allowing you to chain method calls together public LifeSpanHandler OnPopupCreated(OnPopupCreatedDelegate onPopupCreated) { this.onPopupCreated = onPopupCreated; return this; } /// /// The will be called when the has been /// created. The instance is valid until /// is called. provides low level access to the CEF Browser, you can access frames, view source, /// perform navigation (via frame) etc. /// /// Action to be invoked when the has been created. /// instance allowing you to chain method calls together public LifeSpanHandler OnPopupBrowserCreated(OnPopupBrowserCreatedDelegate onPopupBrowserCreated) { this.onPopupBrowserCreated = onPopupBrowserCreated; return this; } /// /// The will be called when the is to be /// removed from it's parent. /// When the is called you must remove/dispose of the . /// /// Action to be invoked when the Popup is to be destroyed. /// instance allowing you to chain method calls together public LifeSpanHandler OnPopupDestroyed(OnPopupDestroyedDelegate onPopupDestroyed) { this.onPopupDestroyed = onPopupDestroyed; return this; } /// /// Create a new instance of the /// which can be used to create a WinForms specific /// implementation that simplifies the process of hosting a Popup as a Control/Tab. /// In scnarios where you also need to implement then instead /// of implementing directly you will need to inherit from . /// As it provides base functionality required to make events work correctly. /// /// /// A which can be used to fluently create an . /// Call to create the actual instance after you have call /// etc. /// public static LifeSpanHandlerBuilder Create(CreatePopupChromiumHostControl chromiumHostControlCreatedDelegate = null) { return new LifeSpanHandlerBuilder(chromiumHostControlCreatedDelegate); } } }