// 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);
}
}
}