You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1347 lines
43 KiB
1347 lines
43 KiB
using System; |
|
using UnityEngine; |
|
using System.Collections.Generic; |
|
using System.Runtime.InteropServices; |
|
using System.Text; |
|
using UnityEngine.Assertions; |
|
using UnityEngine.Serialization; |
|
|
|
#if UNITY_EDITOR |
|
using UnityEditor; |
|
#endif |
|
#if UNITY_5_5_OR_NEWER |
|
using UnityEngine.Profiling; |
|
#endif |
|
|
|
namespace ZenFulcrum.EmbeddedBrowser { |
|
|
|
/** Represents a browser "tab". */ |
|
public partial class Browser : MonoBehaviour { |
|
private static int lastUpdateFrame; |
|
|
|
public static string LocalUrlPrefix { get { return BrowserNative.LocalUrlPrefix; } } |
|
|
|
/** |
|
* List of possible actions when a new window is opened. |
|
*/ |
|
[Flags] |
|
public enum NewWindowAction { |
|
/** Ignore attempts to open new windows. */ |
|
Ignore = 1, |
|
/** Navigate the current window to the new window's URL. */ |
|
Redirect, |
|
/** |
|
* Create a new Browser instance to handle rendering the new window in the scene. |
|
* For this to be useful, you'll need to supply an INewWindowHandler with an |
|
* implementation of your choosing. |
|
* (If you set this behavior in the inspector, it won't take effect until you call SetNewWindowHandler.) |
|
*/ |
|
NewBrowser, |
|
/** |
|
* Create a new OS window, outside the game, to show the page. |
|
* Controlling and interacting with the new window outside is limited, though you can use JavaScript calls |
|
* from the parent. |
|
* OS-level windows may have unexpected or incomplete behavior. Using this outside of debugging/testing |
|
* is not officially supported. Doesn't work with OS X+il2cpp. |
|
*/ |
|
NewWindow, |
|
} |
|
|
|
protected IBrowserUI _uiHandler; |
|
protected bool uiHandlerAssigned = false; |
|
/** |
|
* Input handler. |
|
* If you don't assign anything, it will default to something useful, but you can replace |
|
* it or null it as desired. |
|
* |
|
* If do you want to use your own or disable it, be sure to assign something (or null) before WhenReady fires. |
|
*/ |
|
public IBrowserUI UIHandler { |
|
get { return _uiHandler; } |
|
set { |
|
uiHandlerAssigned = true; |
|
_uiHandler = value; |
|
} |
|
} |
|
|
|
[Tooltip("Initial URL to load.\n\nTo change at runtime use browser.Url to load a page.")] |
|
[SerializeField] private string _url = ""; |
|
|
|
[Tooltip("Initial size.\n\nTo change at runtime use browser.Resize.")] |
|
[SerializeField] private int _width = 512, _height = 512; |
|
|
|
[Tooltip(@"Generate mipmaps? |
|
|
|
Generating mipmaps tends to be somewhat expensive, especially when updating a large texture every frame. Instead of |
|
generating mipmaps, try using one of the ""emulate mipmap"" shader variants. |
|
|
|
To change at runtime modify this value and call browser.Resize.")] |
|
public bool generateMipmap = false; |
|
|
|
[Tooltip(@"Base background color to use for pages. |
|
|
|
The texture will be cleared to this color until the page has rendered. Additionally, if baseColor.a is not |
|
fully opaque the browser will render transparently. (Don't forget to use an appropriate material for transparency.) |
|
|
|
Don't change this after the first Update() tick. (You can still tweak a page via EvalJS and CSS.)")] |
|
[FormerlySerializedAs("backgroundColor")] |
|
public Color32 baseColor = new Color32(0, 0, 0, 0);//default to transparent |
|
|
|
|
|
[Tooltip(@"Initial browser ""zoom level"". Negative numbers are smaller, zero is normal, positive numbers are larger. |
|
The size roughly doubles/halves for every four units added/removed. |
|
Note that zoom level is shared by all pages on the some domain. |
|
Also note that this zoom level may be persisted across runs. |
|
|
|
To change at runtime use browser.Zoom.")] |
|
//TODO: prefer deviceScale (not yet implemented) for DPI-style size changes. |
|
[SerializeField] private float _zoom = 0; |
|
|
|
/** |
|
* Fired when we get a console.log/warn/error from the page. |
|
* args: (message, source) |
|
* |
|
* (CEF's console event leaves a lot to be desired, we are unable to get the log level or additional arguments.) |
|
*/ |
|
public event Action<string, string> onConsoleMessage = (s, s1) => {}; |
|
|
|
[Tooltip(@"Allow right-clicking to show a context menu on what parts of the page? |
|
|
|
May be changed at any time. |
|
")] |
|
[FlagsField] |
|
public BrowserNative.ContextMenuOrigin allowContextMenuOn = BrowserNative.ContextMenuOrigin.Editable; |
|
|
|
[Tooltip(@"What should we do when a user/the page tries to open a new window? |
|
|
|
For ""New Browser"" to work, you need to assign NewWindowHandler to a handler of your creation. |
|
|
|
Don't use ""New Window"" outside debugging and testing. |
|
|
|
Use SetNewWindowHandler to adjust at runtime. |
|
")] |
|
[SerializeField] |
|
private NewWindowAction newWindowAction = NewWindowAction.Redirect; |
|
|
|
[Obsolete("Use SetNewWindowHandler", true)] |
|
public INewWindowHandler NewWindowHandler { get; set; } |
|
|
|
/** If false, the texture won't be updated with new changes. */ |
|
public bool EnableRendering { get; set; } |
|
/** If false, we won't process input with the UIHandler. */ |
|
public bool EnableInput { get; set; } |
|
|
|
public CookieManager CookieManager { get; private set; } |
|
|
|
/** Handle to the native browser. */ |
|
[NonSerialized] |
|
internal protected int browserId; |
|
/** Same as browserId, but will be set before the browser is ready and remain set even after it's disposed */ |
|
private int unsafeBrowserId; |
|
/** Have we requested a native handle yet? (It may take a moment for the native browser to be ready.) */ |
|
protected bool browserIdRequested = false; |
|
protected Texture2D texture; |
|
public Texture2D Texture { get { return texture; } } |
|
/** Called when the image canvas has changed or resized. */ |
|
public event Action<Texture2D> afterResize = t => { }; |
|
protected bool textureIsOurs = false; |
|
protected bool forceNextRender = true; |
|
protected bool isPopup = false; |
|
|
|
/** List of tasks to execute on the main thread. May be used on any thread, but lock before touching. */ |
|
protected List<Action> thingsToDo = new List<Action>(); |
|
/** List of callbacks to call when the page loads next. */ |
|
protected List<Action> onloadActions = new List<Action>(); |
|
|
|
/** |
|
* We pass delegates/closures to the native level. We must ensure that they don't get GC'd |
|
* while the native object still exists and might use them, so we keep track of them here |
|
* to prevent that. |
|
*/ |
|
protected List<object> thingsToRemember = new List<object>(); |
|
/** |
|
* And, to make it more complicated, in some cases we can get GC'd (along with thingsToRemember and the |
|
* generated trampolines) before the native browser finishes shutting down. |
|
* |
|
* We use this to make sure {this} stays alive until the native side is done. |
|
* |
|
* Used across threads, lock before touching. |
|
*/ |
|
protected static Dictionary<int, List<object>> allThingsToRemember = new Dictionary<int, List<object>>(); |
|
|
|
/** A callback. {args} is a JSON node with the top-level type of array. */ |
|
public delegate void JSCallback(JSONNode args); |
|
protected delegate void JSResultFunc(JSONNode value, bool isError); |
|
|
|
private int nextCallbackId = 1; |
|
/** Registered callbacks that JS can call to us with. */ |
|
protected Dictionary<int, JSResultFunc> registeredCallbacks = new Dictionary<int, JSResultFunc>(); |
|
|
|
|
|
/** |
|
* We can't do much (go to url, navigate, etc) until the native browser is ready. |
|
* Most these actions will be queued for you and fired when we are ready. |
|
* |
|
* See also: WhenReady() |
|
*/ |
|
protected event BrowserNative.ReadyFunc onNativeReady; |
|
|
|
/** |
|
* Called when the page's onload fires. (Top frame only.) |
|
* loadData['status'] contains the status code, loadData['url'] the url |
|
*/ |
|
public event Action<JSONNode> onLoad = loadData => {}; |
|
/** |
|
* Called when the top-level page has been fetched (but not necessarily parsed and run). |
|
* loadData['status'] contains the status code, loadData['url'] the url |
|
* (Top frame only.) |
|
*/ |
|
[Obsolete("Doesn't fire reliably due to its design. Consider using onLoad or onNavStateChange.")] |
|
public event Action<JSONNode> onFetch = loadData => {}; |
|
/** |
|
* Called when a page fails to load. |
|
* Use QueuePageReplacer to inject a custom error page. |
|
* (Top frame only.) |
|
* |
|
* Try visiting http://255.255.255.255/ to test. |
|
*/ |
|
public event Action<JSONNode> onFetchError = errCode => {}; |
|
/** |
|
* Called when an SSL cert fails checks. |
|
* Use QueuePageReplacer to inject a custom error page. |
|
* (Top frame only.) |
|
* |
|
* Try visiting https://wrong.host.badssl.com/ to test. |
|
*/ |
|
public event Action<JSONNode> onCertError = errInfo => {}; |
|
/** |
|
* Called when a renderer process dies/is killed. |
|
* Use QueuePageReplacer to inject a custom error page; you might also choose to try reloading once or twice. |
|
* |
|
* Try visiting chrome://checkcrash/ to test. |
|
*/ |
|
public event Action onSadTab = () => {}; |
|
/** |
|
* Called after the browser's texture/image data is updated. |
|
*/ |
|
public event Action onTextureUpdated = () => {}; |
|
|
|
/// <summary> |
|
/// Called when the browser's nav state changes. |
|
/// Presently these are considered nav state changes, but other things may be added in the future: |
|
/// - URL change |
|
/// - canGoForward/Back change |
|
/// - loading started or completed |
|
/// </summary> |
|
public event Action onNavStateChange = () => {}; |
|
|
|
/** |
|
* Called when a download is started. |
|
* See BrowserNative.ChangeType.CHT_DOWNLOAD_STARTED for a list and explanation of |
|
* the elements in the JSON object. |
|
* |
|
* If a handler is given it should call DownloadCommand() to start or cancel the download (eventually). |
|
* Once a download is started onDownloadStatus will be called from time-to-time. Additionally, you can use |
|
* DownloadCommand to cancel, pause, or resume a running download. |
|
* |
|
* If this is null, no downloading will happen. |
|
*/ |
|
public Action<int, JSONNode> onDownloadStarted = null; |
|
|
|
/** |
|
* Called when a download has a status update. |
|
* See BrowserNative.ChangeType.CHT_DOWNLOAD_STATUS for a list and explanation of |
|
* the elements in the JSON object. |
|
* |
|
* NB: You may get status reports on downloads that haven't triggered onDownloadStarted yet. |
|
*/ |
|
public event Action<int, JSONNode> onDownloadStatus = (downloadId, info) => {}; |
|
|
|
/** |
|
* Called when the element in the page with keyboard focus changes. |
|
* If tagName == "", then focus has been lost. |
|
*/ |
|
public event Action<string, bool, string> onNodeFocus = (tagName, editable, value) => {}; |
|
|
|
/// <summary> |
|
/// Called when the browser (as a whole) gains/loses keyboard or mouse focus. |
|
/// </summary> |
|
public event Action<bool, bool> onBrowserFocus = (mouseFocused, keyboardFocused) => {}; |
|
|
|
[HideInInspector] |
|
public readonly BrowserFocusState focusState = new BrowserFocusState(); |
|
|
|
/// <summary> |
|
/// Called when any browser is created. |
|
/// </summary> |
|
public static event Action<Browser> onAnyBrowserCreated = browser => {}; |
|
/// <summary> |
|
/// Called when any browser is destroyed. |
|
/// </summary> |
|
public static event Action<Browser> onAnyBrowserDestroyed = browser => {}; |
|
|
|
|
|
private BrowserInput browserInput; |
|
private Browser overlay; |
|
/** We have to load a blank page before we can inject HTML. If we load a blank page, don't count it as the "loading". */ |
|
private bool skipNextLoad; |
|
/** There may be a short moment between requesting a URL and when IsLoadedRaw turns false. We use this flag to help cope. */ |
|
private bool loadPending; |
|
|
|
private BrowserNavState navState = new BrowserNavState(); |
|
|
|
private bool newWindowHandlerSet = false; |
|
/// <summary> |
|
/// If |
|
/// </summary> |
|
private INewWindowHandler newWindowHandler; |
|
|
|
/** |
|
* This will sometimes contain an inner Browser that handles tasks such as |
|
* rendering alert()s and such. |
|
*/ |
|
protected DialogHandler dialogHandler; |
|
|
|
protected void Awake() { |
|
EnableRendering = true; |
|
EnableInput = true; |
|
CookieManager = new CookieManager(this); |
|
|
|
browserInput = new BrowserInput(this); |
|
|
|
if (!newWindowHandlerSet) { |
|
//(if another component calls SetNewWindowHandler in its Awake, we'll overwrite that |
|
//so only do this if it's not been called yet) |
|
SetNewWindowHandler(newWindowAction == NewWindowAction.NewBrowser ? NewWindowAction.Ignore : newWindowAction, null); |
|
} |
|
|
|
onNativeReady += id => { |
|
if (!uiHandlerAssigned) { |
|
var meshCollider = GetComponent<MeshCollider>(); |
|
if (meshCollider) { |
|
var ui = gameObject.AddComponent<PointerUIMesh>(); |
|
gameObject.AddComponent<CursorRendererOS>(); |
|
UIHandler = ui; |
|
} |
|
} |
|
|
|
Resize(_width, _height); |
|
|
|
Zoom = _zoom; |
|
|
|
if (!isPopup && !string.IsNullOrEmpty(_url)) Url = _url; |
|
}; |
|
|
|
onConsoleMessage += (message, source) => { |
|
var text = source + ": " + message; |
|
Debug.Log(text, this); |
|
}; |
|
|
|
onFetchError += err => { |
|
//don't show anything if the error is a load abort |
|
if (err["error"] == "ERR_ABORTED") return; |
|
|
|
QueuePageReplacer(() => { |
|
LoadDataURI(ErrorGenerator.GenerateFetchError(err)); |
|
}, -1000); |
|
}; |
|
|
|
onCertError += err => { |
|
QueuePageReplacer(() => { |
|
LoadHTML(ErrorGenerator.GenerateCertError(err), Url); |
|
}, -900); |
|
}; |
|
|
|
onSadTab += () => { |
|
// Try visiting chrome://checkcrash |
|
QueuePageReplacer(() => { |
|
//LoadHTML sometimes works, but LoadDataURI works more reliably |
|
LoadDataURI(ErrorGenerator.GenerateSadTabError()); |
|
}, -1000); |
|
}; |
|
|
|
onAnyBrowserCreated(this); |
|
} |
|
|
|
/** Returns true if the browser is ready to take orders. Most actions will be internally delayed until it is. */ |
|
public bool IsReady { |
|
get { return browserId != 0; } |
|
} |
|
|
|
/** |
|
* The given callback will be called when the browser is ready to start taking commands. |
|
*/ |
|
public void WhenReady(Action callback) { |
|
if (IsReady) { |
|
//Call it later instead of now to help head off some subtle bugs that can be produced by such a scheme. |
|
//Call it at next update. (Since our script order is a little bit later than everyone else this usually will add no latency.) |
|
lock (thingsToDo) thingsToDo.Add(callback); |
|
} else { |
|
BrowserNative.ReadyFunc func = null; |
|
func = id => { |
|
try { |
|
callback(); |
|
} catch (Exception ex) { |
|
Debug.LogException(ex); |
|
} |
|
onNativeReady -= func; |
|
}; |
|
onNativeReady += func; |
|
} |
|
} |
|
|
|
/** Fires the given callback during th next Update/LateUpdate tick on the main thread. This may be called from any thread. */ |
|
public void RunOnMainThread(Action callback) { |
|
lock (thingsToDo) thingsToDo.Add(callback); |
|
} |
|
|
|
/** |
|
* Calls the given callback the next time the page is loaded. |
|
* This will not fire right away if IsLoaded is true, it will wait for a new page to load. |
|
* Callbacks won't be fired yet if the url is some type of blank url ("", "about:blank", etc). |
|
*/ |
|
public void WhenLoaded(Action callback) { |
|
onloadActions.Add(callback); |
|
} |
|
|
|
/** |
|
* Sets up a new native browser. |
|
* If newBrowserId is zero, allocates a new browser and sets it up. |
|
* If newBrowserId is nonzero, takes ownership of that allocated browser and sets it up. |
|
* |
|
* Internal use only. |
|
*/ |
|
internal void RequestNativeBrowser(int newBrowserId = 0) { |
|
if (browserId != 0 || browserIdRequested) return; |
|
|
|
browserIdRequested = true; |
|
|
|
try { |
|
BrowserNative.LoadNative(); |
|
} catch { |
|
gameObject.SetActive(false); |
|
throw; |
|
} |
|
|
|
int newId; |
|
if (newBrowserId == 0) { |
|
var settings = new BrowserNative.ZFBSettings() { |
|
bgR = baseColor.r, |
|
bgG = baseColor.g, |
|
bgB = baseColor.b, |
|
bgA = baseColor.a, |
|
offscreen = 1, |
|
}; |
|
newId = BrowserNative.zfb_createBrowser(settings); |
|
} else { |
|
newId = newBrowserId; |
|
isPopup = true;//don't nav to our to URL, it will be loaded by the backend |
|
} |
|
|
|
unsafeBrowserId = newId; |
|
allBrowsers[unsafeBrowserId] = this; |
|
|
|
//Debug.Log("Requested browser for " + name + " " + newId); |
|
|
|
//We have a native browser, but it is invalid to do anything with it until it's ready. |
|
//Therefore, we don't set browserId until it's ready. |
|
|
|
//But we will put all our callbacks in place. |
|
|
|
//Don't let our remember list get destroyed until we are ready for that. |
|
lock (allThingsToRemember) allThingsToRemember[newId] = thingsToRemember; |
|
|
|
//Set up callbacks: |
|
BrowserNative.ForwardJSCallFunc forwardCall = CB_ForwardJSCallFunc; |
|
thingsToRemember.Add(forwardCall); |
|
BrowserNative.zfb_registerJSCallback(newId, forwardCall); |
|
|
|
BrowserNative.ChangeFunc changeCall = CB_ChangeFunc; |
|
thingsToRemember.Add(changeCall); |
|
BrowserNative.zfb_registerChangeCallback(newId, changeCall); |
|
|
|
BrowserNative.DisplayDialogFunc dialogCall = CB_DisplayDialogFunc; |
|
thingsToRemember.Add(dialogCall); |
|
BrowserNative.zfb_registerDialogCallback(newId, dialogCall); |
|
|
|
BrowserNative.ShowContextMenuFunc contextCall = CB_ShowContextMenuFunc; |
|
thingsToRemember.Add(contextCall); |
|
BrowserNative.zfb_registerContextMenuCallback(newId, contextCall); |
|
|
|
BrowserNative.ConsoleFunc consoleCall = CB_ConsoleFunc; |
|
thingsToRemember.Add(consoleCall); |
|
BrowserNative.zfb_registerConsoleCallback(newId, consoleCall); |
|
|
|
BrowserNative.ReadyFunc readyCall = CB_ReadyFunc; |
|
thingsToRemember.Add(readyCall); |
|
BrowserNative.zfb_setReadyCallback(newId, readyCall); |
|
|
|
BrowserNative.NavStateFunc navStateCall = CB_NavStateFunc; |
|
thingsToRemember.Add(navStateCall); |
|
BrowserNative.zfb_registerNavStateCallback(newId, navStateCall); |
|
} |
|
|
|
protected void OnItemChange(BrowserNative.ChangeType type, string arg1) { |
|
//Debug.Log("ChangeType " + name + " " + type + " arg " + arg1 + " loaded " + IsLoaded); |
|
switch (type) { |
|
case BrowserNative.ChangeType.CHT_CURSOR: |
|
UpdateCursor(); |
|
break; |
|
case BrowserNative.ChangeType.CHT_BROWSER_CLOSE: |
|
//handled directly on the calling thread, nothing to do here |
|
break; |
|
case BrowserNative.ChangeType.CHT_FETCH_FINISHED: |
|
#pragma warning disable 618 |
|
onFetch(JSONNode.Parse(arg1)); |
|
#pragma warning restore 618 |
|
break; |
|
case BrowserNative.ChangeType.CHT_FETCH_FAILED: |
|
onFetchError(JSONNode.Parse(arg1)); |
|
break; |
|
case BrowserNative.ChangeType.CHT_LOAD_FINISHED: |
|
if (skipNextLoad) { |
|
//deal with extra step we have to do to load HTML to an empty page |
|
skipNextLoad = false; |
|
return; |
|
} |
|
|
|
loadPending = false; |
|
|
|
navState.loading = false;//we'll get the event to update this in a moment, but we need to not be "loading" now |
|
|
|
if (onloadActions.Count != 0) { |
|
foreach (var action in onloadActions) action(); |
|
onloadActions.Clear(); |
|
} |
|
|
|
onLoad(JSONNode.Parse(arg1)); |
|
break; |
|
case BrowserNative.ChangeType.CHT_CERT_ERROR: |
|
onCertError(JSONNode.Parse(arg1)); |
|
break; |
|
case BrowserNative.ChangeType.CHT_SAD_TAB: |
|
onSadTab(); |
|
break; |
|
case BrowserNative.ChangeType.CHT_DOWNLOAD_STARTED: { |
|
var info = JSONNode.Parse(arg1); |
|
if (onDownloadStarted != null) { |
|
onDownloadStarted(info["id"], info); |
|
} else { |
|
DownloadCommand(info["id"], BrowserNative.DownloadAction.Cancel); |
|
} |
|
break; |
|
} |
|
case BrowserNative.ChangeType.CHT_DOWNLOAD_STATUS: { |
|
var info = JSONNode.Parse(arg1); |
|
onDownloadStatus(info["id"], info); |
|
break; |
|
} |
|
case BrowserNative.ChangeType.CHT_FOCUSED_NODE: { |
|
var info = JSONNode.Parse(arg1); |
|
focusState.focusedTagName = info["TagName"]; |
|
focusState.focusedNodeEditable = info["editable"]; |
|
onNodeFocus(info["tagName"], info["editable"], info["value"]); |
|
break; |
|
} |
|
|
|
} |
|
} |
|
|
|
protected void CreateDialogHandler() { |
|
if (dialogHandler != null) return; |
|
|
|
DialogHandler.DialogCallback dialogCallback = (affirm, text1, text2) => { |
|
CheckSanity(); |
|
BrowserNative.zfb_sendDialogResults(browserId, affirm, text1, text2); |
|
}; |
|
DialogHandler.MenuCallback contextCallback = commandId => { |
|
CheckSanity(); |
|
BrowserNative.zfb_sendContextMenuResults(browserId, commandId); |
|
}; |
|
|
|
dialogHandler = DialogHandler.Create(this, dialogCallback, contextCallback); |
|
} |
|
|
|
/** |
|
* Call this before you do any native things with our browser instance. |
|
* If something terribly stupid is going on this may be able to bail out with an exception instead of |
|
* crashing everything. |
|
*/ |
|
protected void CheckSanity() { |
|
if (browserId == 0) throw new InvalidOperationException("No native browser allocated"); |
|
if (!BrowserNative.SymbolsLoaded) throw new InvalidOperationException("Browser .dll not loaded"); |
|
} |
|
|
|
/** |
|
* If we aren't ready, queues the given action to happen later and returns true. |
|
* Else calls CheckSanity and returns false. |
|
*/ |
|
internal bool DeferUnready(Action ifNotReady) { |
|
if (browserId == 0) { |
|
WhenReady(ifNotReady); |
|
return true; |
|
} else { |
|
CheckSanity(); |
|
return false; |
|
} |
|
} |
|
|
|
protected void OnDisable() { |
|
//note: if you want a browser to stop, load a different page or destroy it |
|
//The browser will continue to run until destroyed. |
|
} |
|
|
|
protected void OnDestroy() { |
|
onAnyBrowserDestroyed(this); |
|
|
|
if (browserId == 0) return; |
|
|
|
if (dialogHandler) DestroyImmediate(dialogHandler.gameObject); |
|
dialogHandler = null; |
|
|
|
if (BrowserNative.SymbolsLoaded) BrowserNative.zfb_destroyBrowser(browserId); |
|
if (textureIsOurs) Destroy(texture); |
|
//(Don't remove from allBrowsers here, there's some callbacks that need to happen first.) |
|
|
|
browserId = 0; |
|
texture = null; |
|
} |
|
|
|
public string Url { |
|
/** |
|
* Gets the current browser URL. |
|
* Note that if you just set the URL and the page hasn't loaded, this won't return the new value. |
|
* It always returns the current URL of the browser as we are most recently aware of. |
|
*/ |
|
get { |
|
return navState.url; |
|
} |
|
/** Shortcut for LoadURL(value, true) */ |
|
set { |
|
LoadURL(value, true); |
|
} |
|
} |
|
|
|
/** |
|
* Navigates to the given URL. If force is true, it will go there right away. |
|
* If force is false, pages that wish to can prompt the user and possibly cancel the |
|
* navigation. |
|
*/ |
|
public void LoadURL(string url, bool force) { |
|
if (string.IsNullOrEmpty(url)) { |
|
//If we ask CEF to load "" it will crash. Try Url = "about:blank" or LoadHTML() instead. |
|
throw new ArgumentException("URL must be non-empty", "value"); |
|
} |
|
|
|
if (DeferUnready(() => LoadURL(url, force))) return; |
|
|
|
const string magicPrefix = "localGame://"; |
|
|
|
if (url.StartsWith(magicPrefix)) { |
|
url = LocalUrlPrefix + url.Substring(magicPrefix.Length); |
|
} |
|
|
|
loadPending = true; |
|
|
|
BrowserNative.zfb_goToURL(browserId, url, force); |
|
} |
|
|
|
/** |
|
* Loads the given HTML string as if it were the given URL. |
|
* For the URL use http://-like porotocols or else things may not work right. (In particular, the backend |
|
* might sanitize it to "about:blank" and things won't work right because it appears a page isn't loaded.) |
|
* |
|
* Note that, instead of using this, you can also load "data:" URIs into this.Url. |
|
* This allows pretty much any type of content to be loaded as the whole page. |
|
*/ |
|
public void LoadHTML(string html, string url = null) { |
|
if (DeferUnready(() => LoadHTML(html, url))) return; |
|
|
|
//Debug.Log("Load HTML " + html); |
|
|
|
loadPending = true; |
|
|
|
if (string.IsNullOrEmpty(url)) { |
|
url = LocalUrlPrefix + "custom"; |
|
} |
|
|
|
if (string.IsNullOrEmpty(this.Url)) { |
|
//Nothing will happen if we don't have an initial page, so cause one. |
|
this.Url = "about:blank"; |
|
skipNextLoad = true; |
|
} |
|
|
|
BrowserNative.zfb_goToHTML(browserId, html, url); |
|
} |
|
|
|
/// <summary> |
|
/// Generates a data URI for the given content and loads that URI. |
|
/// </summary> |
|
/// <param name="text"></param> |
|
/// <param name="mimeType"></param> |
|
public void LoadDataURI(string text, string mimeType = "text/html") { |
|
if (mimeType.StartsWith("text/") && !mimeType.Contains(";")) mimeType = mimeType + ";charset=UTF-8"; |
|
LoadDataURI(Encoding.UTF8.GetBytes(text), mimeType); |
|
} |
|
|
|
/// <summary> |
|
/// Generates a data URI for the given content and loads that URI. |
|
/// </summary> |
|
/// <param name="text"></param> |
|
/// <param name="mimeType"></param> |
|
public void LoadDataURI(byte[] data, string mimeType) { |
|
var dataStr = Convert.ToBase64String(data); |
|
this.Url = "data:" + mimeType + ";base64," + dataStr; |
|
} |
|
|
|
/// <summary> |
|
/// Sets how new popup windows are handled. |
|
/// </summary> |
|
/// <param name="action"></param> |
|
/// <param name="newWindowHandler"> |
|
/// If action==NewBrowser, this handler will be invoked to create the browser in the scene. |
|
/// May be null otherwise. |
|
/// </param> |
|
public void SetNewWindowHandler(NewWindowAction action, INewWindowHandler newWindowHandler) { |
|
newWindowHandlerSet = true; |
|
if (action == NewWindowAction.NewBrowser && newWindowHandler == null) { |
|
throw new Exception("No new window handler supplied for NewBrowser action"); |
|
} |
|
|
|
if (DeferUnready(() => SetNewWindowHandler(action, newWindowHandler))) return; |
|
|
|
var settings = new BrowserNative.ZFBSettings() { |
|
bgR = baseColor.r, |
|
bgG = baseColor.g, |
|
bgB = baseColor.b, |
|
bgA = baseColor.a, |
|
}; |
|
|
|
this.newWindowHandler = newWindowHandler; |
|
this.newWindowAction = action; |
|
BrowserNative.NewWindowFunc cb = CB_NewWindowFunc; |
|
thingsToRemember.Add(cb); |
|
BrowserNative.zfb_registerPopupCallback(browserId, (BrowserNative.NewWindowAction)action, settings, cb); |
|
} |
|
|
|
/** |
|
* Sends a command such as "select all", "undo", or "copy" |
|
* to the currently focused frame in th browser. |
|
*/ |
|
public void SendFrameCommand(BrowserNative.FrameCommand command) { |
|
if (DeferUnready(() => SendFrameCommand(command))) return; |
|
|
|
BrowserNative.zfb_sendCommandToFocusedFrame(browserId, command); |
|
} |
|
|
|
private Action pageReplacer; |
|
private float pageReplacerPriority; |
|
/** |
|
* Queues a function to replace the current page. |
|
* |
|
* This is used mostly in error handling. Namely, the default error handler will queue an error page at a low |
|
* priority, but your onLoadError callback can call this to queue its own error page. |
|
* |
|
* At the end of the tick, the {replacePage} callback with the highest priority will |
|
* be called. Typically {replacePage} will call LoadHTML to change things around. |
|
* |
|
* Default error handles will have a priority of less than -100. |
|
*/ |
|
public void QueuePageReplacer(Action replacePage, float priority) { |
|
if (pageReplacer == null || priority >= pageReplacerPriority) { |
|
pageReplacer = replacePage; |
|
pageReplacerPriority = priority; |
|
} |
|
} |
|
|
|
public bool CanGoBack { |
|
get { |
|
return navState.canGoBack; |
|
} |
|
} |
|
|
|
public void GoBack() { |
|
if (!IsReady) return; |
|
CheckSanity(); |
|
BrowserNative.zfb_doNav(browserId, -1); |
|
} |
|
|
|
public bool CanGoForward { |
|
get { |
|
return navState.canGoForward; |
|
} |
|
} |
|
|
|
public void GoForward() { |
|
if (!IsReady) return; |
|
CheckSanity(); |
|
BrowserNative.zfb_doNav(browserId, 1); |
|
} |
|
|
|
/** |
|
* Returns true if the browser is loading a page. |
|
* Unlike IsLoaded, this does not account for special case urls. |
|
*/ |
|
public bool IsLoadingRaw { |
|
get { |
|
return navState.loading; |
|
} |
|
} |
|
|
|
/** |
|
* Returns true if we have a page and it's loaded. |
|
* This will not return true if we haven't gone to a URL or we are on "about:blank" |
|
*/ |
|
public bool IsLoaded { |
|
get { |
|
if (!IsReady || loadPending) return false; |
|
if (navState.loading) return false; |
|
|
|
var url = Url; |
|
var urlIsBlank = string.IsNullOrEmpty(url) || url == "about:blank"; |
|
|
|
return !urlIsBlank; |
|
} |
|
} |
|
|
|
public void Stop() { |
|
if (!IsReady) return; |
|
CheckSanity(); |
|
BrowserNative.zfb_changeLoading(browserId, BrowserNative.LoadChange.LC_STOP); |
|
} |
|
|
|
/** |
|
* Reloads the current page. |
|
* If force is true, the cache will be skipped. |
|
*/ |
|
public void Reload(bool force = false) { |
|
if (!IsReady) return; |
|
CheckSanity(); |
|
if (force) BrowserNative.zfb_changeLoading(browserId, BrowserNative.LoadChange.LC_FORCE_RELOAD); |
|
else BrowserNative.zfb_changeLoading(browserId, BrowserNative.LoadChange.LC_RELOAD); |
|
} |
|
|
|
|
|
/** |
|
* Show the development tools for the current page. |
|
* |
|
* If {show} is false the dev tools will be hidden, if possible. |
|
* |
|
* Like NewWindowAction.NewWindow using this outside of debugging/testing |
|
* is not officially supported. Doesn't work with OS X+il2cpp. |
|
*/ |
|
public void ShowDevTools(bool show = true) { |
|
if (DeferUnready(() => ShowDevTools(show))) return; |
|
|
|
BrowserNative.zfb_showDevTools(browserId, show); |
|
} |
|
|
|
public Vector2 Size { |
|
get { return new Vector2(_width, _height); } |
|
} |
|
|
|
protected void _Resize(Texture2D newTexture, bool newTextureIsOurs) { |
|
|
|
var width = newTexture.width; |
|
var height = newTexture.height; |
|
|
|
if (textureIsOurs && texture && newTexture != texture) { |
|
Destroy(texture); |
|
} |
|
|
|
_width = width; |
|
_height = height; |
|
|
|
if (IsReady) BrowserNative.zfb_resize(browserId, width, height); |
|
else WhenReady(() => BrowserNative.zfb_resize(browserId, width, height)); |
|
|
|
texture = newTexture; |
|
textureIsOurs = newTextureIsOurs; |
|
|
|
var renderer = GetComponent<Renderer>(); |
|
if (renderer) renderer.material.mainTexture = texture; |
|
|
|
afterResize(texture); |
|
|
|
if (overlay) overlay.Resize(Texture); |
|
|
|
forceNextRender = true; |
|
} |
|
|
|
/** |
|
* Creates a new texture of the given size and starts rendering to that. |
|
*/ |
|
public void Resize(int width, int height) { |
|
var newTexture = new Texture2D(width, height, TextureFormat.ARGB32, generateMipmap); |
|
if (generateMipmap) newTexture.filterMode = FilterMode.Trilinear; |
|
newTexture.wrapMode = TextureWrapMode.Clamp; |
|
|
|
//Clear it to a color: |
|
var pixelCount = width * height; |
|
if (newTexture.mipmapCount > 1) { |
|
//generateMipmap doesn't tell us how many or how big, so quick hack to look it up: |
|
for (int i = 1; i < newTexture.mipmapCount; i++) { |
|
pixelCount += newTexture.GetPixels32(i).Length; |
|
} |
|
} |
|
BrowserNative.LoadSymbols(); |
|
var pixelData = BrowserNative.zfb_flatColorTexture( |
|
pixelCount, baseColor.r, baseColor.g, baseColor.b, baseColor.a |
|
); |
|
newTexture.LoadRawTextureData(pixelData, pixelCount * 4); |
|
newTexture.Apply(); |
|
BrowserNative.zfb_free(pixelData); |
|
|
|
_Resize(newTexture, true); |
|
} |
|
|
|
/** Tells the Browser to render to the given ARGB32 texture. */ |
|
public void Resize(Texture2D newTexture) { |
|
Assert.AreEqual(TextureFormat.ARGB32, newTexture.format); |
|
_Resize(newTexture, false); |
|
} |
|
|
|
/** Sets and gets the current zoom level/DPI scaling factor. */ |
|
public float Zoom { |
|
get { return _zoom; } |
|
set { |
|
if (DeferUnready(() => Zoom = value)) return; |
|
|
|
BrowserNative.zfb_setZoom(browserId, value); |
|
_zoom = value; |
|
} |
|
} |
|
|
|
|
|
/// <summary> |
|
/// Evaluates JavaScript in the browser. |
|
/// |
|
/// (This is JavaScript. Not UnityScript. If you try to feed this UnityScript it will choke and die.) |
|
/// |
|
/// If IsLoaded is false, the script will be deferred until IsLoaded is true. |
|
/// |
|
/// The script is asynchronously executed in a separate process. |
|
/// A promise (see the docs) is returned which you can use to inspect the last evaluated value. |
|
/// For example: |
|
/// browser.EvalJS("var a = 3; a + 3;").Then(ret => Debug.Log("Result: " + (int)ret).Done(); |
|
/// |
|
/// To see script errors and debug issues, call ShowDevTools and use the inspector window to tackle |
|
/// your problems. Also, keep an eye on console output (which gets forwarded to Debug.Log). |
|
/// |
|
/// If desired, you can fill out scriptURL with a URL for the file you are reading from. This can help fill out errors |
|
/// with the correct filename and in some cases allow you to view the source in the inspector. |
|
/// |
|
/// If the page you are viewing has a Content Security Policy (CSP) that prevents evaluating scripts |
|
/// don't use this function, use EvalJSCSP instead. |
|
/// </summary> |
|
/// <param name="script"></param> |
|
/// <param name="scriptURL"></param> |
|
/// <returns></returns> |
|
public IPromise<JSONNode> EvalJS(string script, string scriptURL = "scripted command") { |
|
//Debug.Log("Asked to EvalJS " + script + " loaded state: " + IsLoaded); |
|
var promise = new Promise<JSONNode>(); |
|
var id = nextCallbackId++; |
|
|
|
var jsonScript = new JSONNode(script).AsJSON; |
|
var resultJS = @"try {"+ |
|
"_zfb_event(" + id + ", JSON.stringify(eval(" + jsonScript + " )) || 'null');" + |
|
"} catch(ex) {" + |
|
"_zfb_event(" + id + ", 'fail-' + (JSON.stringify(ex.stack) || 'null'));" + |
|
"}" |
|
; |
|
|
|
registeredCallbacks.Add(id, (val, isError) => { |
|
registeredCallbacks.Remove(id); |
|
if (isError) promise.Reject(new JSException(val)); |
|
else promise.Resolve(val); |
|
}); |
|
|
|
if (!IsLoaded) WhenLoaded(() => _EvalJS(resultJS, scriptURL)); |
|
else _EvalJS(resultJS, scriptURL); |
|
|
|
return promise; |
|
} |
|
|
|
/// <summary> |
|
/// Like EvalJS, but for pages with a Content Security Policy (CSP) that prevents evaluating scripts. |
|
/// This will also work on regular pages, but it has some disadvantages, keep reading. |
|
/// |
|
/// Unlike EvalJS: |
|
/// If your script has syntax errors, you won't get the error in the returned promise, it will just not work. |
|
/// To inspect a result you must `return` it: |
|
/// browser.EvalJS("var a = 3; return a + 3;").Then(ret => Debug.Log("Result: " + (int)ret).Done(); |
|
/// |
|
/// This version of the function doens't use eval() to run the script. This keeps it from getting blocked by a |
|
/// Content Security Policy, but we will be unable to handle and report syntax errors. |
|
/// |
|
/// Don't forget to respect the user's privacy and any applicable ToS terms for the page you are manipulating. |
|
/// </summary> |
|
/// <param name="script"></param> |
|
/// <param name="scriptURL"></param> |
|
/// <returns></returns> |
|
public IPromise<JSONNode> EvalJSCSP(string script, string scriptURL = "scripted command") { |
|
//Debug.Log("Asked to EvalJSCSP " + script + " loaded state: " + IsLoaded); |
|
var promise = new Promise<JSONNode>(); |
|
var id = nextCallbackId++; |
|
|
|
var resultJS = @"try {"+ |
|
"_zfb_event(" + id + ", JSON.stringify( (function() {" + script + "})() ) || 'null');" + |
|
"} catch(ex) {" + |
|
"_zfb_event(" + id + ", 'fail-' + (JSON.stringify(ex.stack) || 'null'));" + |
|
"}" |
|
; |
|
|
|
registeredCallbacks.Add(id, (val, isError) => { |
|
registeredCallbacks.Remove(id); |
|
if (isError) promise.Reject(new JSException(val)); |
|
else promise.Resolve(val); |
|
}); |
|
|
|
if (!IsLoaded) WhenLoaded(() => _EvalJS(resultJS, scriptURL)); |
|
else _EvalJS(resultJS, scriptURL); |
|
|
|
return promise; |
|
} |
|
|
|
protected void _EvalJS(string script, string scriptURL) { |
|
BrowserNative.zfb_evalJS(browserId, script, scriptURL); |
|
} |
|
|
|
|
|
/** |
|
* Looks up {name} by evaluating it as JavaScript code, then calls it with the given arguments. |
|
* |
|
* If IsLoaded is false, the call will be deferred until IsLoaded is true. |
|
* |
|
* Because {name} is evaluated, you can use lookups like "MyGUI.show" or "Foo.getThing().doBob" |
|
* |
|
* The call itself is run asynchronously in a separate process. To get the value returned by the JS back, yield |
|
* on the promise CallFunction returns (in a coroutine) then take a look at promise.Value. |
|
* |
|
* Note that because JSONNode is implicitly convertible from many different types, you can often just |
|
* dump the values in directly when you call this: |
|
* int x = 5, y = 47; |
|
* browser.CallFunction("Menu.setPosition", x, y); |
|
* browser.CallFunction("Menu.setTitle", "Super Game"); |
|
* |
|
*/ |
|
public IPromise<JSONNode> CallFunction(string name, params JSONNode[] arguments) { |
|
var js = name + "("; |
|
|
|
var sep = ""; |
|
foreach (var arg in arguments) { |
|
js += sep + (arg ?? JSONNode.NullNode).AsJSON; |
|
sep = ", "; |
|
} |
|
|
|
js += ");"; |
|
|
|
return EvalJS(js); |
|
} |
|
|
|
/** |
|
* Registers a JavaScript function in the Browser. When called, the given Mono {callback} will be executed. |
|
* |
|
* If IsLoaded is false, the in-page registration will be deferred until IsLoaded is true. |
|
* |
|
* The callback will be executed with one argument: a JSONNode array representing the arguments to the function |
|
* given in the browser. (Access the first argument with args[0], second with args[1], etc.) |
|
* |
|
* The arguments sent back-and forth must be JSON-able. |
|
* |
|
* The JavaScript process runs asynchronously. Callbacks triggered will be collected and fired during the next Update(). |
|
* |
|
* {name} is evaluate-assigned JavaScript. You can use values like "myCallback", "MySystem.myCallback" (only if MySystem |
|
* already exists), or "GetThing().bobFunc" (if GetThing() returns an object you can use later). |
|
* |
|
*/ |
|
public void RegisterFunction(string name, JSCallback callback) { |
|
var id = nextCallbackId++; |
|
registeredCallbacks.Add(id, (value, error) => { |
|
//(we shouldn't be able to get an error here) |
|
callback(value); |
|
}); |
|
|
|
var js = name + " = function() { _zfb_event(" + id + ", JSON.stringify(Array.prototype.slice.call(arguments))); };"; |
|
EvalJS(js); |
|
} |
|
|
|
protected List<Action> thingsToDoClone = new List<Action>(); |
|
protected void ProcessCallbacks() { |
|
while (thingsToDo.Count != 0) { |
|
Profiler.BeginSample("Browser.ProcessCallbacks", this); |
|
|
|
//It's not uncommon for some callbacks to add other callbacks |
|
//To keep from altering thingsToDo while iterating, we'll make a quick copy and use that. |
|
lock (thingsToDo) { |
|
thingsToDoClone.AddRange(thingsToDo); |
|
thingsToDo.Clear(); |
|
} |
|
|
|
foreach (var thingToDo in thingsToDoClone) thingToDo(); |
|
|
|
thingsToDoClone.Clear(); |
|
|
|
Profiler.EndSample(); |
|
} |
|
} |
|
|
|
protected void Update() { |
|
ProcessCallbacks(); |
|
|
|
if (browserId == 0) { |
|
RequestNativeBrowser(); |
|
return;//not ready yet or not loaded |
|
} |
|
|
|
if (!BrowserNative.SymbolsLoaded) return; |
|
|
|
HandleInput(); |
|
} |
|
|
|
protected void LateUpdate() { |
|
//Note: we use LateUpdate here in hopes that commands issued during (anybody's) Update() |
|
//will have a better chance of being completed before we push the render |
|
|
|
if (lastUpdateFrame != Time.frameCount && BrowserNative.NativeLoaded) { |
|
Profiler.BeginSample("Browser.NativeTick"); |
|
BrowserNative.zfb_tick(); |
|
Profiler.EndSample(); |
|
lastUpdateFrame = Time.frameCount; |
|
} |
|
|
|
|
|
if (browserId == 0) return; |
|
|
|
ProcessCallbacks(); |
|
|
|
if (pageReplacer != null) { |
|
pageReplacer(); |
|
pageReplacer = null; |
|
} |
|
|
|
if (browserId == 0) return;//not ready yet or not loaded |
|
if (EnableRendering) Render(); |
|
} |
|
|
|
private Color32[] colorBuffer = null; |
|
|
|
protected void Render() { |
|
if (!BrowserNative.SymbolsLoaded) return; |
|
CheckSanity(); |
|
|
|
BrowserNative.RenderData renderData; |
|
|
|
Profiler.BeginSample("Browser.UpdateTexture.zfb_getImage", this); |
|
try { |
|
renderData = BrowserNative.zfb_getImage(browserId, forceNextRender); |
|
forceNextRender = false; |
|
|
|
if (renderData.pixels == IntPtr.Zero) return;//no changes |
|
|
|
if (renderData.w != texture.width || renderData.h != texture.height) { |
|
//Mismatch. Can happen, for example, when we resize but got an "old" image at the old resolution. (IPC is async.) |
|
return; |
|
} |
|
} finally { |
|
Profiler.EndSample(); |
|
} |
|
|
|
|
|
if (texture.mipmapCount == 1) { |
|
Profiler.BeginSample("Browser.UpdateTexture.LoadRawTextureData", this); |
|
texture.LoadRawTextureData(renderData.pixels, renderData.w * renderData.h * 4); |
|
Profiler.EndSample(); |
|
} else { |
|
//whelp, this is gonna be slow. |
|
//First, having Unity calculate mipmaps is slow. Second, we can't just LoadRawTextureData because it doesn't |
|
//contain mip levels. Third, LoadRawTextureData and Color32 have different in-memory byte orders (for our texture type). |
|
|
|
if (colorBuffer == null || colorBuffer.Length != renderData.w * renderData.h) { |
|
colorBuffer = new Color32[renderData.w * renderData.h]; |
|
} |
|
|
|
Profiler.BeginSample("Browser.UpdateTexture.CopyData", this); |
|
var handle = GCHandle.Alloc(colorBuffer, GCHandleType.Pinned); |
|
BrowserNative.zfb_copyToColor32(renderData.pixels, handle.AddrOfPinnedObject(), renderData.w * renderData.h); |
|
handle.Free(); |
|
Profiler.EndSample(); |
|
|
|
Profiler.BeginSample("Browser.UpdateTexture.SetPixels32", this); |
|
texture.SetPixels32(colorBuffer); |
|
Profiler.EndSample(); |
|
} |
|
|
|
Profiler.BeginSample("Browser.UpdateTexture.Apply", this); |
|
{ |
|
texture.Apply(true); |
|
} |
|
Profiler.EndSample(); |
|
|
|
onTextureUpdated(); |
|
} |
|
|
|
/** |
|
* Adds the given browser as an overlay of this browser. |
|
* |
|
* The overlaid browser will appear transparently over the top of us on our texture. |
|
* {overlayBrowser} must not have an overlay and must be sized exactly the same as {this}. |
|
* Additionally, overlayBrowser.EnableRendering must be false. You still need to |
|
* do something to handle getting input to the right places. Overlays take a notable performance |
|
* hit on rendering (CPU alpha compositing). |
|
* |
|
* Overlays are used internally to implement context menus and pop-up dialogs (alert, onbeforeunload). |
|
* If the page causes any type of dialog, the overlay will be replaced. |
|
* |
|
* Overlays will be resized onto our texture when we are resized. The sizes must always match exactly. |
|
* |
|
* Remove the overlay (SetOverlay(null)) before closing either browser. |
|
* |
|
* (Note: though you can't set B as an overlay to A when B has an overlay, you can set |
|
* an overlay on B /while/ it is the overlay for A. For an example of this, try |
|
* right-clicking on the text area inside a prompt() popup. The context menu that |
|
* appears is an overlay to the overlay to the actual browser.) |
|
*/ |
|
public void SetOverlay(Browser overlayBrowser) { |
|
if (DeferUnready(() => SetOverlay(overlayBrowser))) return; |
|
if (overlayBrowser && overlayBrowser.DeferUnready(() => SetOverlay(overlayBrowser))) return; |
|
|
|
if (!overlayBrowser) { |
|
BrowserNative.zfb_setOverlay(browserId, 0); |
|
overlay = null; |
|
} else { |
|
overlay = overlayBrowser; |
|
|
|
if ( |
|
!overlay.Texture || |
|
(overlay.Texture.width != Texture.width || overlay.Texture.height != Texture.height) |
|
) { |
|
overlay.Resize(Texture); |
|
} |
|
|
|
BrowserNative.zfb_setOverlay(browserId, overlayBrowser.browserId); |
|
} |
|
} |
|
|
|
protected void HandleInput() { |
|
if (_uiHandler == null || !EnableInput) return; |
|
CheckSanity(); |
|
|
|
browserInput.HandleInput(); |
|
} |
|
|
|
protected void OnApplicationFocus(bool focus) { |
|
if (!focus && browserInput != null) browserInput.HandleFocusLoss(); |
|
} |
|
|
|
protected void OnApplicationPause(bool paused) { |
|
if (paused && browserInput != null) browserInput.HandleFocusLoss(); |
|
} |
|
|
|
/** |
|
* Updates the cursor on our UIHandler. |
|
* Usually you don't need to call this, but if you are sharing input with an overlay, call this any time the |
|
* "focused" overlay changes. |
|
*/ |
|
public void UpdateCursor() { |
|
if (UIHandler == null) return; |
|
if (DeferUnready(UpdateCursor)) return; |
|
|
|
int w, h; |
|
var cursorType = BrowserNative.zfb_getMouseCursor(browserId, out w, out h); |
|
if (cursorType != BrowserNative.CursorType.Custom) { |
|
UIHandler.BrowserCursor.SetActiveCursor(cursorType); |
|
} else { |
|
if (w == 0 && h == 0) { |
|
//bad cursor |
|
UIHandler.BrowserCursor.SetActiveCursor(BrowserNative.CursorType.None); |
|
return; |
|
} |
|
|
|
var buffer = new Color32[w * h]; |
|
int hx, hy; |
|
|
|
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); |
|
BrowserNative.zfb_getMouseCustomCursor(browserId, handle.AddrOfPinnedObject(), w, h, out hx, out hy); |
|
handle.Free(); |
|
|
|
var tex = new Texture2D(w, h, TextureFormat.ARGB32, false); |
|
tex.SetPixels32(buffer); |
|
//in-memory only, no Apply() |
|
|
|
UIHandler.BrowserCursor.SetCustomCursor(tex, new Vector2(hx, hy)); |
|
DestroyImmediate(tex); |
|
} |
|
} |
|
|
|
/** |
|
* Take an action on a download. |
|
* |
|
* At the outset: |
|
* Begin: Starts the download. Saves to the given file if given. If fileName is null, the user will be prompted. |
|
* Cancel: Does nothing with a download. |
|
* |
|
* After starting a download: |
|
* Pause, Cancel, Resume: Does what it says on the tin. |
|
* |
|
* Once a download is finished or canceled it is not valid to call this function for that download any more. |
|
* |
|
* fileName is ignored except when beginning a download. |
|
*/ |
|
public void DownloadCommand(int downloadId, BrowserNative.DownloadAction action, string fileName = null) { |
|
CheckSanity(); |
|
|
|
BrowserNative.zfb_downloadCommand(browserId, downloadId, action, fileName); |
|
} |
|
|
|
/// <summary> |
|
/// Injects the given unicode text to the browser as if it had been typed. |
|
/// (No key press events are generated.) |
|
/// </summary> |
|
/// <param name="text"></param> |
|
public void TypeText(string text) { |
|
for (int i = 0; i < text.Length; i++) { |
|
var ev = new Event() { |
|
type = EventType.KeyDown, |
|
keyCode = 0, |
|
character = text[i], |
|
}; |
|
browserInput.extraEventsToInject.Add(ev); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Sends key presses/releases to the browser. |
|
/// </summary> |
|
/// <param name="key"></param> |
|
/// <param name="action"></param> |
|
public void PressKey(KeyCode key, KeyAction action = KeyAction.PressAndRelease) { |
|
if (action == KeyAction.Press || action == KeyAction.PressAndRelease) { |
|
var ev = new Event() { |
|
type = EventType.KeyDown, |
|
keyCode = key, |
|
character = (char)0, |
|
}; |
|
browserInput.extraEventsToInject.Add(ev); |
|
} |
|
|
|
if (action == KeyAction.Release || action == KeyAction.PressAndRelease) { |
|
var ev = new Event() { |
|
type = EventType.KeyUp, |
|
keyCode = key, |
|
character = (char)0, |
|
}; |
|
browserInput.extraEventsToInject.Add(ev); |
|
} |
|
|
|
} |
|
|
|
internal void _RaiseFocusEvent(bool mouseIsFocused, bool keyboardIsFocused) { |
|
focusState.hasMouseFocus = mouseIsFocused; |
|
focusState.hasKeyboardFocus = keyboardIsFocused; |
|
onBrowserFocus(mouseIsFocused, keyboardIsFocused); |
|
} |
|
|
|
} |
|
|
|
}
|
|
|