// Copyright (c) 2024 Vuplex Inc. All rights reserved. // // Licensed under the Vuplex Commercial Software Library License, you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // https://vuplex.com/commercial-library-license // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Only define BaseWebView.cs on supported platforms to avoid IL2CPP linking // errors on unsupported platforms. using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Vuplex.WebView.Internal { /// /// Internal class that implements the KeyboardEnabled setting for WebViewPrefab and CanvasWebViewPrefab. /// class KeyboardManager : MonoBehaviour { public static KeyboardManager Instance { get { // Don't recreate the instance if it's already been destroyed due to the app closing, otherwise it will cause Unity to // log the error "Some objects were not cleaned up when closing the scene" when stopping the player in the editor. if (_instance == null && !_destroyed) { _instance = new GameObject("WebView Keyboard Manager").AddComponent(); DontDestroyOnLoad(_instance.gameObject); } return _instance; } } public void AddKeyboard(BaseKeyboard keyboard) { _keyboards.Add(keyboard); keyboard.KeyPressed += OnScreenKeyboard_KeyPressed; keyboard.BaseWebViewPrefab.PointerEntered += OnScreenKeyboard_PointerEntered; keyboard.BaseWebViewPrefab.PointerExited += OnScreenKeyboard_PointerExited; } public void RemoveKeyboard(BaseKeyboard keyboard) { if (!_keyboards.Contains(keyboard)) { return; } _keyboards.Remove(keyboard); keyboard.KeyPressed -= OnScreenKeyboard_KeyPressed; keyboard.BaseWebViewPrefab.PointerEntered -= OnScreenKeyboard_PointerEntered; keyboard.BaseWebViewPrefab.PointerExited -= OnScreenKeyboard_PointerExited; } public void SetKeyboardEnabled(BaseWebViewPrefab webViewPrefab, bool enabled) { if (enabled) { _addWebViewPrefab(webViewPrefab); } else { _removeWebViewPrefab(webViewPrefab); } } static bool _destroyed; BaseWebViewPrefab _focusedWebViewPrefab; BaseWebViewPrefab _hoveredWebViewPrefab; static KeyboardManager _instance; HashSet _keyboards = new HashSet(); NativeKeyboardListener _nativeKeyboardListener; bool _pointerIsHoveringOverKeyboard; HashSet _webViewPrefabs = new HashSet(); void Awake() { _nativeKeyboardListener = NativeKeyboardListener.Instantiate(); _nativeKeyboardListener.transform.parent = transform; _nativeKeyboardListener.ImeCompositionCancelled += NativeKeyboardListener_ImeCompositionCancelled; _nativeKeyboardListener.ImeCompositionChanged += NativeKeyboardListener_ImeCompositionChanged; _nativeKeyboardListener.ImeCompositionFinished += NativeKeyboardListener_ImeCompositionFinished; _nativeKeyboardListener.KeyDownReceived += NativeKeyboardListener_KeyDownReceived; _nativeKeyboardListener.KeyUpReceived += NativeKeyboardListener_KeyUpReceived; } void OnDestroy() => _destroyed = true; void WebViewPrefab_Clicked(object sender, ClickedEventArgs eventArgs) { var webViewPrefab = (BaseWebViewPrefab)sender; _setFocusedWebViewPrefab(webViewPrefab); // Also set _hoveredWebViewPrefab here in case the input module doesn't support PointerEntered. _hoveredWebViewPrefab = webViewPrefab; } void _addWebViewPrefab(BaseWebViewPrefab webViewPrefab) { _webViewPrefabs.Add(webViewPrefab); // Automatically focus the new prefab. _setFocusedWebViewPrefab(webViewPrefab); webViewPrefab.Clicked += WebViewPrefab_Clicked; webViewPrefab.PointerEntered += WebViewPrefab_PointerEntered; webViewPrefab.PointerExited += WebViewPrefab_PointerExited; // Note: BaseWebViewPrefab.cs intentionally calls KeyboardManager.SetKeyboardEnabled() after BaseWebViewPrefab.WebView // is set but before raising BaseWebViewPrefab.Initialized so that this method can set the FocusChanged handler before // the application has the chance to call SetFocused(). webViewPrefab.WebView.FocusChanged += WebView_FocusChanged; var webViewWithIme = webViewPrefab.WebView as IWithIme; if (webViewWithIme != null) { webViewWithIme.ImeInputFieldPositionChanged += WebView_ImeInputFieldPositionChanged; } } void NativeKeyboardListener_ImeCompositionCancelled(object sender, EventArgs eventArgs) { var webViewWithIme = _focusedWebViewPrefab?.WebView as IWithIme; if (webViewWithIme != null) { webViewWithIme.CancelImeComposition(); } } void NativeKeyboardListener_ImeCompositionChanged(object sender, EventArgs eventArgs) { var webViewWithIme = _focusedWebViewPrefab?.WebView as IWithIme; if (webViewWithIme != null) { webViewWithIme.SetImeComposition(eventArgs.Value); } } void NativeKeyboardListener_ImeCompositionFinished(object sender, EventArgs eventArgs) { var webViewWithIme = _focusedWebViewPrefab?.WebView as IWithIme; if (webViewWithIme != null) { webViewWithIme.FinishImeComposition(eventArgs.Value); } } void NativeKeyboardListener_KeyDownReceived(object sender, KeyboardEventArgs eventArgs) { var webViewWithKeyDown = _focusedWebViewPrefab?.WebView as IWithKeyDownAndUp; if (webViewWithKeyDown != null) { webViewWithKeyDown.KeyDown(eventArgs.Key, eventArgs.Modifiers); } else { _focusedWebViewPrefab?.WebView?.SendKey(eventArgs.Key); } } void NativeKeyboardListener_KeyUpReceived(object sender, KeyboardEventArgs eventArgs) { var webViewWithKeyUp = _focusedWebViewPrefab?.WebView as IWithKeyDownAndUp; webViewWithKeyUp?.KeyUp(eventArgs.Key, eventArgs.Modifiers); } void OnScreenKeyboard_KeyPressed(object sender, EventArgs eventArgs) { _focusedWebViewPrefab?.WebView?.SendKey(eventArgs.Value); } void OnScreenKeyboard_PointerEntered(object sender, EventArgs eventArgs) => _pointerIsHoveringOverKeyboard = true; void OnScreenKeyboard_PointerExited(object sender, EventArgs eventArgs) => _pointerIsHoveringOverKeyboard = false; void Update() { var mouseDown = false; #if !ENABLE_INPUT_SYSTEM || ENABLE_LEGACY_INPUT_MANAGER mouseDown = Input.GetMouseButtonDown(0); #endif if (_hoveredWebViewPrefab == null && !_pointerIsHoveringOverKeyboard && mouseDown) { // Some area outside of a keyboard-enabled webview or Keyboard was clicked, so unfocus the // webview in case the object clicked was a Unity Input Field. _setFocusedWebViewPrefab(null); } if (_focusedWebViewPrefab != null && !_focusedWebViewPrefab.gameObject.activeInHierarchy) { // The focused WebViewPrefab was deactivated, so unfocus it so that we don't continue sending // keys to the webview while it's invisible. _setFocusedWebViewPrefab(null); } } void _removeWebViewPrefab(BaseWebViewPrefab webViewPrefab) { if (!_webViewPrefabs.Contains(webViewPrefab)) { return; } _webViewPrefabs.Remove(webViewPrefab); if (_focusedWebViewPrefab == webViewPrefab) { _focusedWebViewPrefab = null; } webViewPrefab.Clicked -= WebViewPrefab_Clicked; webViewPrefab.PointerEntered -= WebViewPrefab_PointerEntered; webViewPrefab.PointerExited -= WebViewPrefab_PointerExited; if (webViewPrefab.WebView != null) { webViewPrefab.WebView.FocusChanged -= WebView_FocusChanged; } var webViewWithIme = webViewPrefab.WebView as IWithIme; if (webViewWithIme != null) { webViewWithIme.ImeInputFieldPositionChanged -= WebView_ImeInputFieldPositionChanged; } } void _setFocusedWebViewPrefab(BaseWebViewPrefab webViewPrefab) { var previouslyFocusedPrefab = _focusedWebViewPrefab; _focusedWebViewPrefab = webViewPrefab; if (previouslyFocusedPrefab != null && previouslyFocusedPrefab != webViewPrefab) { // Unfocus the previous webview. previouslyFocusedPrefab.WebView?.SetFocused(false); } } void WebView_FocusChanged(object sender, EventArgs eventArgs) { var prefab = _webViewPrefabs.ToList().Find(p => p.WebView == sender); if (prefab == null) { return; } var focused = eventArgs.Value; if (focused) { _setFocusedWebViewPrefab(prefab); } else if (prefab == _focusedWebViewPrefab) { _setFocusedWebViewPrefab(null); } } void WebView_ImeInputFieldPositionChanged(object sender, EventArgs eventArgs) { var prefab = _webViewPrefabs.ToList().Find(p => p.WebView == sender); if (prefab != null && prefab == _focusedWebViewPrefab) { #if !ENABLE_INPUT_SYSTEM || ENABLE_LEGACY_INPUT_MANAGER var screenPoint = prefab.BrowserToScreenPoint(eventArgs.Value.x, eventArgs.Value.y); var screenPointY = screenPoint.y; switch (Application.platform) { case RuntimePlatform.WindowsEditor: // For some reason, in the Windows editor, Input.compositionCursorPos doesn't work correctly // and the IME popup window is positioned higher than the specified point. // So, add an extra Y offset when running in the Windows editor. screenPointY += 60; break; case RuntimePlatform.OSXPlayer: case RuntimePlatform.OSXEditor: // Unity has a bug on macOS where the Y axis for Input.compositionCursorPos is incorrectly flipped. screenPointY = Screen.height - screenPointY; break; } Input.compositionCursorPos = new Vector2(screenPoint.x, screenPointY); #endif } } void WebViewPrefab_PointerEntered(object sender, EventArgs eventArgs) { _hoveredWebViewPrefab = (BaseWebViewPrefab)sender; } void WebViewPrefab_PointerExited(object sender, EventArgs eventArgs) { var webViewPrefab = (BaseWebViewPrefab)sender; if (_hoveredWebViewPrefab == webViewPrefab) { _hoveredWebViewPrefab = null; } } } }