// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. #include "CEF/CEFBrowserHandler.h" #if WITH_CEF3 #include "WebBrowserModule.h" #include "CEFBrowserClosureTask.h" #include "IWebBrowserSingleton.h" #include "WebBrowserSingleton.h" #include "CEFBrowserPopupFeatures.h" #include "CEFWebBrowserWindow.h" #include "CEFBrowserByteResource.h" #include "SlateApplication.h" #include "ThreadingBase.h" #define LOCTEXT_NAMESPACE "WebBrowserHandler" // Used to force returning custom content instead of performing a request. const FString CustomContentHeader(TEXT("X-UE-Content")); const FString CustomContentMethod(TEXT("X-GET-CUSTOM-CONTENT")); FCEFBrowserHandler::FCEFBrowserHandler(bool InUseTransparency) : bUseTransparency(InUseTransparency) { } void FCEFBrowserHandler::OnTitleChange(CefRefPtr Browser, const CefString& Title) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->SetTitle(Title); } } void FCEFBrowserHandler::OnAddressChange(CefRefPtr Browser, CefRefPtr Frame, const CefString& Url) { if (Frame->IsMain()) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->SetUrl(Url); } } } bool FCEFBrowserHandler::OnTooltip(CefRefPtr Browser, CefString& Text) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->SetToolTip(Text); } return false; } void FCEFBrowserHandler::OnAfterCreated(CefRefPtr Browser) { if(Browser->IsPopup()) { TSharedPtr BrowserWindowParent = ParentHandler.get() ? ParentHandler->BrowserWindowPtr.Pin() : nullptr; if(BrowserWindowParent.IsValid() && ParentHandler->OnCreateWindow().IsBound()) { TSharedPtr NewBrowserWindowInfo = MakeShareable(new FWebBrowserWindowInfo(Browser, this)); TSharedPtr NewBrowserWindow = IWebBrowserModule::Get().GetSingleton()->CreateBrowserWindow( BrowserWindowParent, NewBrowserWindowInfo ); { // @todo: At the moment we need to downcast since the handler does not support using the interface. TSharedPtr HandlerSpecificBrowserWindow = StaticCastSharedPtr(NewBrowserWindow); BrowserWindowPtr = HandlerSpecificBrowserWindow; } // Request a UI window for the browser. If it is not created we do some cleanup. bool bUIWindowCreated = ParentHandler->OnCreateWindow().Execute(TWeakPtr(NewBrowserWindow), TWeakPtr(BrowserPopupFeatures)); if(!bUIWindowCreated) { NewBrowserWindow->CloseBrowser(true); } else { checkf(!NewBrowserWindow.IsUnique(), TEXT("Handler indicated that new window UI was created, but failed to save the new WebBrowserWindow instance.")); } } else { Browser->GetHost()->CloseBrowser(true); } } } bool FCEFBrowserHandler::DoClose(CefRefPtr Browser) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if(BrowserWindow.IsValid()) { BrowserWindow->OnBrowserClosing(); } #if PLATFORM_WINDOWS // If we have a window handle, we're rendering directly to the screen and not off-screen HWND NativeWindowHandle = Browser->GetHost()->GetWindowHandle(); if (NativeWindowHandle != nullptr) { HWND ParentWindow = ::GetParent(NativeWindowHandle); if (ParentWindow) { HWND FocusHandle = ::GetFocus(); if (FocusHandle && (FocusHandle == NativeWindowHandle || ::IsChild(NativeWindowHandle, FocusHandle))) { // Set focus to the parent window, otherwise keyboard and mouse wheel input will become wonky ::SetFocus(ParentWindow); } // CEF will send a WM_CLOSE to the parent window and potentially exit the application if we don't do this ::SetParent(NativeWindowHandle, nullptr); } } #endif return false; } void FCEFBrowserHandler::OnBeforeClose(CefRefPtr Browser) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->OnBrowserClosed(); } } bool FCEFBrowserHandler::OnBeforePopup( CefRefPtr Browser, CefRefPtr Frame, const CefString& TargetUrl, const CefString& TargetFrameName, const CefPopupFeatures& PopupFeatures, CefWindowInfo& OutWindowInfo, CefRefPtr& OutClient, CefBrowserSettings& OutSettings, bool* OutNoJavascriptAccess ) { FString URL = TargetUrl.ToWString().c_str(); FString FrameName = TargetFrameName.ToWString().c_str(); /* If OnBeforePopup() is not bound, we allow creating new windows as long as OnCreateWindow() is bound. The BeforePopup delegate is always executed even if OnCreateWindow is not bound to anything . */ if((OnBeforePopup().IsBound() && OnBeforePopup().Execute(URL, FrameName)) || !OnCreateWindow().IsBound()) { return true; } else { TSharedPtr NewBrowserPopupFeatures = MakeShareable(new FCEFBrowserPopupFeatures(PopupFeatures)); // Allow overriding transparency setting for child windows bool shouldUseTransparency = bUseTransparency ? NewBrowserPopupFeatures->GetAdditionalFeatures().Find(TEXT("Epic_NoTransparency")) == INDEX_NONE : NewBrowserPopupFeatures->GetAdditionalFeatures().Find(TEXT("Epic_UseTransparency")) != INDEX_NONE; CefRefPtr NewHandler(new FCEFBrowserHandler(shouldUseTransparency)); NewHandler->ParentHandler = this; NewHandler->SetPopupFeatures(NewBrowserPopupFeatures); OutClient = NewHandler; // Always use off screen rendering so we can integrate with our windows OutWindowInfo.SetAsWindowless( #if PLATFORM_LINUX // may be applicable to other platforms, but I cannot test those at the moment kNullWindowHandle, #else nullptr, #endif // PLATFORM_LINUX shouldUseTransparency); // We need to rely on CEF to create our window so we set the WindowInfo, BrowserSettings, Client, and then return false return false; } } bool FCEFBrowserHandler::OnCertificateError(CefRefPtr Browser, cef_errorcode_t CertError, const CefString &RequestUrl, CefRefPtr SslInfo, CefRefPtr Callback) { // Forward the cert error to the normal load error handler CefString ErrorText = "Certificate error"; OnLoadError(Browser, Browser->GetMainFrame(), CertError, ErrorText, RequestUrl); return false; } void FCEFBrowserHandler::OnLoadError(CefRefPtr Browser, CefRefPtr Frame, CefLoadHandler::ErrorCode InErrorCode, const CefString& ErrorText, const CefString& FailedUrl) { if (InErrorCode == ERR_ABORTED) { return; } // notify browser window if (Frame->IsMain()) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { // Display a load error message. Note: The user's code will still have a chance to handle this error after this error message is displayed. if (BrowserWindow->IsShowingErrorMessages()) { FFormatNamedArguments Args; { Args.Add(TEXT("FailedUrl"), FText::FromString(FailedUrl.ToWString().c_str())); Args.Add(TEXT("ErrorText"), FText::FromString(ErrorText.ToWString().c_str())); Args.Add(TEXT("ErrorCode"), FText::AsNumber(InErrorCode)); } FText ErrorMsg = FText::Format(LOCTEXT("WebBrowserLoadError", "Failed to load URL {FailedUrl} with error {ErrorText} ({ErrorCode})."), Args); FString ErrorHTML = TEXT("

