1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// 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
{
    /// <summary>
    /// Called <b>before</b>the popup is created, can be used to cancel popup creation if required
    /// or modify <see cref="IBrowserSettings"/>.
    /// 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.
    /// </summary>
    /// <param name="chromiumWebBrowser">the ChromiumWebBrowser control</param>
    /// <param name="browser">The browser instance that launched this popup.</param>
    /// <param name="frame">The HTML frame that launched this popup.</param>
    /// <param name="targetUrl">The URL of the popup content. (This may be empty/null)</param>
    /// <param name="targetFrameName">The name of the popup. (This may be empty/null)</param>
    /// <param name="targetDisposition">The value indicates where the user intended to
    /// open the popup (e.g. current tab, new tab, etc)</param>
    /// <param name="userGesture">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).</param>
    /// <param name="browserSettings">browser settings, defaults to source browsers</param>
    /// <returns>To cancel creation of the popup return true otherwise return false.</returns>
    public delegate PopupCreation OnBeforePopupCreatedDelegate(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IBrowserSettings browserSettings);
 
    /// <summary>
    /// Called when the <see cref="ChromiumHostControl"/> has been created.
    /// When called you must add the control to it's intended parent
    /// so the <see cref="Control.ClientRectangle"/> can be calculated to set the initial
    /// size correctly.
    /// </summary>
    /// <param name="control">popup host control</param>
    /// <param name="url">url</param>
    public delegate void OnPopupCreatedDelegate(ChromiumHostControl control, string url);
 
    /// <summary>
    /// Called when the <see cref="IBrowser"/> instance has been created.
    /// The <see cref="IBrowser"/> reference will be valid until <see cref="OnPopupDestroyedDelegate"/> is called
    /// </summary>
    /// <param name="control">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.</param>
    /// <param name="browser">browser</param>
    public delegate void OnPopupBrowserCreatedDelegate(ChromiumHostControl control, IBrowser browser);
 
    /// <summary>
    /// Called when the <see cref="ChromiumHostControl"/> is to be removed from it's parent.
    /// When called you must remove/dispose of the <see cref="ChromiumHostControl"/>.
    /// </summary>
    /// <param name="control">popup host control</param>
    /// <param name="browser">browser</param>
    public delegate void OnPopupDestroyedDelegate(ChromiumHostControl control, IBrowser browser);
 
    /// <summary>
    /// Called to create a new instance of <see cref="ChromiumHostControl"/>. Allows creation of a derived
    /// implementation of <see cref="ChromiumHostControl"/>.
    /// </summary>
    /// <returns>A custom instance of <see cref="ChromiumHostControl"/>.</returns>
    public delegate ChromiumHostControl CreatePopupChromiumHostControl();
 
    /// <summary>
    /// A WinForms Specific <see cref="ILifeSpanHandler"/> implementation that simplifies
    /// the process of hosting a Popup as a Control/Tab.
    /// This <see cref="ILifeSpanHandler"/> implementation returns true in <see cref="ILifeSpanHandler.DoClose(IWebBrowser, IBrowser)"/>
    /// so no WM_CLOSE message is sent, this differs from the default CEF behaviour.
    /// </summary>
    public class LifeSpanHandler : CefSharp.Handler.LifeSpanHandler
    {
        private readonly Dictionary<int, ParentFormMessageInterceptor> popupParentFormMessageInterceptors = new Dictionary<int, ParentFormMessageInterceptor>();
        private OnBeforePopupCreatedDelegate onBeforePopupCreated;
        private OnPopupDestroyedDelegate onPopupDestroyed;
        private OnPopupBrowserCreatedDelegate onPopupBrowserCreated;
        private OnPopupCreatedDelegate onPopupCreated;
        private CreatePopupChromiumHostControl chromiumHostControlCreatedDelegate;
 
        /// <summary>
        /// Default constructor
        /// </summary>
        /// <param name="chromiumHostControlCreatedDelegate">Optional delegate used to create custom <see cref="ChromiumHostControl"/> instances.</param>
        public LifeSpanHandler(CreatePopupChromiumHostControl chromiumHostControlCreatedDelegate = null)
        {
            this.chromiumHostControlCreatedDelegate = chromiumHostControlCreatedDelegate;
        }
 
        /// <inheritdoc/>
        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;
        }
 
        /// <inheritdoc/>
        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);
            }
        }
 
        /// <inheritdoc/>
        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();
                }
            }
        }
 
        /// <inheritdoc/>
        /// <remarks>
        /// NOTE: DevTools popups DO NOT trigger OnBeforePopup.
        /// </remarks>
        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;
        }
 
        /// <summary>
        /// The <see cref="OnBeforePopupCreatedDelegate"/> will be called <b>before</b> the popup has been created and
        /// can be used to cancel popup creation if required or modify <see cref="IBrowserSettings"/>.
        /// </summary>
        /// <param name="onBeforePopupCreated">Action to be invoked before popup is created.</param>
        /// <returns><see cref="LifeSpanHandler"/> instance allowing you to chain method calls together</returns>
        public LifeSpanHandler OnBeforePopupCreated(OnBeforePopupCreatedDelegate onBeforePopupCreated)
        {
            this.onBeforePopupCreated = onBeforePopupCreated;
 
            return this;
        }
 
        /// <summary>
        /// The <see cref="OnPopupCreatedDelegate"/> will be called when the<see cref="ChromiumHostControl"/> has been
        /// created. When the <see cref="OnPopupCreatedDelegate"/> is called you must add the control to it's intended parent
        /// so the <see cref="Control.ClientRectangle"/> can be calculated to set the initial
        /// size correctly.
        /// </summary>
        /// <param name="onPopupCreated">Action to be invoked when the Popup host has been created and is ready to be attached to it's parent.</param>
        /// <returns><see cref="LifeSpanHandler"/> instance allowing you to chain method calls together</returns>
        public LifeSpanHandler OnPopupCreated(OnPopupCreatedDelegate onPopupCreated)
        {
            this.onPopupCreated = onPopupCreated;
 
            return this;
        }
 
        /// <summary>
        /// The <see cref="OnPopupBrowserCreatedDelegate"/> will be called when the<see cref="IBrowser"/> has been
        /// created. The <see cref="IBrowser"/> instance is valid until <see cref="OnPopupDestroyed(OnPopupDestroyedDelegate)"/>
        /// is called. <see cref="IBrowser"/> provides low level access to the CEF Browser, you can access frames, view source,
        /// perform navigation (via frame) etc.
        /// </summary>
        /// <param name="onPopupBrowserCreated">Action to be invoked when the <see cref="IBrowser"/> has been created.</param>
        /// <returns><see cref="LifeSpanHandler"/> instance allowing you to chain method calls together</returns>
        public LifeSpanHandler OnPopupBrowserCreated(OnPopupBrowserCreatedDelegate onPopupBrowserCreated)
        {
            this.onPopupBrowserCreated = onPopupBrowserCreated;
 
            return this;
        }
 
        /// <summary>
        /// The <see cref="OnPopupDestroyedDelegate"/> will be called when the <see cref="ChromiumHostControl"/> is to be
        /// removed from it's parent.
        /// When the <see cref="OnPopupDestroyedDelegate"/> is called you must remove/dispose of the <see cref="ChromiumHostControl"/>.
        /// </summary>
        /// <param name="onPopupDestroyed">Action to be invoked when the Popup is to be destroyed.</param>
        /// <returns><see cref="LifeSpanHandler"/> instance allowing you to chain method calls together</returns>
        public LifeSpanHandler OnPopupDestroyed(OnPopupDestroyedDelegate onPopupDestroyed)
        {
            this.onPopupDestroyed = onPopupDestroyed;
 
            return this;
        }
 
        /// <summary>
        /// Create a new instance of the <see cref="LifeSpanHandlerBuilder"/>
        /// which can be used to create a WinForms specific <see cref="ILifeSpanHandler"/>
        /// implementation that simplifies the process of hosting a Popup as a Control/Tab.
        /// In scnarios where you also need to implement <see cref="ILoadHandler"/> then instead
        /// of implementing directly you will need to inherit from <see cref="CefSharp.WinForms.Handler.LoadHandler"/>.
        /// As it provides base functionality required to make <see cref="ChromiumHostControl"/> events work correctly.
        /// </summary>
        /// <returns>
        /// A <see cref="LifeSpanHandlerBuilder"/> which can be used to fluently create an <see cref="ILifeSpanHandler"/>.
        /// Call <see cref="LifeSpanHandlerBuilder.Build"/> to create the actual instance after you have call
        /// <see cref="LifeSpanHandlerBuilder.OnPopupCreated(OnPopupCreatedDelegate)"/> etc.
        /// </returns>
        public static LifeSpanHandlerBuilder Create(CreatePopupChromiumHostControl chromiumHostControlCreatedDelegate = null)
        {
            return new LifeSpanHandlerBuilder(chromiumHostControlCreatedDelegate);
        }
    }
}