") + ErrorMsg.ToString() + TEXT("

"); Frame->LoadString(*ErrorHTML, FailedUrl); } BrowserWindow->NotifyDocumentError((int)InErrorCode); } } } void FCEFBrowserHandler::OnLoadStart(CefRefPtr Browser, CefRefPtr Frame) { } void FCEFBrowserHandler::OnLoadingStateChange(CefRefPtr Browser, bool bIsLoading, bool bCanGoBack, bool bCanGoForward) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->NotifyDocumentLoadingStateChange(bIsLoading); } } bool FCEFBrowserHandler::GetRootScreenRect(CefRefPtr Browser, CefRect& Rect) { FDisplayMetrics DisplayMetrics; FSlateApplication::Get().GetDisplayMetrics(DisplayMetrics); Rect.width = DisplayMetrics.PrimaryDisplayWidth; Rect.height = DisplayMetrics.PrimaryDisplayHeight; return true; } bool FCEFBrowserHandler::GetViewRect(CefRefPtr Browser, CefRect& Rect) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { return BrowserWindow->GetViewRect(Rect); } else { return false; } } void FCEFBrowserHandler::OnPaint(CefRefPtr Browser, PaintElementType Type, const RectList& DirtyRects, const void* Buffer, int Width, int Height) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->OnPaint(Type, DirtyRects, Buffer, Width, Height); } } void FCEFBrowserHandler::OnCursorChange(CefRefPtr Browser, CefCursorHandle Cursor, CefRenderHandler::CursorType Type, const CefCursorInfo& CustomCursorInfo) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->OnCursorChange(Cursor, Type, CustomCursorInfo); } } void FCEFBrowserHandler::OnPopupShow(CefRefPtr Browser, bool bShow) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->ShowPopupMenu(bShow); } } void FCEFBrowserHandler::OnPopupSize(CefRefPtr Browser, const CefRect& Rect) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->SetPopupMenuPosition(Rect); } } bool FCEFBrowserHandler::GetScreenInfo(CefRefPtr Browser, CefScreenInfo& ScreenInfo) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid() && BrowserWindow->GetParentWindow().IsValid()) { ScreenInfo.device_scale_factor = BrowserWindow->GetParentWindow()->GetNativeWindow()->GetDPIScaleFactor(); } else { FDisplayMetrics DisplayMetrics; FDisplayMetrics::GetDisplayMetrics(DisplayMetrics); ScreenInfo.device_scale_factor = FPlatformMisc::GetDPIScaleFactorAtPoint(DisplayMetrics.PrimaryDisplayWorkAreaRect.Left, DisplayMetrics.PrimaryDisplayWorkAreaRect.Top); } return true; } CefRequestHandler::ReturnValue FCEFBrowserHandler::OnBeforeResourceLoad(CefRefPtr Browser, CefRefPtr Frame, CefRefPtr Request, CefRefPtr Callback) { // Current thread is IO thread. We need to invoke BrowserWindow->GetResourceContent on the UI (aka Game) thread: CefPostTask(TID_UI, new FCEFBrowserClosureTask(this, [=]() { const FString LanguageHeaderText(TEXT("Accept-Language")); const FString LocaleCode = FWebBrowserSingleton::GetCurrentLocaleCode(); CefRequest::HeaderMap HeaderMap; Request->GetHeaderMap(HeaderMap); auto LanguageHeader = HeaderMap.find(*LanguageHeaderText); if (LanguageHeader != HeaderMap.end()) { (*LanguageHeader).second = *LocaleCode; } else { HeaderMap.insert(std::pair(*LanguageHeaderText, *LocaleCode)); } TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { TOptional Contents = BrowserWindow->GetResourceContent(Frame, Request); if(Contents.IsSet()) { // Set a custom request header, so that we can return it wrapped in a custom resource handler in GetResourceHandler later on HeaderMap.insert(std::pair(*CustomContentHeader, *Contents.GetValue())); // Change http method to tell GetResourceHandler to return the content Request->SetMethod(*CustomContentMethod); } } Request->SetHeaderMap(HeaderMap); Callback->Continue(true); })); // Tell CEF that we're handling this asynchronously. return RV_CONTINUE_ASYNC; } void FCEFBrowserHandler::OnRenderProcessTerminated(CefRefPtr Browser, TerminationStatus Status) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->OnRenderProcessTerminated(Status); } } bool FCEFBrowserHandler::OnBeforeBrowse(CefRefPtr Browser, CefRefPtr Frame, CefRefPtr Request, bool IsRedirect) { // Current thread: UI thread TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { if(BrowserWindow->OnBeforeBrowse(Browser, Frame, Request, IsRedirect)) { return true; } } return false; } CefRefPtr FCEFBrowserHandler::GetResourceHandler( CefRefPtr Browser, CefRefPtr< CefFrame > Frame, CefRefPtr< CefRequest > Request ) { if (Request->GetMethod() == *CustomContentMethod) { // Content override header will be set by OnBeforeResourceLoad before passing the request on to this. CefRequest::HeaderMap HeaderMap; Request->GetHeaderMap(HeaderMap); auto ContentOverride = HeaderMap.find(*CustomContentHeader); if (ContentOverride != HeaderMap.end()) { std::string Convert = ContentOverride->second.ToString(); return new FCEFBrowserByteResource(Convert.c_str(), Convert.length()); } } return nullptr; } void FCEFBrowserHandler::SetBrowserWindow(TSharedPtr InBrowserWindow) { BrowserWindowPtr = InBrowserWindow; } bool FCEFBrowserHandler::OnProcessMessageReceived(CefRefPtr Browser, CefProcessId SourceProcess, CefRefPtr Message) { bool Retval = false; TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { Retval = BrowserWindow->OnProcessMessageReceived(Browser, SourceProcess, Message); } return Retval; } bool FCEFBrowserHandler::ShowDevTools(const CefRefPtr& Browser) { CefPoint Point; CefString TargetUrl = "chrome-devtools://devtools/devtools.html"; CefString TargetFrameName = "devtools"; CefPopupFeatures PopupFeatures; CefWindowInfo WindowInfo; CefRefPtr NewClient; CefBrowserSettings BrowserSettings; bool NoJavascriptAccess = false; PopupFeatures.xSet = false; PopupFeatures.ySet = false; PopupFeatures.heightSet = false; PopupFeatures.widthSet = false; PopupFeatures.locationBarVisible = false; PopupFeatures.menuBarVisible = false; PopupFeatures.toolBarVisible = false; PopupFeatures.statusBarVisible = false; PopupFeatures.resizable = true; PopupFeatures.additionalFeatures = cef_string_list_alloc(); // Override the parent window setting for transparency, as the Dev Tools require a solid background CefString OverrideTransparencyFeature = "Epic_NoTransparency"; cef_string_list_append(PopupFeatures.additionalFeatures, OverrideTransparencyFeature.GetStruct()); // Add a custom tag used to detect dev tools. CefString DevToolsTag = "Epic_DevTools"; cef_string_list_append(PopupFeatures.additionalFeatures, DevToolsTag.GetStruct()); // Set max framerate to maximum supported. BrowserSettings.windowless_frame_rate = 60; // Disable plugins BrowserSettings.plugins = STATE_DISABLED; // Dev Tools look best with a white background color BrowserSettings.background_color = CefColorSetARGB(255, 255, 255, 255); // OnBeforePopup already takes care of all the details required to ask the host application to create a new browser window. bool bSuppressWindowCreation = OnBeforePopup(Browser, Browser->GetFocusedFrame(), TargetUrl, TargetFrameName, PopupFeatures, WindowInfo, NewClient, BrowserSettings, &NoJavascriptAccess); if(! bSuppressWindowCreation) { Browser->GetHost()->ShowDevTools(WindowInfo, NewClient, BrowserSettings, Point); } return !bSuppressWindowCreation; } bool FCEFBrowserHandler::OnKeyEvent(CefRefPtr Browser, const CefKeyEvent& Event, CefEventHandle OsEvent) { // Show dev tools on CMD/CTRL+SHIFT+I if( (Event.type == KEYEVENT_RAWKEYDOWN || Event.type == KEYEVENT_KEYDOWN) && #if PLATFORM_MAC (Event.modifiers == (EVENTFLAG_COMMAND_DOWN | EVENTFLAG_SHIFT_DOWN)) && #else (Event.modifiers == (EVENTFLAG_CONTROL_DOWN | EVENTFLAG_SHIFT_DOWN)) && #endif (Event.unmodified_character == 'i' || Event.unmodified_character == 'I') && IWebBrowserModule::Get().GetSingleton()->IsDevToolsShortcutEnabled() ) { return ShowDevTools(Browser); } #if PLATFORM_MAC // We need to handle standard Copy/Paste/etc... shortcuts on OS X if( (Event.type == KEYEVENT_RAWKEYDOWN || Event.type == KEYEVENT_KEYDOWN) && (Event.modifiers & EVENTFLAG_COMMAND_DOWN) != 0 && (Event.modifiers & EVENTFLAG_CONTROL_DOWN) == 0 && (Event.modifiers & EVENTFLAG_ALT_DOWN) == 0 && ( (Event.modifiers & EVENTFLAG_SHIFT_DOWN) == 0 || Event.unmodified_character == 'z' ) ) { CefRefPtr Frame = Browser->GetFocusedFrame(); if (Frame) { switch (Event.unmodified_character) { case 'a': Frame->SelectAll(); return true; case 'c': Frame->Copy(); return true; case 'v': Frame->Paste(); return true; case 'x': Frame->Cut(); return true; case 'z': if( (Event.modifiers & EVENTFLAG_SHIFT_DOWN) == 0 ) { Frame->Undo(); } else { Frame->Redo(); } return true; } } } #endif TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { return BrowserWindow->OnUnhandledKeyEvent(Event); } return false; } bool FCEFBrowserHandler::OnJSDialog(CefRefPtr Browser, const CefString& OriginUrl, const CefString& AcceptLang, JSDialogType DialogType, const CefString& MessageText, const CefString& DefaultPromptText, CefRefPtr Callback, bool& OutSuppressMessage) { bool Retval = false; TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { Retval = BrowserWindow->OnJSDialog(DialogType, MessageText, DefaultPromptText, Callback, OutSuppressMessage); } return Retval; } bool FCEFBrowserHandler::OnBeforeUnloadDialog(CefRefPtr Browser, const CefString& MessageText, bool IsReload, CefRefPtr Callback) { bool Retval = false; TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { Retval = BrowserWindow->OnBeforeUnloadDialog(MessageText, IsReload, Callback); } return Retval; } void FCEFBrowserHandler::OnResetDialogState(CefRefPtr Browser) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if (BrowserWindow.IsValid()) { BrowserWindow->OnResetDialogState(); } } void FCEFBrowserHandler::OnBeforeContextMenu(CefRefPtr Browser, CefRefPtr Frame, CefRefPtr Params, CefRefPtr Model) { TSharedPtr BrowserWindow = BrowserWindowPtr.Pin(); if ( BrowserWindow.IsValid() && BrowserWindow->OnSuppressContextMenu().IsBound() && BrowserWindow->OnSuppressContextMenu().Execute() ) { Model->Clear(); } } #undef LOCTEXT_NAMESPACE #endif // WITH_CEF