2024.5.23杨华提交2
This commit is contained in:
parent
6fe3bcbb66
commit
09a5c704f2
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 56b8faaae4b14ef46be6a4552ecae7fb
|
||||
folderAsset: yes
|
||||
timeCreated: 1447088713
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
public class FlagsFieldAttribute : PropertyAttribute {}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 578312a754d9da142ad199b54e09488f
|
||||
timeCreated: 1450462477
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 35bf0bd4ded61a34f9be34f6738e4010
|
||||
timeCreated: 1447801917
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 10
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,216 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using AOT;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Callbacks used by Browser.cs
|
||||
/// Man, that file's getting big. (Breaking it up would likely involve backwards-incompatible API changes, so
|
||||
/// let it be for now.)
|
||||
/// </summary>
|
||||
public partial class Browser {
|
||||
/// <summary>
|
||||
/// Map of browserId => Browser fro looking up C# objects when we get a native callback.
|
||||
///
|
||||
/// Note that this reflects the native state, that is, we include Browsers that havne't fully initialized yet,
|
||||
/// but instead list what we need to lookup callbacks coming from the native side.
|
||||
///
|
||||
/// (We used to rely on closures and generated trampolines for this data, but il2cpp doens't support that.)
|
||||
/// </summary>
|
||||
internal static Dictionary<int, Browser> allBrowsers = new Dictionary<int, Browser>();
|
||||
|
||||
private static Browser GetBrowser(int browserId) {
|
||||
lock (allBrowsers) {
|
||||
Browser ret;
|
||||
if (allBrowsers.TryGetValue(browserId, out ret)) return ret;
|
||||
}
|
||||
|
||||
Debug.LogWarning("Got a callback for brower id " + browserId + " which doesn't exist.");
|
||||
return null;
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.ForwardJSCallFunc))]
|
||||
private static void CB_ForwardJSCallFunc(int browserId, int callbackId, string data, int size) {
|
||||
var browser = GetBrowser(browserId);
|
||||
if (!browser) return;
|
||||
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
|
||||
JSResultFunc cb;
|
||||
if (!browser.registeredCallbacks.TryGetValue(callbackId, out cb)) {
|
||||
Debug.LogWarning("Got a JS callback for event " + callbackId + ", but no such event is registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
var isError = false;
|
||||
if (data.StartsWith("fail-")) {
|
||||
isError = true;
|
||||
data = data.Substring(5);
|
||||
}
|
||||
|
||||
JSONNode node;
|
||||
try {
|
||||
node = JSONNode.Parse(data);
|
||||
} catch (SerializationException) {
|
||||
Debug.LogWarning("Invalid JSON sent from browser: " + data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cb(node, isError);
|
||||
} catch (Exception ex) {
|
||||
//user's function died, log it and carry on
|
||||
Debug.LogException(ex);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.ChangeFunc))]
|
||||
private static void CB_ChangeFunc(int browserId, BrowserNative.ChangeType changeType, string arg1) {
|
||||
//Note: we may have been Object.Destroy'd at this point, so guard against that.
|
||||
//That means we can't trust that `browser == null` means we don't have a browser, we may have an object that
|
||||
//is destroyed, but not actually null.
|
||||
Browser browser;
|
||||
lock (allBrowsers) {
|
||||
if (!allBrowsers.TryGetValue(browserId, out browser)) return;
|
||||
}
|
||||
|
||||
if (changeType == BrowserNative.ChangeType.CHT_BROWSER_CLOSE) {
|
||||
//We can't continue if the browser is closed, so goodbye.
|
||||
|
||||
//At this point, we may or may not be destroyed, but if not, become destroyed.
|
||||
//Debug.Log("Got close notification for " + unsafeBrowserId);
|
||||
if (browser) {
|
||||
//Need to be destroyed.
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
Destroy(browser.gameObject);
|
||||
});
|
||||
} else {
|
||||
//If we are (Unity) destroyed, we won't get another update, so we can't rely on thingsToDo
|
||||
//That said, there's not anything else for us to do but step out of allThingsToRemember.
|
||||
}
|
||||
|
||||
//The native side has acknowledged it's done, now we can finally let the native trampolines be GC'd
|
||||
lock (allThingsToRemember) allThingsToRemember.Remove(browser.unsafeBrowserId);
|
||||
|
||||
//Now that we know we can allow ourselves to be GC'd and destroyed (without leaking to allThingsToRemember)
|
||||
//go ahead and allow it. (And we won't get any more native callbacks at this point.)
|
||||
lock (allBrowsers) allBrowsers.Remove(browserId);
|
||||
|
||||
//Just in case someone tries to call something, make sure CheckSanity and such fail.
|
||||
browser.browserId = 0;
|
||||
} else if (browser) {
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => browser.OnItemChange(changeType, arg1));
|
||||
}
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.DisplayDialogFunc))]
|
||||
private static void CB_DisplayDialogFunc(
|
||||
int browserId, BrowserNative.DialogType dialogType, IntPtr textPtr,
|
||||
IntPtr promptTextPtr, IntPtr sourceURL
|
||||
) {
|
||||
var browser = GetBrowser(browserId);
|
||||
if (!browser) return;
|
||||
|
||||
var text = Util.PtrToStringUTF8(textPtr);
|
||||
var promptText = Util.PtrToStringUTF8(promptTextPtr);
|
||||
//var url = Util.PtrToStringUTF8(urlPtr);
|
||||
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
browser.CreateDialogHandler();
|
||||
browser.dialogHandler.HandleDialog(dialogType, text, promptText);
|
||||
});
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.ShowContextMenuFunc))]
|
||||
private static void CB_ShowContextMenuFunc(
|
||||
int browserId, string json, int x, int y, BrowserNative.ContextMenuOrigin origin
|
||||
) {
|
||||
var browser = GetBrowser(browserId);
|
||||
if (!browser) return;
|
||||
|
||||
if (json != null && (browser.allowContextMenuOn & origin) == 0) {
|
||||
//ignore this
|
||||
BrowserNative.zfb_sendContextMenuResults(browserId, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
if (json != null) browser.CreateDialogHandler();
|
||||
if (browser.dialogHandler != null) browser.dialogHandler.HandleContextMenu(json, x, y);
|
||||
});
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.ConsoleFunc))]
|
||||
private static void CB_ConsoleFunc(int browserId, string message, string source, int line) {
|
||||
var browser = GetBrowser(browserId);
|
||||
if (!browser) return;
|
||||
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
browser.onConsoleMessage(message, source + ":" + line);
|
||||
});
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.ReadyFunc))]
|
||||
private static void CB_ReadyFunc(int browserId) {
|
||||
var browser = GetBrowser(browserId);
|
||||
if (!browser) return;
|
||||
|
||||
//We could be on any thread at this time, so schedule the callbacks to fire during the next InputUpdate
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
browser.browserId = browserId;
|
||||
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
browser.onNativeReady(browserId);
|
||||
});
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.NavStateFunc))]
|
||||
private static void CB_NavStateFunc(int browserId, bool canGoBack, bool canGoForward, bool lodaing, IntPtr urlRaw) {
|
||||
var browser = GetBrowser(browserId);
|
||||
if (!browser) return;
|
||||
|
||||
var url = Util.PtrToStringUTF8(urlRaw);
|
||||
|
||||
lock (browser.thingsToDo) browser.thingsToDo.Add(() => {
|
||||
browser.navState.canGoBack = canGoBack;
|
||||
browser.navState.canGoForward = canGoForward;
|
||||
browser.navState.loading = lodaing;
|
||||
browser.navState.url = url;
|
||||
|
||||
browser._url = url;//update the inspector
|
||||
|
||||
browser.onNavStateChange();
|
||||
});
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.NewWindowFunc))]
|
||||
private static void CB_NewWindowFunc(int creatorBrowserId, int newBrowserId, IntPtr urlPtr) {
|
||||
var browser = GetBrowser(creatorBrowserId);
|
||||
if (!browser) return;
|
||||
|
||||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
var url = Util.PtrToStringUTF8(urlPtr);
|
||||
if (url == "about:inspector" || browser.newWindowAction == NewWindowAction.NewWindow) lock (browser.thingsToDo) {
|
||||
browser.thingsToDo.Add(() => {
|
||||
PopUpBrowser.Create(newBrowserId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
lock (browser.thingsToDo) {
|
||||
browser.thingsToDo.Add(() => {
|
||||
var newBrowser = browser.newWindowHandler.CreateBrowser(browser);
|
||||
newBrowser.RequestNativeBrowser(newBrowserId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a30036008e7d3434291fcfba205bf239
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,170 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CursorType = ZenFulcrum.EmbeddedBrowser.BrowserNative.CursorType;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Manages finding and copying cursors for you.
|
||||
* Each instance has one active cursor and a Texture2D you can read from to use it.
|
||||
*/
|
||||
public class BrowserCursor {
|
||||
public class CursorInfo {
|
||||
public int atlasOffset;
|
||||
public Vector2 hotspot;
|
||||
}
|
||||
|
||||
private static Dictionary<CursorType, CursorInfo> mapping = new Dictionary<CursorType, CursorInfo>();
|
||||
|
||||
private static bool loaded = false;
|
||||
|
||||
private static int size;
|
||||
private static Texture2D allCursors;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the mouse cursor's appearance or hotspot changes.
|
||||
/// Also fired when the mouse enters/leaves the browser.
|
||||
/// </summary>
|
||||
public event Action cursorChange = () => {};
|
||||
|
||||
private static void Load() {
|
||||
if (loaded) return;
|
||||
allCursors = Resources.Load<Texture2D>("Browser/Cursors");
|
||||
if (!allCursors) throw new Exception("Failed to find browser allCursors");
|
||||
|
||||
size = allCursors.height;
|
||||
|
||||
var listObj = Resources.Load<TextAsset>("Browser/Cursors");
|
||||
|
||||
foreach (var row in listObj.text.Split('\n')) {
|
||||
var info = row.Split(',');
|
||||
|
||||
var k = (CursorType)Enum.Parse(typeof(CursorType), info[0]);
|
||||
var v = new CursorInfo() {
|
||||
atlasOffset = int.Parse(info[1]),
|
||||
hotspot = new Vector2(int.Parse(info[2]), int.Parse(info[3])),
|
||||
};
|
||||
mapping[k] = v;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture for the current cursor.
|
||||
* If the cursor should be hidden, this will be null.
|
||||
*/
|
||||
public virtual Texture2D Texture { get; protected set; }
|
||||
|
||||
/**
|
||||
* Hotspot for the current cursor. (0, 0) indicates the top-left of the texture is the hotspot.
|
||||
* (1, 1) indicates the bottom-right.
|
||||
*/
|
||||
public virtual Vector2 Hotspot { get; protected set; }
|
||||
|
||||
private bool _hasMouse;
|
||||
/// <summary>
|
||||
/// True when the mouse is over the browser, false otherwise.
|
||||
/// </summary>
|
||||
public bool HasMouse {
|
||||
get {
|
||||
return _hasMouse;
|
||||
}
|
||||
set {
|
||||
_hasMouse = value;
|
||||
cursorChange();
|
||||
}
|
||||
}
|
||||
|
||||
protected Texture2D normalTexture;
|
||||
protected Texture2D customTexture;
|
||||
|
||||
public BrowserCursor() {
|
||||
Load();
|
||||
|
||||
normalTexture = new Texture2D(size, size, TextureFormat.ARGB32, false);
|
||||
#if UNITY_EDITOR
|
||||
normalTexture.alphaIsTransparency = true;
|
||||
#endif
|
||||
|
||||
SetActiveCursor(BrowserNative.CursorType.Pointer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the active cursor type. After calling this you can access the cursor image through this.Texture.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
public virtual void SetActiveCursor(CursorType type) {
|
||||
if (type == CursorType.Custom) throw new ArgumentException("Use SetCustomCursor to set custom cursors.", "type");
|
||||
|
||||
if (type == CursorType.None) {
|
||||
Texture = null;
|
||||
//Side note: if you copy down a bunch of transparent pixels and try to set the mouse cursor to that
|
||||
//both OS X and Windows fail to do what you'd expect.
|
||||
//Edit: OS X is now crashing for me if you try to do that.
|
||||
cursorChange();
|
||||
return;
|
||||
}
|
||||
|
||||
var info = mapping[type];
|
||||
var pixelData = allCursors.GetPixels(info.atlasOffset * size, 0, size, size);
|
||||
|
||||
Hotspot = info.hotspot;
|
||||
|
||||
normalTexture.SetPixels(pixelData);
|
||||
|
||||
normalTexture.Apply(true);
|
||||
|
||||
Texture = normalTexture;
|
||||
|
||||
cursorChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a custom cursor.
|
||||
/// </summary>
|
||||
/// <param name="cursor">ARGB texture to set</param>
|
||||
/// <param name="hotspot"></param>
|
||||
public virtual void SetCustomCursor(Texture2D cursor, Vector2 hotspot) {
|
||||
var pixels = cursor.GetPixels32();
|
||||
|
||||
//First off, is it completely blank? 'Cuz if so that can cause OS X to crash.
|
||||
var hasData = false;
|
||||
for (int i = 0; i < pixels.Length; i++) {
|
||||
if (pixels[i].a != 0) {
|
||||
hasData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
//it's blank, so handle it like a regular blank cursor
|
||||
SetActiveCursor(CursorType.None);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customTexture || customTexture.width != cursor.width || customTexture.height != cursor.height) {
|
||||
Object.Destroy(customTexture);
|
||||
customTexture = new Texture2D(cursor.width, cursor.height, TextureFormat.ARGB32, false);
|
||||
#if UNITY_EDITOR
|
||||
customTexture.alphaIsTransparency = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
customTexture.SetPixels32(pixels);
|
||||
customTexture.Apply(true);
|
||||
|
||||
this.Hotspot = hotspot;
|
||||
|
||||
Texture = customTexture;
|
||||
cursorChange();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d74b3f6909bb41f4c86d03a80156416f
|
||||
timeCreated: 1448061720
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,335 @@
|
|||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
#define ZF_OSX
|
||||
#endif
|
||||
#if UNITY_EDITOR_LINX || UNITY_STANDALONE_LINUX
|
||||
#define ZF_LINUX
|
||||
#endif
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/** Helper class for reading data from an IUIHandler, converting it, and feeding it to the native backend. */
|
||||
internal class BrowserInput {
|
||||
|
||||
private readonly Browser browser;
|
||||
|
||||
public BrowserInput(Browser browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
private bool kbWasFocused = false;
|
||||
private bool mouseWasFocused = false;
|
||||
|
||||
public void HandleInput() {
|
||||
browser.UIHandler.InputUpdate();
|
||||
bool focusChanged = false;
|
||||
|
||||
if (browser.UIHandler.MouseHasFocus || mouseWasFocused) {
|
||||
HandleMouseInput();
|
||||
}
|
||||
if (browser.UIHandler.MouseHasFocus != mouseWasFocused) {
|
||||
browser.UIHandler.BrowserCursor.HasMouse = browser.UIHandler.MouseHasFocus;
|
||||
focusChanged = true;
|
||||
}
|
||||
mouseWasFocused = browser.UIHandler.MouseHasFocus;
|
||||
|
||||
|
||||
|
||||
if (kbWasFocused != browser.UIHandler.KeyboardHasFocus) focusChanged = true;
|
||||
|
||||
if (browser.UIHandler.KeyboardHasFocus) {
|
||||
if (!kbWasFocused) {
|
||||
BrowserNative.zfb_setFocused(browser.browserId, kbWasFocused = true);
|
||||
}
|
||||
HandleKeyInput();
|
||||
} else {
|
||||
if (kbWasFocused) {
|
||||
BrowserNative.zfb_setFocused(browser.browserId, kbWasFocused = false);
|
||||
}
|
||||
}
|
||||
|
||||
if (focusChanged) {
|
||||
browser._RaiseFocusEvent(browser.UIHandler.MouseHasFocus, browser.UIHandler.KeyboardHasFocus);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<KeyCode> keysToReleaseOnFocusLoss = new HashSet<KeyCode>();
|
||||
public List<Event> extraEventsToInject = new List<Event>();
|
||||
|
||||
private MouseButton prevButtons = 0;
|
||||
private Vector2 prevPos;
|
||||
|
||||
private class ButtonHistory {
|
||||
public float lastPressTime;
|
||||
public int repeatCount;
|
||||
public Vector3 lastPosition;
|
||||
|
||||
public void ButtonPress(Vector3 mousePos, IBrowserUI uiHandler, Vector2 browserSize) {
|
||||
var now = Time.realtimeSinceStartup;
|
||||
|
||||
if (now - lastPressTime > uiHandler.InputSettings.multiclickSpeed) {
|
||||
//too long ago? forget the past
|
||||
repeatCount = 0;
|
||||
}
|
||||
|
||||
if (repeatCount > 0) {
|
||||
//close enough to be a multiclick?
|
||||
var p1 = Vector2.Scale(mousePos, browserSize);
|
||||
var p2 = Vector2.Scale(lastPosition, browserSize);
|
||||
if (Vector2.Distance(p1, p2) > uiHandler.InputSettings.multiclickTolerance) {
|
||||
repeatCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
repeatCount++;
|
||||
|
||||
lastPressTime = now;
|
||||
lastPosition = mousePos;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ButtonHistory leftClickHistory = new ButtonHistory();
|
||||
|
||||
private void HandleMouseInput() {
|
||||
var handler = browser.UIHandler;
|
||||
var mousePos = handler.MousePosition;
|
||||
|
||||
var currentButtons = handler.MouseButtons;
|
||||
var mouseScroll = handler.MouseScroll;
|
||||
|
||||
if (mousePos != prevPos) {
|
||||
BrowserNative.zfb_mouseMove(browser.browserId, mousePos.x, 1 - mousePos.y);
|
||||
}
|
||||
|
||||
FeedScrolling(mouseScroll, handler.InputSettings.scrollSpeed);
|
||||
|
||||
var leftChange = (prevButtons & MouseButton.Left) != (currentButtons & MouseButton.Left);
|
||||
var leftDown = (currentButtons & MouseButton.Left) == MouseButton.Left;
|
||||
var middleChange = (prevButtons & MouseButton.Middle) != (currentButtons & MouseButton.Middle);
|
||||
var middleDown = (currentButtons & MouseButton.Middle) == MouseButton.Middle;
|
||||
var rightChange = (prevButtons & MouseButton.Right) != (currentButtons & MouseButton.Right);
|
||||
var rightDown = (currentButtons & MouseButton.Right) == MouseButton.Right;
|
||||
|
||||
if (leftChange) {
|
||||
if (leftDown) leftClickHistory.ButtonPress(mousePos, handler, browser.Size);
|
||||
BrowserNative.zfb_mouseButton(
|
||||
browser.browserId, BrowserNative.MouseButton.MBT_LEFT, leftDown,
|
||||
leftDown ? leftClickHistory.repeatCount : 0
|
||||
);
|
||||
}
|
||||
if (middleChange) {
|
||||
//no double-clicks, to be consistent with other browsers
|
||||
BrowserNative.zfb_mouseButton(
|
||||
browser.browserId, BrowserNative.MouseButton.MBT_MIDDLE, middleDown, 1
|
||||
);
|
||||
}
|
||||
if (rightChange) {
|
||||
//no double-clicks, to be consistent with other browsers
|
||||
BrowserNative.zfb_mouseButton(
|
||||
browser.browserId, BrowserNative.MouseButton.MBT_RIGHT, rightDown, 1
|
||||
);
|
||||
}
|
||||
|
||||
prevPos = mousePos;
|
||||
prevButtons = currentButtons;
|
||||
}
|
||||
|
||||
private Vector2 accumulatedScroll;
|
||||
private float lastScrollEvent;
|
||||
/// <summary>
|
||||
/// How often (in sec) we can send scroll events to the browser without it choking up.
|
||||
/// The right number seems to depend on how hard the page is to render, so there's not a perfect number.
|
||||
/// Hopefully this one works well, though.
|
||||
/// </summary>
|
||||
private const float maxScrollEventRate = 1 / 15f;
|
||||
|
||||
/// <summary>
|
||||
/// Feeds scroll events to the browser.
|
||||
/// In particular, it will clump together scrolling "floods" into fewer larger scrolls
|
||||
/// to prevent the backend from getting choked up and taking forever to execute the requests.
|
||||
/// </summary>
|
||||
/// <param name="mouseScroll"></param>
|
||||
private void FeedScrolling(Vector2 mouseScroll, float scrollSpeed) {
|
||||
accumulatedScroll += mouseScroll * scrollSpeed;
|
||||
|
||||
if (accumulatedScroll.sqrMagnitude != 0 && Time.realtimeSinceStartup > lastScrollEvent + maxScrollEventRate) {
|
||||
//Debug.Log("Do scroll: " + accumulatedScroll);
|
||||
|
||||
//The backend seems to have trouble coping with horizontal AND vertical scroll. So only do one at a time.
|
||||
//(And if we do both at once, vertical appears to get priority and horizontal gets ignored.)
|
||||
|
||||
if (Mathf.Abs(accumulatedScroll.x) > Mathf.Abs(accumulatedScroll.y)) {
|
||||
BrowserNative.zfb_mouseScroll(browser.browserId, (int)accumulatedScroll.x, 0);
|
||||
accumulatedScroll.x = 0;
|
||||
accumulatedScroll.y = Mathf.Round(accumulatedScroll.y * .5f);//reduce the thing we weren't doing so it's less likely to accumulate strange
|
||||
} else {
|
||||
BrowserNative.zfb_mouseScroll(browser.browserId, 0, (int)accumulatedScroll.y);
|
||||
accumulatedScroll.x = Mathf.Round(accumulatedScroll.x * .5f);
|
||||
accumulatedScroll.y = 0;
|
||||
}
|
||||
|
||||
lastScrollEvent = Time.realtimeSinceStartup;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleKeyInput() {
|
||||
var keyEvents = browser.UIHandler.KeyEvents;
|
||||
if (keyEvents.Count > 0) HandleKeyInput(keyEvents);
|
||||
|
||||
if (extraEventsToInject.Count > 0) {
|
||||
HandleKeyInput(extraEventsToInject);
|
||||
extraEventsToInject.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleKeyInput(List<Event> keyEvents) {
|
||||
|
||||
#if ZF_OSX
|
||||
ReconstructInputs(keyEvents);
|
||||
#endif
|
||||
|
||||
foreach (var ev in keyEvents) {
|
||||
var keyCode = KeyMappings.GetWindowsKeyCode(ev);
|
||||
if (ev.character == '\n') ev.character = '\r';//'cuz that's what Chromium expects
|
||||
|
||||
if (ev.character == 0) {
|
||||
if (ev.type == EventType.KeyDown) keysToReleaseOnFocusLoss.Add(ev.keyCode);
|
||||
else keysToReleaseOnFocusLoss.Remove(ev.keyCode);
|
||||
}
|
||||
|
||||
// if (false) {
|
||||
// if (ev.character != 0) Debug.Log("k >>> " + ev.character);
|
||||
// else if (ev.type == EventType.KeyUp) Debug.Log("k ^^^ " + ev.keyCode);
|
||||
// else if (ev.type == EventType.KeyDown) Debug.Log("k vvv " + ev.keyCode);
|
||||
// }
|
||||
|
||||
FireCommands(ev);
|
||||
|
||||
if (ev.character != 0 && ev.type == EventType.KeyDown) {
|
||||
#if ZF_LINUX
|
||||
//It seems, on Linux, we don't get keydown, keypress, keyup, we just get a keypress, keyup.
|
||||
//So, fire the keydown just before the keypress.
|
||||
BrowserNative.zfb_keyEvent(browser.browserId, true, keyCode);
|
||||
//Thanks for being consistent, Unity.
|
||||
#endif
|
||||
|
||||
BrowserNative.zfb_characterEvent(browser.browserId, ev.character, keyCode);
|
||||
} else {
|
||||
BrowserNative.zfb_keyEvent(browser.browserId, ev.type == EventType.KeyDown, keyCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleFocusLoss() {
|
||||
foreach (var keyCode in keysToReleaseOnFocusLoss) {
|
||||
//Debug.Log("Key " + keyCode + " is held, release");
|
||||
var wCode = KeyMappings.GetWindowsKeyCode(new Event() { keyCode = keyCode });
|
||||
BrowserNative.zfb_keyEvent(browser.browserId, false, wCode);
|
||||
}
|
||||
|
||||
keysToReleaseOnFocusLoss.Clear();
|
||||
}
|
||||
|
||||
#if ZF_OSX
|
||||
/** Used by ReconstructInputs */
|
||||
protected HashSet<KeyCode> pressedKeys = new HashSet<KeyCode>();
|
||||
|
||||
/**
|
||||
* OS X + Unity has issues.
|
||||
*
|
||||
* Mac editor: If I press cmd+A: The "keydown A" event doesn't get sent,
|
||||
* though we do get a keypress A and a keyup A.
|
||||
* Mac player: We get duplicate keyUPs normally. If cmd is down we get duplicate keyDOWNs instead.
|
||||
*/
|
||||
protected void ReconstructInputs(List<Event> keyEvents) {
|
||||
for (int i = 0; i < keyEvents.Count; ++i) {//int loop, not iterator, we mutate during iteration
|
||||
var ev = keyEvents[i];
|
||||
|
||||
if (ev.type == EventType.KeyDown && ev.character == 0) {
|
||||
pressedKeys.Add(ev.keyCode);
|
||||
|
||||
//Repeated keydown events sent in the same frame are always bogus (but valid if in different
|
||||
//frames for held key repeats)
|
||||
//Remove duplicated key down events in this tick.
|
||||
for (int j = i + 1; j < keyEvents.Count; ++j) {
|
||||
if (keyEvents[j].Equals(ev)) keyEvents.RemoveAt(j--);
|
||||
}
|
||||
} else if (ev.type == EventType.KeyDown) {
|
||||
//key down with character.
|
||||
//...did the key actually get pressed, though?
|
||||
if (ev.keyCode != KeyCode.None && !pressedKeys.Contains(ev.keyCode)) {
|
||||
//no. insert a keydown before the press
|
||||
var downEv = new Event(ev) {
|
||||
type = EventType.KeyDown,
|
||||
character = (char)0
|
||||
};
|
||||
keyEvents.Insert(i++, downEv);
|
||||
pressedKeys.Add(ev.keyCode);
|
||||
}
|
||||
} else if (ev.type == EventType.KeyUp) {
|
||||
if (!pressedKeys.Contains(ev.keyCode)) {
|
||||
//Ignore duplicate key up events
|
||||
keyEvents.RemoveAt(i--);
|
||||
}
|
||||
|
||||
pressedKeys.Remove(ev.keyCode);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* OS X + Unity has issues.
|
||||
* Commands won't be run if the command is not in the application menu.
|
||||
* Here we trap keystrokes and manually fire the relevant events in the browser.
|
||||
*
|
||||
* Also, ctrl+A stopped working with CEF at some point on Windows.
|
||||
*/
|
||||
protected void FireCommands(Event ev) {
|
||||
|
||||
#if ZF_OSX
|
||||
if (ev.type != EventType.KeyDown || ev.character != 0 || !ev.command) return;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.C:
|
||||
browser.SendFrameCommand(BrowserNative.FrameCommand.Copy);
|
||||
break;
|
||||
case KeyCode.X:
|
||||
browser.SendFrameCommand(BrowserNative.FrameCommand.Cut);
|
||||
break;
|
||||
case KeyCode.V:
|
||||
browser.SendFrameCommand(BrowserNative.FrameCommand.Paste);
|
||||
break;
|
||||
case KeyCode.A:
|
||||
browser.SendFrameCommand(BrowserNative.FrameCommand.SelectAll);
|
||||
break;
|
||||
case KeyCode.Z:
|
||||
if (ev.shift) browser.SendFrameCommand(BrowserNative.FrameCommand.Redo);
|
||||
else browser.SendFrameCommand(BrowserNative.FrameCommand.Undo);
|
||||
break;
|
||||
case KeyCode.Y:
|
||||
//I, for one, prefer Y for redo, but shift+Z is more idiomatic on OS X
|
||||
//Support both.
|
||||
browser.SendFrameCommand(BrowserNative.FrameCommand.Redo);
|
||||
break;
|
||||
}
|
||||
|
||||
#else
|
||||
//mmm, yeah. I guess Unity doesn't send us the keydown on a ctrl+a keystroke anymore.
|
||||
if (ev.type != EventType.KeyUp || !ev.control) return;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.A:
|
||||
browser.SendFrameCommand(BrowserNative.FrameCommand.SelectAll);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d00cb9636aac20d4989efeaf7de81909
|
||||
timeCreated: 1450218467
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 78ee81db88d224e40843c8e826697dee
|
||||
timeCreated: 1447282504
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,28 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for setting options on the browser system as a whole berfore the
|
||||
/// backend initalizes.
|
||||
/// </summary>
|
||||
public class BrowserSystemSettings : MonoBehaviour {
|
||||
[Tooltip("Where should we save the user's web profile (cookies, localStorage, etc.)? Leave blank to forget every times we restart.")]
|
||||
public string profilePath;
|
||||
|
||||
[Tooltip("What user agent should we send to web pages when we request sites? Leave blank to use the default.")]
|
||||
public string userAgent;
|
||||
|
||||
public void Awake() {
|
||||
if (!string.IsNullOrEmpty(profilePath)) {
|
||||
BrowserNative.ProfilePath = profilePath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(userAgent)) {
|
||||
UserAgent.SetUserAgent(userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 731482151f2ac2041ab65282156b1664
|
||||
timeCreated: 1510797762
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e460e45c0d5976544a2af3c86b00bdb9
|
||||
folderAsset: yes
|
||||
timeCreated: 1453258062
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,31 @@
|
|||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// These classes handle rendering the cursor of a browser.
|
||||
///
|
||||
/// Using one is optional. You can opt not to show a cursor.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(PointerUIBase))]
|
||||
abstract public class CursorRendererBase : MonoBehaviour {
|
||||
protected BrowserCursor cursor;
|
||||
|
||||
public virtual void OnEnable() {
|
||||
StartCoroutine(Setup());
|
||||
}
|
||||
|
||||
private IEnumerator Setup() {
|
||||
if (cursor != null) yield break;
|
||||
|
||||
yield return null;//wait a frame to let the browser UI get set up
|
||||
|
||||
cursor = GetComponent<Browser>().UIHandler.BrowserCursor;
|
||||
cursor.cursorChange += CursorChange;
|
||||
}
|
||||
|
||||
protected abstract void CursorChange();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30820dfc981b58e48a678a9b0e422fef
|
||||
timeCreated: 1511217990
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,42 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Handles rendering a browser's cursor by letting using t he OS-level mouse cursor
|
||||
/// (and changing it as needed).
|
||||
/// </summary>
|
||||
public class CursorRendererOS : CursorRendererBase {
|
||||
[Tooltip("If true, the mouse cursor should be visible when it's not on a browser.")]
|
||||
public bool cursorNormallyVisible = true;
|
||||
|
||||
protected override void CursorChange() {
|
||||
if (!cursor.HasMouse) {
|
||||
//no browser, do default
|
||||
Cursor.visible = cursorNormallyVisible;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
} else {
|
||||
if (cursor.Texture != null) {
|
||||
//browser, show this cursor
|
||||
Cursor.visible = true;
|
||||
Cursor.SetCursor(
|
||||
cursor.Texture, cursor.Hotspot,
|
||||
#if UNITY_STANDALONE_OSX
|
||||
//Not sure why, but we get really ugly looking "garbled" shadows around the mouse cursor.
|
||||
//I hate latency, but a software cursor is probably less irritating than looking at
|
||||
//that ugly stuff.
|
||||
CursorMode.ForceSoftware
|
||||
#else
|
||||
CursorMode.Auto
|
||||
#endif
|
||||
);
|
||||
} else {
|
||||
//browser, so no cursor
|
||||
Cursor.visible = false;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b27bad8d50722bb4083b964da1d1cae4
|
||||
timeCreated: 1511217990
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,30 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Renders a browser's cursor by rendering something in the center of the screen.
|
||||
/// </summary>
|
||||
public class CursorRendererOverlay : CursorRendererBase {
|
||||
|
||||
[Tooltip("How large should we render the cursor?")]
|
||||
public float scale = .5f;
|
||||
|
||||
protected override void CursorChange() {}
|
||||
|
||||
public void OnGUI() {
|
||||
if (cursor == null) return;
|
||||
|
||||
if (!cursor.HasMouse || !cursor.Texture) return;
|
||||
|
||||
var tex = cursor.Texture;
|
||||
|
||||
var pos = new Rect(Screen.width / 2f, Screen.height / 2f, tex.width * scale, tex.height * scale);
|
||||
pos.x -= cursor.Hotspot.x * scale;
|
||||
pos.y -= cursor.Hotspot.y * scale;
|
||||
|
||||
GUI.DrawTexture(pos, tex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 62aba41510529884eaf9bdaa450168d7
|
||||
timeCreated: 1511217990
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,81 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Renders a browser's cursor by add polygons to the scene offset from browser's face.
|
||||
/// </summary>
|
||||
public class CursorRendererWorldSpace : CursorRendererBase {
|
||||
[Tooltip("How far to keep the cursor from the surface of the browser. Set it as low as you can without causing z-fighting." +
|
||||
" (Note: The default cursor material will always render on top of everything, this is more useful if you use a different material.")]
|
||||
public float zOffset = .005f;
|
||||
[Tooltip("How large should the cursor be when rendered? (meters)")]
|
||||
public float size = .1f;
|
||||
|
||||
private GameObject cursorHolder, cursorImage;
|
||||
private PointerUIBase pointerUI;
|
||||
|
||||
private bool cursorVisible;
|
||||
|
||||
public void Awake() {
|
||||
|
||||
pointerUI = GetComponent<PointerUIBase>();
|
||||
|
||||
cursorHolder = new GameObject("Cursor for " + name);
|
||||
cursorHolder.transform.localScale = new Vector3(size, size, size);
|
||||
|
||||
//If we place it in the parent, the scale can get wonky in some cases.
|
||||
//so don't cursorHolder.transform.parent = transform;
|
||||
|
||||
cursorImage = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
||||
cursorImage.name = "Cursor Image";
|
||||
cursorImage.transform.parent = cursorHolder.transform;
|
||||
var mr = cursorImage.GetComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = Resources.Load<Material>("Browser/CursorDecal");
|
||||
Destroy(cursorImage.GetComponent<Collider>());
|
||||
cursorImage.transform.SetParent(cursorHolder.transform, false);
|
||||
cursorImage.transform.localScale = new Vector3(1, 1, 1);
|
||||
|
||||
|
||||
cursorHolder.SetActive(false);
|
||||
}
|
||||
|
||||
protected override void CursorChange() {
|
||||
if (cursor.HasMouse && cursor.Texture) {
|
||||
cursorVisible = true;
|
||||
|
||||
var cursorRenderer = cursorImage.GetComponent<Renderer>();
|
||||
cursorRenderer.material.mainTexture = cursor.Texture;
|
||||
|
||||
var hs = cursor.Hotspot;
|
||||
cursorRenderer.transform.localPosition = new Vector3(
|
||||
.5f - hs.x / cursor.Texture.width,
|
||||
-.5f + hs.y / cursor.Texture.height,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
cursorVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void LateUpdate() {
|
||||
Vector3 pos;
|
||||
Quaternion rot;
|
||||
pointerUI.GetCurrentHitLocation(out pos, out rot);
|
||||
|
||||
if (float.IsNaN(pos.x)) {
|
||||
cursorHolder.SetActive(false);
|
||||
} else {
|
||||
cursorHolder.SetActive(cursorVisible);
|
||||
|
||||
cursorHolder.transform.position = pos + rot * new Vector3(0, 0, -zOffset);
|
||||
cursorHolder.transform.rotation = rot;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDestroy() {
|
||||
Destroy(cursorHolder.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b0f8a9c4898c5a644a048df52d9472fd
|
||||
timeCreated: 1511217990
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
[Flags]
|
||||
public enum MouseButton {
|
||||
Left = 0x1,
|
||||
Middle = 0x2,
|
||||
Right = 0x4,
|
||||
}
|
||||
|
||||
public class BrowserInputSettings {
|
||||
/**
|
||||
* How fast do we scroll?
|
||||
*/
|
||||
public int scrollSpeed = 120;
|
||||
|
||||
/**
|
||||
* How far can the cursor wander from its position before won't consider another click as a double/triple click?
|
||||
* Value is number of pixels in browser space.
|
||||
*/
|
||||
public float multiclickTolerance = 6;
|
||||
|
||||
/**
|
||||
* How long must we wait between clicks before we don't consider it a double/triple/etc. click?
|
||||
* Measured in seconds.
|
||||
*/
|
||||
public float multiclickSpeed = .7f;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for browser input (and current mouse cursor).
|
||||
* You can create your own implementation to take input however you'd like. To use your implementation,
|
||||
* create a new instance and assign it to browser.UIHandler just after creating the browser.
|
||||
*/
|
||||
public interface IBrowserUI {
|
||||
|
||||
/** Called once per frame by the browser before fetching properties. */
|
||||
void InputUpdate();
|
||||
|
||||
/**
|
||||
* Returns true if the browser will be getting mouse events. Typically this is true when the mouse if over the browser.
|
||||
*
|
||||
* If this is false, the Mouse* properties will be ignored.
|
||||
*/
|
||||
bool MouseHasFocus { get; }
|
||||
|
||||
/**
|
||||
* Current mouse position.
|
||||
*
|
||||
* Returns the current position of the mouse with (0, 0) in the bottom-left corner and (1, 1) in the
|
||||
* top-right corner.
|
||||
*/
|
||||
Vector2 MousePosition { get; }
|
||||
|
||||
/** Bitmask of currently depressed mouse buttons */
|
||||
MouseButton MouseButtons { get; }
|
||||
|
||||
/**
|
||||
* Delta X and Y scroll values since the last time InputUpdate() was called.
|
||||
*
|
||||
* Return 1 for every "click" of the scroll wheel, or smaller numbers for more incremental scrolling.
|
||||
*/
|
||||
Vector2 MouseScroll { get; }
|
||||
|
||||
/**
|
||||
* Returns true when the browser will receive keyboard events.
|
||||
*
|
||||
* In the simplest case, return the same value as MouseHasFocus, but you can track focus yourself if desired.
|
||||
*
|
||||
* If this is false, the Key* properties will be ignored.
|
||||
*/
|
||||
bool KeyboardHasFocus { get; }
|
||||
|
||||
/**
|
||||
* List of key up/down events that have happened since the last InputUpdate() call.
|
||||
*
|
||||
* The returned list is not to be altered or retained.
|
||||
*/
|
||||
List<Event> KeyEvents { get; }
|
||||
|
||||
/**
|
||||
* Returns a BrowserCursor instance. The Browser will update the current cursor to reflect the
|
||||
* mouse's position on the page.
|
||||
*
|
||||
* The IBrowserUI is responsible for changing the actual cursor, be it the mouse cursor or some in-game display.
|
||||
*/
|
||||
BrowserCursor BrowserCursor { get; }
|
||||
|
||||
/**
|
||||
* These settings are used to interpret the input data.
|
||||
*/
|
||||
BrowserInputSettings InputSettings { get; }
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 387f33fa3a8867e4fad235ad24e7fc95
|
||||
timeCreated: 1448050780
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,112 @@
|
|||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
#define ZF_OSX
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
/// <summary>
|
||||
/// Helper class for IBrowserUI implementations for getting/generating keyboard events for sending to an IBrowserUI.
|
||||
/// </summary>
|
||||
public class KeyEvents {
|
||||
|
||||
|
||||
/** Fills up with key events as they happen. */
|
||||
protected List<Event> keyEvents = new List<Event>();
|
||||
|
||||
/** Swaps with keyEvents on InputUpdate and is returned in the main getter. */
|
||||
protected List<Event> keyEventsLast = new List<Event>();
|
||||
|
||||
/// <summary>
|
||||
/// After calling InputUpdate, contains the key events to send to the browser.
|
||||
/// </summary>
|
||||
public List<Event> Events {
|
||||
get { return keyEventsLast; }
|
||||
}
|
||||
|
||||
/** List of keys Unity won't give us events for. So we have to poll. */
|
||||
static readonly KeyCode[] keysToCheck = {
|
||||
#if ZF_OSX
|
||||
//On windows you get GUI events for ctrl, super, alt. On mac...you don't!
|
||||
KeyCode.LeftCommand,
|
||||
KeyCode.RightCommand,
|
||||
KeyCode.LeftControl,
|
||||
KeyCode.RightControl,
|
||||
KeyCode.LeftAlt,
|
||||
KeyCode.RightAlt,
|
||||
//KeyCode.CapsLock, unity refuses to inform us of this, so there's not much we can do
|
||||
#endif
|
||||
//Unity consistently doesn't send events for shift across all platforms.
|
||||
KeyCode.LeftShift,
|
||||
KeyCode.RightShift,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Call once per frame before grabbing
|
||||
/// </summary>
|
||||
public void InputUpdate() {
|
||||
//Note: keyEvents gets filled in OnGUI as things happen. InputUpdate get called just before it's read.
|
||||
//To get the right events to the right place at the right time, swap the "double buffer" of key events.
|
||||
var tmp = keyEvents;
|
||||
keyEvents = keyEventsLast;
|
||||
keyEventsLast = tmp;
|
||||
keyEvents.Clear();
|
||||
|
||||
//Unity doesn't include events for some keys, so fake it by checking each frame.
|
||||
for (int i = 0; i < keysToCheck.Length; i++) {
|
||||
if (Input.GetKeyDown(keysToCheck[i])) {
|
||||
//Prepend down, postpend up. We don't know which happened first, but pressing
|
||||
//modifiers usually precedes other key presses and releasing tends to follow.
|
||||
keyEventsLast.Insert(0, new Event() { type = EventType.KeyDown, keyCode = keysToCheck[i] });
|
||||
} else if (Input.GetKeyUp(keysToCheck[i])) {
|
||||
keyEventsLast.Add(new Event() { type = EventType.KeyUp, keyCode = keysToCheck[i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this with any GUI events you get from Unity that you want to have passed to the browser.
|
||||
/// </summary>
|
||||
/// <param name="ev"></param>
|
||||
public void Feed(Event ev) {
|
||||
if (ev.type != EventType.KeyDown && ev.type != EventType.KeyUp) return;
|
||||
|
||||
// if (ev.character != 0) Debug.Log("ev >>> " + ev.character);
|
||||
// else if (ev.type == EventType.KeyUp) Debug.Log("ev ^^^ " + ev.keyCode);
|
||||
// else if (ev.type == EventType.KeyDown) Debug.Log("ev vvv " + ev.keyCode);
|
||||
|
||||
keyEvents.Add(new Event(ev));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects a key press. Call Release laster to let go.
|
||||
/// If the key you are pressing represents a character this may not type that character. Use Type() instead.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
public void Press(KeyCode key) {
|
||||
keyEvents.Add(new Event {
|
||||
type = EventType.KeyDown, keyCode = key
|
||||
});
|
||||
}
|
||||
|
||||
public void Release(KeyCode key) {
|
||||
keyEvents.Add(new Event {
|
||||
type = EventType.KeyUp, keyCode = key
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types the given text in. THis does not simulate pressing each key and releasing it.
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public void Type(string text) {
|
||||
for (int i = 0; i < text.Length; i++) {
|
||||
//fixme: multibyte chars >16 bits
|
||||
keyEvents.Add(new Event {
|
||||
type = EventType.KeyDown, character = text[i],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 34df8173f4fc89a4a94045d4973f1dac
|
||||
timeCreated: 1495912815
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,10 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cab02f7db18ee9f41abc1d23c3818b09
|
||||
folderAsset: yes
|
||||
timeCreated: 1511210876
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,200 @@
|
|||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
#define ZF_OSX
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* This class will handle input to a browser based on the mouse position and a mesh collider on the browser.
|
||||
* Mouse positions are looked up according to the UVs on the *collider* mesh. Generally, you will want to use
|
||||
* the same or a visually similar (including UVs) mesh for the renderer.
|
||||
*/
|
||||
[Obsolete("Use PointerUIMesh instead.")]
|
||||
public class ClickMeshBrowserUI : MonoBehaviour, IBrowserUI {
|
||||
/**
|
||||
* Creates a new UI handler.
|
||||
* We will attach to {parent}, which must have the mesh we are interacting with.
|
||||
* In most cases, this will also be the same object that has the Browser we will be fed to. (a la browser.UIHandler)
|
||||
*/
|
||||
public static ClickMeshBrowserUI Create(MeshCollider meshCollider) {
|
||||
var ui = meshCollider.gameObject.AddComponent<ClickMeshBrowserUI>();
|
||||
ui.meshCollider = meshCollider;
|
||||
return ui;
|
||||
}
|
||||
|
||||
public void Awake() {
|
||||
BrowserCursor = new BrowserCursor();
|
||||
BrowserCursor.cursorChange += CursorUpdated;
|
||||
|
||||
InputSettings = new BrowserInputSettings();
|
||||
}
|
||||
|
||||
protected MeshCollider meshCollider;
|
||||
|
||||
/**
|
||||
* How far can we reach to touch a browser?
|
||||
*
|
||||
* HideInInspector:
|
||||
* Showing it in the inspector would imply that changing the value would be used, but in most practical cases
|
||||
* with FPSBrowserUI, the value will be overridden by the FPSCursorRenderer.
|
||||
*/
|
||||
[HideInInspector]
|
||||
public float maxDistance = float.PositiveInfinity;
|
||||
|
||||
/** Fills up with key events as they happen. */
|
||||
protected List<Event> keyEvents = new List<Event>();
|
||||
|
||||
/** Swaps with keyEvents on InputUpdate and is returned in the main getter. */
|
||||
protected List<Event> keyEventsLast = new List<Event>();
|
||||
|
||||
/** Returns the user's interacting ray, usually the mouse pointer in some form. */
|
||||
protected virtual Ray LookRay {
|
||||
get { return Camera.main.ScreenPointToRay(Input.mousePosition); }
|
||||
}
|
||||
|
||||
/** List of keys Unity won't give us events for. So we have to poll. */
|
||||
static readonly KeyCode[] keysToCheck = {
|
||||
#if ZF_OSX
|
||||
//On windows you get GUI events for ctrl, super, alt. On mac...you don't!
|
||||
KeyCode.LeftCommand,
|
||||
KeyCode.RightCommand,
|
||||
KeyCode.LeftControl,
|
||||
KeyCode.RightControl,
|
||||
KeyCode.LeftAlt,
|
||||
KeyCode.RightAlt,
|
||||
//KeyCode.CapsLock, unity refuses to inform us of this, so there's not much we can do
|
||||
#endif
|
||||
//Unity consistently doesn't send events for shift across all platforms.
|
||||
KeyCode.LeftShift,
|
||||
KeyCode.RightShift,
|
||||
};
|
||||
|
||||
public virtual void InputUpdate() {
|
||||
//Note: keyEvents gets filled in OnGUI as things happen. InputUpdate get called just before it's read.
|
||||
//To get the right events to the right place at the right time, swap the "double buffer" of key events.
|
||||
var tmp = keyEvents;
|
||||
keyEvents = keyEventsLast;
|
||||
keyEventsLast = tmp;
|
||||
keyEvents.Clear();
|
||||
|
||||
|
||||
//Trace mouse from the main camera
|
||||
var mouseRay = LookRay;
|
||||
RaycastHit hit;
|
||||
Physics.Raycast(mouseRay, out hit, maxDistance);
|
||||
|
||||
if (hit.transform != meshCollider.transform) {
|
||||
//not looking at it.
|
||||
MousePosition = new Vector3(0, 0);
|
||||
MouseButtons = 0;
|
||||
MouseScroll = new Vector2(0, 0);
|
||||
|
||||
MouseHasFocus = false;
|
||||
KeyboardHasFocus = false;
|
||||
|
||||
LookOff();
|
||||
return;
|
||||
}
|
||||
LookOn();
|
||||
MouseHasFocus = true;
|
||||
KeyboardHasFocus = true;
|
||||
|
||||
//convert ray hit to useful mouse position on page
|
||||
var localPoint = hit.textureCoord;
|
||||
MousePosition = localPoint;
|
||||
|
||||
var buttons = (MouseButton)0;
|
||||
if (Input.GetMouseButton(0)) buttons |= MouseButton.Left;
|
||||
if (Input.GetMouseButton(1)) buttons |= MouseButton.Right;
|
||||
if (Input.GetMouseButton(2)) buttons |= MouseButton.Middle;
|
||||
MouseButtons = buttons;
|
||||
|
||||
MouseScroll = Input.mouseScrollDelta;
|
||||
|
||||
|
||||
//Unity doesn't include events for some keys, so fake it by checking each frame.
|
||||
for (int i = 0; i < keysToCheck.Length; i++) {
|
||||
if (Input.GetKeyDown(keysToCheck[i])) {
|
||||
//Prepend down, postpend up. We don't know which happened first, but pressing
|
||||
//modifiers usually precedes other key presses and releasing tends to follow.
|
||||
keyEventsLast.Insert(0, new Event() {type = EventType.KeyDown, keyCode = keysToCheck[i]});
|
||||
} else if (Input.GetKeyUp(keysToCheck[i])) {
|
||||
keyEventsLast.Add(new Event() {type = EventType.KeyUp, keyCode = keysToCheck[i]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnGUI() {
|
||||
var ev = Event.current;
|
||||
if (ev.type != EventType.KeyDown && ev.type != EventType.KeyUp) return;
|
||||
|
||||
// if (ev.character != 0) Debug.Log("ev >>> " + ev.character);
|
||||
// else if (ev.type == EventType.KeyUp) Debug.Log("ev ^^^ " + ev.keyCode);
|
||||
// else if (ev.type == EventType.KeyDown) Debug.Log("ev vvv " + ev.keyCode);
|
||||
|
||||
keyEvents.Add(new Event(ev));
|
||||
}
|
||||
|
||||
protected bool mouseWasOver = false;
|
||||
protected void LookOn() {
|
||||
if (BrowserCursor != null) {
|
||||
CursorUpdated();
|
||||
}
|
||||
mouseWasOver = true;
|
||||
}
|
||||
|
||||
protected void LookOff() {
|
||||
if (BrowserCursor != null && mouseWasOver) {
|
||||
SetCursor(null);
|
||||
}
|
||||
mouseWasOver = false;
|
||||
}
|
||||
|
||||
protected void CursorUpdated() {
|
||||
SetCursor(BrowserCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current mouse cursor.
|
||||
* If the cursor is null we are not looking at this browser.
|
||||
*
|
||||
* This base implementation changes the mouse cursor, but you could change an in-game reticle, etc.
|
||||
*/
|
||||
protected virtual void SetCursor(BrowserCursor newCursor) {
|
||||
//note that HandleKeyInputBrowserCursor can change while we don't have focus.
|
||||
//In such a case, don't do anything
|
||||
if (!MouseHasFocus && newCursor != null) return;
|
||||
|
||||
if (newCursor == null) {
|
||||
Cursor.visible = true;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
} else {
|
||||
if (newCursor.Texture != null) {
|
||||
Cursor.visible = true;
|
||||
Cursor.SetCursor(newCursor.Texture, newCursor.Hotspot, CursorMode.Auto);
|
||||
} else {
|
||||
Cursor.visible = false;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool MouseHasFocus { get; protected set; }
|
||||
public Vector2 MousePosition { get; protected set; }
|
||||
public MouseButton MouseButtons { get; protected set; }
|
||||
public Vector2 MouseScroll { get; protected set; }
|
||||
public bool KeyboardHasFocus { get; protected set; }
|
||||
public List<Event> KeyEvents { get { return keyEventsLast; } }
|
||||
public BrowserCursor BrowserCursor { get; protected set; }
|
||||
public BrowserInputSettings InputSettings { get; protected set; }
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: beafc338c0850714f8831f03b4ba9a67
|
||||
timeCreated: 1450133185
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,62 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
/**
|
||||
* Works like ClickMeshBrowserUI, but assumes you are pointing at buttons with your nose
|
||||
* (a camera or object's transform.forward) instead of with a visible mouse pointer.
|
||||
*
|
||||
* This relies on the given FPSCursorRenderer to render the cursor.
|
||||
*
|
||||
* Unlike MeshColliderBrowserUI, this won't be used by default. If you'd like to use it,
|
||||
* call CursorCrosshair.SetUpBrowserInput.
|
||||
*
|
||||
* As with MeshColliderBrowserUI, pass in the mesh we interact on to {meshCollider}.
|
||||
*
|
||||
* {worldPointer} is the object we are pointing with. Usually you can use Camera.main.transsform.
|
||||
* Its world-space forward direction will be used to get the user's interaction ray.
|
||||
*/
|
||||
[RequireComponent(typeof(Browser))]
|
||||
[RequireComponent(typeof(MeshCollider))]
|
||||
[Obsolete("Use PointerUIMesh instead.")]
|
||||
public class FPSBrowserUI : ClickMeshBrowserUI {
|
||||
protected Transform worldPointer;
|
||||
protected FPSCursorRenderer cursorRenderer;
|
||||
|
||||
public void Start() {
|
||||
FPSCursorRenderer.SetUpBrowserInput(GetComponent<Browser>(), GetComponent<MeshCollider>());
|
||||
}
|
||||
|
||||
public static FPSBrowserUI Create(MeshCollider meshCollider, Transform worldPointer, FPSCursorRenderer cursorRenderer) {
|
||||
var ui = meshCollider.gameObject.GetComponent<FPSBrowserUI>();
|
||||
if (!ui) ui = meshCollider.gameObject.AddComponent<FPSBrowserUI>();
|
||||
ui.meshCollider = meshCollider;
|
||||
ui.worldPointer = worldPointer;
|
||||
ui.cursorRenderer = cursorRenderer;
|
||||
|
||||
return ui;
|
||||
}
|
||||
|
||||
protected override Ray LookRay {
|
||||
get { return new Ray(worldPointer.position, worldPointer.forward); }
|
||||
}
|
||||
|
||||
protected override void SetCursor(BrowserCursor newCursor) {
|
||||
if (newCursor != null && !MouseHasFocus) return;
|
||||
|
||||
cursorRenderer.SetCursor(newCursor, this);
|
||||
}
|
||||
|
||||
public override void InputUpdate() {
|
||||
if (!cursorRenderer.EnableInput) {
|
||||
MouseHasFocus = false;
|
||||
KeyboardHasFocus = false;
|
||||
return;
|
||||
}
|
||||
|
||||
base.InputUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b336f8b13576fc5459369f2a394339d5
|
||||
timeCreated: 1452987345
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Draws a crosshair in the middle of the screen which changes to cursors as you mouseover
|
||||
* things in world-space browsers.
|
||||
*
|
||||
* Often, this will be created automatically. If you want to alter parameters, add this script
|
||||
* to an object (such as the camera) and edit them there.
|
||||
*/
|
||||
[Obsolete("Use PointerUIMesh and CursorRendererOverlay instead.")]
|
||||
public class FPSCursorRenderer : MonoBehaviour {
|
||||
private static FPSCursorRenderer _instance;
|
||||
public static FPSCursorRenderer Instance {
|
||||
get {
|
||||
if (!_instance) {
|
||||
_instance = FindObjectOfType<FPSCursorRenderer>();
|
||||
if (!_instance) {
|
||||
var go = new GameObject("Cursor Crosshair");
|
||||
_instance = go.AddComponent<FPSCursorRenderer>();
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("How large should we render the cursor?")]
|
||||
public float scale = .5f;
|
||||
|
||||
[Tooltip("How far can we reach to push buttons and such?")]
|
||||
public float maxDistance = 7f;
|
||||
|
||||
[Tooltip("What are we using to point at things? Leave as null to use Camera.main")]
|
||||
public Transform pointer;
|
||||
|
||||
/**
|
||||
* Toggle this to enable/disable input for all FPSBrowserUI objects.
|
||||
* This is useful, for example, during plot sequences and pause menus.
|
||||
*/
|
||||
public bool EnableInput { get; set; }
|
||||
|
||||
public static void SetUpBrowserInput(Browser browser, MeshCollider mesh) {
|
||||
var crossHair = Instance;
|
||||
|
||||
var pointer = crossHair.pointer;
|
||||
if (!pointer) pointer = Camera.main.transform;//nb: don't use crossHair.pointer ?? camera, will incorrectly return null
|
||||
var fpsUI = FPSBrowserUI.Create(mesh, pointer, crossHair);
|
||||
fpsUI.maxDistance = crossHair.maxDistance;
|
||||
browser.UIHandler = fpsUI;
|
||||
}
|
||||
|
||||
protected BrowserCursor baseCursor, currentCursor;
|
||||
|
||||
public void Start() {
|
||||
EnableInput = true;
|
||||
baseCursor = new BrowserCursor();
|
||||
baseCursor.SetActiveCursor(BrowserNative.CursorType.Cross);
|
||||
}
|
||||
|
||||
public void OnGUI() {
|
||||
if (!EnableInput) return;
|
||||
|
||||
var cursor = currentCursor ?? baseCursor;
|
||||
var tex = cursor.Texture;
|
||||
|
||||
if (tex == null) return;//hidden cursor
|
||||
|
||||
var pos = new Rect(Screen.width / 2f, Screen.height / 2f, tex.width * scale, tex.height * scale);
|
||||
pos.x -= cursor.Hotspot.x * scale;
|
||||
pos.y -= cursor.Hotspot.y * scale;
|
||||
|
||||
GUI.DrawTexture(pos, tex);
|
||||
}
|
||||
|
||||
public void SetCursor(BrowserCursor newCursor, FPSBrowserUI ui) {
|
||||
currentCursor = newCursor;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 27abec923057368408f62c2ebf6d54b3
|
||||
timeCreated: 1453246003
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,185 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/** Attach this script to a GUI Image to use a browser on it. */
|
||||
[RequireComponent(typeof(RawImage))]
|
||||
[RequireComponent(typeof(Browser))]
|
||||
[Obsolete("Use PointerUIGUI and CursorRendererOS instead.")]
|
||||
public class GUIBrowserUI :
|
||||
MonoBehaviour,
|
||||
IBrowserUI,
|
||||
ISelectHandler, IDeselectHandler,
|
||||
IPointerEnterHandler, IPointerExitHandler,
|
||||
IPointerDownHandler
|
||||
{
|
||||
protected RawImage myImage;
|
||||
protected Browser browser;
|
||||
|
||||
public bool enableInput = true, autoResize = true;
|
||||
|
||||
protected void Awake() {
|
||||
BrowserCursor = new BrowserCursor();
|
||||
InputSettings = new BrowserInputSettings();
|
||||
|
||||
browser = GetComponent<Browser>();
|
||||
myImage = GetComponent<RawImage>();
|
||||
|
||||
browser.afterResize += UpdateTexture;
|
||||
browser.UIHandler = this;
|
||||
BrowserCursor.cursorChange += () => {
|
||||
SetCursor(BrowserCursor);
|
||||
};
|
||||
|
||||
rTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
protected void OnEnable() {
|
||||
if (autoResize) StartCoroutine(WatchResize());
|
||||
}
|
||||
|
||||
/** Automatically resizes the browser to match the size of this object. */
|
||||
private IEnumerator WatchResize() {
|
||||
Rect currentSize = new Rect();
|
||||
|
||||
while (enabled) {
|
||||
var rect = rTransform.rect;
|
||||
|
||||
if (rect.size.x <= 0 || rect.size.y <= 0) rect.size = new Vector2(512, 512);
|
||||
if (rect.size != currentSize.size) {
|
||||
browser.Resize((int)rect.size.x, (int)rect.size.y);
|
||||
currentSize = rect;
|
||||
}
|
||||
|
||||
//yield return new WaitForSeconds(.5f); won't work if you pause the game, which, BTW, is a great time to resize the screen ;-)
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateTexture(Texture2D texture) {
|
||||
myImage.texture = texture;
|
||||
myImage.uvRect = new Rect(0, 0, 1, 1);
|
||||
}
|
||||
|
||||
protected List<Event> keyEvents = new List<Event>();
|
||||
protected List<Event> keyEventsLast = new List<Event>();
|
||||
protected BaseRaycaster raycaster;
|
||||
protected RectTransform rTransform;
|
||||
// protected List<RaycastResult> raycastResults = new List<RaycastResult>();
|
||||
|
||||
public virtual void InputUpdate() {
|
||||
var tmp = keyEvents;
|
||||
keyEvents = keyEventsLast;
|
||||
keyEventsLast = tmp;
|
||||
keyEvents.Clear();
|
||||
|
||||
if (MouseHasFocus) {
|
||||
if (!raycaster) raycaster = GetComponentInParent<BaseRaycaster>();
|
||||
|
||||
// raycastResults.Clear();
|
||||
|
||||
// raycaster.Raycast(data, raycastResults);
|
||||
|
||||
// if (raycastResults.Count != 0) {
|
||||
// Vector2 pos = raycastResults[0].stuff
|
||||
Vector2 pos;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
(RectTransform)transform, Input.mousePosition, raycaster.eventCamera, out pos
|
||||
);
|
||||
pos.x = pos.x / rTransform.rect.width + rTransform.pivot.x;
|
||||
pos.y = pos.y / rTransform.rect.height + rTransform.pivot.y;
|
||||
MousePosition = pos;
|
||||
|
||||
|
||||
var buttons = (MouseButton)0;
|
||||
if (Input.GetMouseButton(0)) buttons |= MouseButton.Left;
|
||||
if (Input.GetMouseButton(1)) buttons |= MouseButton.Right;
|
||||
if (Input.GetMouseButton(2)) buttons |= MouseButton.Middle;
|
||||
MouseButtons = buttons;
|
||||
|
||||
|
||||
|
||||
MouseScroll = Input.mouseScrollDelta;
|
||||
} else {
|
||||
MouseButtons = 0;
|
||||
}
|
||||
|
||||
|
||||
//Unity doesn't include events for some keys, so fake it.
|
||||
if (Input.GetKeyDown(KeyCode.LeftShift) || Input.GetKeyDown(KeyCode.RightShift)) {
|
||||
//Note: doesn't matter if left or right shift, the browser can't tell.
|
||||
//(Prepend event. We don't know what happened first, but pressing shift usually precedes other key presses)
|
||||
keyEventsLast.Insert(0, new Event() { type = EventType.KeyDown, keyCode = KeyCode.LeftShift });
|
||||
}
|
||||
|
||||
if (Input.GetKeyUp(KeyCode.LeftShift) || Input.GetKeyUp(KeyCode.RightShift)) {
|
||||
//Note: doesn't matter if left or right shift, the browser can't tell.
|
||||
keyEventsLast.Add(new Event() { type = EventType.KeyUp, keyCode = KeyCode.LeftShift });
|
||||
}
|
||||
}
|
||||
|
||||
public void OnGUI() {
|
||||
var ev = Event.current;
|
||||
if (ev.type != EventType.KeyDown && ev.type != EventType.KeyUp) return;
|
||||
|
||||
keyEvents.Add(new Event(ev));
|
||||
}
|
||||
|
||||
protected virtual void SetCursor(BrowserCursor newCursor) {
|
||||
if (!_mouseHasFocus && newCursor != null) return;
|
||||
|
||||
if (newCursor == null) {
|
||||
Cursor.visible = true;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
} else {
|
||||
if (newCursor.Texture != null) {
|
||||
Cursor.visible = true;
|
||||
Cursor.SetCursor(newCursor.Texture, newCursor.Hotspot, CursorMode.Auto);
|
||||
} else {
|
||||
Cursor.visible = false;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected bool _mouseHasFocus;
|
||||
public bool MouseHasFocus { get { return _mouseHasFocus && enableInput; } }
|
||||
public Vector2 MousePosition { get; private set; }
|
||||
public MouseButton MouseButtons { get; private set; }
|
||||
public Vector2 MouseScroll { get; private set; }
|
||||
protected bool _keyboardHasFocus;
|
||||
public bool KeyboardHasFocus { get { return _keyboardHasFocus && enableInput; } }
|
||||
public List<Event> KeyEvents { get { return keyEventsLast; } }
|
||||
public BrowserCursor BrowserCursor { get; private set; }
|
||||
public BrowserInputSettings InputSettings { get; private set; }
|
||||
|
||||
public void OnSelect(BaseEventData eventData) {
|
||||
_keyboardHasFocus = true;
|
||||
}
|
||||
|
||||
public void OnDeselect(BaseEventData eventData) {
|
||||
_keyboardHasFocus = false;
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData) {
|
||||
_mouseHasFocus = true;
|
||||
SetCursor(BrowserCursor);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData) {
|
||||
_mouseHasFocus = false;
|
||||
SetCursor(null);
|
||||
}
|
||||
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData) {
|
||||
EventSystem.current.SetSelectedGameObject(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 312e08c685ba1ee4e96f0ff4128d6e49
|
||||
timeCreated: 1453317757
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,434 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if UNITY_2017_2_OR_NEWER
|
||||
using UnityEngine.XR;
|
||||
#endif
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Base class that handles input for mouse/touch/pointer/VR/nose inputs.
|
||||
/// The concept is thus:
|
||||
/// You have an arbitrary number of 3D (FPS player, VR pointer, and world ray) and
|
||||
/// 2D (mouse, touch, and screen position) pointers and you want any of them
|
||||
/// to be able to interact with the Browser.
|
||||
///
|
||||
/// Concrete implementations of this class handle interacting with different rendered mediums
|
||||
/// (such as a mesh or a GUI renderer).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Browser))]
|
||||
public abstract class PointerUIBase : MonoBehaviour, IBrowserUI {
|
||||
public readonly KeyEvents keyEvents = new KeyEvents();
|
||||
|
||||
|
||||
protected Browser browser;
|
||||
protected bool appFocused = true;
|
||||
|
||||
/// <summary>
|
||||
/// Called once per tick. Handlers registered here should look at the pointers they have
|
||||
/// and tell us about them.
|
||||
/// </summary>
|
||||
public event Action onHandlePointers = () => {};
|
||||
|
||||
protected int currentPointerId;
|
||||
|
||||
protected readonly List<PointerState> currentPointers = new List<PointerState>();
|
||||
|
||||
[Tooltip(
|
||||
"When clicking, how far (in browser-space pixels) must the cursor be moved before we send this movement to the browser backend? " +
|
||||
"This helps keep us from unintentionally dragging when we meant to just click, esp. under VR where it's hard to hold the cursor still."
|
||||
)]
|
||||
public float dragMovementThreshold = 0;
|
||||
/// <summary>
|
||||
/// Cursor location when we most recently went from 0 buttons to any buttons down.
|
||||
/// </summary>
|
||||
protected Vector2 mouseDownPosition;
|
||||
/// <summary>
|
||||
/// True when we have the any mouse button down AND we've moved at least dragMovementThreshold after that.
|
||||
/// </summary>
|
||||
protected bool dragging = false;
|
||||
|
||||
#region Pointer Core
|
||||
|
||||
public struct PointerState {
|
||||
/// <summary>
|
||||
/// Unique value for this pointer, to distinguish it by. Must be > 0.
|
||||
/// </summary>
|
||||
public int id;
|
||||
/// <summary>
|
||||
/// Is the pointer a 2d or 3d pointer?
|
||||
/// </summary>
|
||||
public bool is2D;
|
||||
public Vector2 position2D;
|
||||
public Ray position3D;
|
||||
/// <summary>
|
||||
/// Currently depressed "buttons" on this pointer.
|
||||
/// </summary>
|
||||
public MouseButton activeButtons;
|
||||
/// <summary>
|
||||
/// If the pointer can scroll, delta scrolling values since last frame.
|
||||
/// </summary>
|
||||
public Vector2 scrollDelta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the browser gets clicked with any mouse button.
|
||||
/// (More precisely, when we go from having no buttons down to 1+ buttons down.)
|
||||
/// </summary>
|
||||
public event Action onClick = () => {};
|
||||
|
||||
|
||||
public virtual void Awake() {
|
||||
BrowserCursor = new BrowserCursor();
|
||||
BrowserCursor.cursorChange += CursorUpdated;
|
||||
|
||||
InputSettings = new BrowserInputSettings();
|
||||
|
||||
browser = GetComponent<Browser>();
|
||||
browser.UIHandler = this;
|
||||
|
||||
|
||||
onHandlePointers += OnHandlePointers;
|
||||
|
||||
if (disableMouseEmulation) Input.simulateMouseWithTouches = false;
|
||||
}
|
||||
|
||||
public virtual void InputUpdate() {
|
||||
keyEvents.InputUpdate();
|
||||
|
||||
p_currentDown = p_anyDown = p_currentOver = p_anyOver = -1;
|
||||
currentPointers.Clear();
|
||||
|
||||
onHandlePointers();
|
||||
|
||||
CalculatePointer();
|
||||
}
|
||||
|
||||
public void OnApplicationFocus(bool focused) {
|
||||
appFocused = focused;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a 2D screen-space coordinate to browser-space coordinates.
|
||||
/// If the given point doesn't map to the browser, return float.NaN for the position.
|
||||
/// </summary>
|
||||
/// <param name="screenPosition"></param>
|
||||
/// <param name="pointerId"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract Vector2 MapPointerToBrowser(Vector2 screenPosition, int pointerId);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a 3D world-space ray to a browser-space coordinate.
|
||||
/// If the given ray doesn't map to the browser, return float.NaN for the position.
|
||||
/// </summary>
|
||||
/// <param name="worldRay"></param>
|
||||
/// <param name="pointerId"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract Vector2 MapRayToBrowser(Ray worldRay, int pointerId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current position+rotation of the active pointer in world space.
|
||||
/// If there is none, or it doesn't make sense to map to world space, position will
|
||||
/// be NaNs.
|
||||
///
|
||||
/// Coordinates are in world space. The rotation should point up in the direction the browser sees as up.
|
||||
/// Z+ should go "into" the browser surface.
|
||||
/// </summary>
|
||||
/// <param name="pos"></param>
|
||||
/// <param name="rot"></param>
|
||||
public abstract void GetCurrentHitLocation(out Vector3 pos, out Quaternion rot);
|
||||
|
||||
/** Indexes into currentPointers for useful items this frame. -1 if N/A. */
|
||||
protected int p_currentDown, p_anyDown, p_currentOver, p_anyOver;
|
||||
|
||||
/// <summary>
|
||||
/// Feeds the state of the given pointer into the handler.
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
public virtual void FeedPointerState(PointerState state) {
|
||||
if (state.is2D) state.position2D = MapPointerToBrowser(state.position2D, state.id);
|
||||
else {
|
||||
Debug.DrawRay(state.position3D.origin, state.position3D.direction * (Mathf.Min(500, maxDistance)), Color.cyan);
|
||||
state.position2D = MapRayToBrowser(state.position3D, state.id);
|
||||
//Debug.Log("Pointer " + state.id + " at " + state.position3D.origin + " pointing " + state.position3D.direction + " maps to " + state.position2D);
|
||||
}
|
||||
|
||||
if (float.IsNaN(state.position2D.x)) return;
|
||||
|
||||
if (state.id == currentPointerId) {
|
||||
p_currentOver = currentPointers.Count;
|
||||
|
||||
if (state.activeButtons != 0) p_currentDown = currentPointers.Count;
|
||||
} else {
|
||||
p_anyOver = currentPointers.Count;
|
||||
|
||||
if (state.activeButtons != 0) p_anyDown = currentPointers.Count;
|
||||
}
|
||||
|
||||
currentPointers.Add(state);
|
||||
}
|
||||
|
||||
protected virtual void CalculatePointer() {
|
||||
// if (!appFocused) {
|
||||
// MouseIsOff();
|
||||
// return;
|
||||
// }
|
||||
|
||||
/*
|
||||
* The position/priority we feed to the browser is determined in this order:
|
||||
* - Pointer we used earlier with a button down
|
||||
* - Pointer with a button down
|
||||
* - Pointer we used earlier
|
||||
* - Any pointer that is over the browser
|
||||
* Pointers that aren't over the browser are ignored.
|
||||
* If multiple pointers meet the criteria we may pick any that qualify.
|
||||
*/
|
||||
|
||||
PointerState stateToUse;
|
||||
if (p_currentDown >= 0) {
|
||||
//last frame's pointer with a button down
|
||||
stateToUse = currentPointers[p_currentDown];
|
||||
} else if (p_anyDown >= 0) {
|
||||
//mouse button count became > 0 this frame
|
||||
stateToUse = currentPointers[p_anyDown];
|
||||
} else if (p_currentOver >= 0) {
|
||||
//just hovering (use the pointer from last frame)
|
||||
stateToUse = currentPointers[p_currentOver];
|
||||
} else if (p_anyOver >= 0) {
|
||||
//just hovering (use any pointer over us)
|
||||
stateToUse = currentPointers[p_anyOver];
|
||||
} else {
|
||||
//no pointers over us
|
||||
MouseIsOff();
|
||||
return;
|
||||
}
|
||||
|
||||
MouseIsOver();
|
||||
|
||||
if (MouseButtons == 0 && stateToUse.activeButtons != 0) {
|
||||
//no buttons -> 1+ buttons
|
||||
onClick();
|
||||
//start drag prevention
|
||||
dragging = false;
|
||||
mouseDownPosition = stateToUse.position2D;
|
||||
}
|
||||
|
||||
if (float.IsNaN(stateToUse.position2D.x)) Debug.LogError("Using an invalid pointer");// "shouldn't happen"
|
||||
|
||||
if (stateToUse.activeButtons != 0 || MouseButtons != 0) {
|
||||
//Button(s) held or being released, do some extra logic to prevent unintentional dragging during clicks.
|
||||
|
||||
if (!dragging && stateToUse.activeButtons != 0) {//only check distance if buttons(s) held and not already dragging
|
||||
//Check to see if we passed the drag threshold.
|
||||
var size = browser.Size;
|
||||
var distance = Vector2.Distance(
|
||||
Vector2.Scale(stateToUse.position2D, size),//convert from [0, 1] to pixels
|
||||
Vector2.Scale(mouseDownPosition, size)//convert from [0, 1] to pixels
|
||||
);
|
||||
|
||||
if (distance > dragMovementThreshold) {
|
||||
dragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (dragging) MousePosition = stateToUse.position2D;
|
||||
else MousePosition = mouseDownPosition;
|
||||
} else {
|
||||
//no buttons held (or being release), no need to fiddle with the position
|
||||
MousePosition = stateToUse.position2D;
|
||||
}
|
||||
|
||||
MouseButtons = stateToUse.activeButtons;
|
||||
MouseScroll = stateToUse.scrollDelta;
|
||||
currentPointerId = stateToUse.id;
|
||||
}
|
||||
|
||||
|
||||
public void OnGUI() {
|
||||
keyEvents.Feed(Event.current);
|
||||
}
|
||||
|
||||
protected bool mouseWasOver = false;
|
||||
protected void MouseIsOver() {
|
||||
MouseHasFocus = true;
|
||||
KeyboardHasFocus = true;
|
||||
|
||||
if (BrowserCursor != null) {
|
||||
CursorUpdated();
|
||||
}
|
||||
|
||||
mouseWasOver = true;
|
||||
}
|
||||
|
||||
protected void MouseIsOff() {
|
||||
// if (BrowserCursor != null && mouseWasOver) {
|
||||
// SetCursor(null);
|
||||
// }
|
||||
mouseWasOver = false;
|
||||
|
||||
MouseHasFocus = false;
|
||||
if (focusForceCount <= 0) KeyboardHasFocus = false;
|
||||
MouseButtons = 0;
|
||||
MouseScroll = Vector2.zero;
|
||||
|
||||
currentPointerId = 0;
|
||||
}
|
||||
|
||||
protected void CursorUpdated() {
|
||||
// SetCursor(BrowserCursor);
|
||||
}
|
||||
|
||||
private int focusForceCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Sets a flag to keep the keyboard focus on this browser, even if it has no pointers.
|
||||
/// Useful for focusing it to type things in via external keyboard.
|
||||
///
|
||||
/// Call again with force = false to return to the default behavior. (You must call force
|
||||
/// on and force off an equal number of times to revert to the default behavior.)
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="force"></param>
|
||||
public void ForceKeyboardHasFocus(bool force) {
|
||||
if (force) ++focusForceCount;
|
||||
else --focusForceCount;
|
||||
if (focusForceCount == 1) KeyboardHasFocus = true;
|
||||
else if (focusForceCount == 0) KeyboardHasFocus = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input Handlers
|
||||
|
||||
[Tooltip("Camera to use to interpret 2D inputs and to originate FPS rays from. Defaults to Camera.main.")]
|
||||
public Camera viewCamera;
|
||||
|
||||
public bool enableMouseInput = true;
|
||||
public bool enableTouchInput = true;
|
||||
public bool enableFPSInput = false;
|
||||
public bool enableVRInput = false;
|
||||
|
||||
[Tooltip("(For ray-based interaction) How close must you be to the browser to be able to interact with it?")]
|
||||
public float maxDistance = float.PositiveInfinity;
|
||||
|
||||
[Space(5)]
|
||||
[Tooltip("Disable Input.simulateMouseWithTouches globally. This will prevent touches from appearing as mouse events.")]
|
||||
public bool disableMouseEmulation = false;
|
||||
|
||||
protected virtual void OnHandlePointers() {
|
||||
if (enableFPSInput) FeedFPS();
|
||||
|
||||
//special case to avoid duplicate pointer from the first touch (ignore mouse)
|
||||
if (enableMouseInput && enableTouchInput && Input.simulateMouseWithTouches && Input.touchCount > 0) {
|
||||
FeedTouchPointers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableMouseInput) FeedMousePointer();
|
||||
if (enableTouchInput) FeedTouchPointers();
|
||||
#if UNITY_2017_2_OR_NEWER
|
||||
if (enableVRInput) FeedVRPointers();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls FeedPointerState with all the items in Input.touches.
|
||||
/// (Does not happen automatically, call when desired.)
|
||||
/// </summary>
|
||||
protected virtual void FeedTouchPointers() {
|
||||
for (int i = 0; i < Input.touchCount; i++) {
|
||||
var touch = Input.GetTouch(i);
|
||||
|
||||
FeedPointerState(new PointerState {
|
||||
id = 10 + touch.fingerId,
|
||||
is2D = true,
|
||||
position2D = touch.position,
|
||||
activeButtons = (touch.phase == TouchPhase.Began || touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary) ? MouseButton.Left : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls FeedPointerState with the current mouse state.
|
||||
/// (Does not happen automatically, call when desired.)
|
||||
/// </summary>
|
||||
protected virtual void FeedMousePointer() {
|
||||
var buttons = (MouseButton)0;
|
||||
if (Input.GetMouseButton(0)) buttons |= MouseButton.Left;
|
||||
if (Input.GetMouseButton(1)) buttons |= MouseButton.Right;
|
||||
if (Input.GetMouseButton(2)) buttons |= MouseButton.Middle;
|
||||
|
||||
FeedPointerState(new PointerState {
|
||||
id = 1,
|
||||
is2D = true,
|
||||
position2D = Input.mousePosition,
|
||||
activeButtons = buttons,
|
||||
scrollDelta = Input.mouseScrollDelta,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected virtual void FeedFPS() {
|
||||
var buttons =
|
||||
(Input.GetButton("Fire1") ? MouseButton.Left : 0) |
|
||||
(Input.GetButton("Fire2") ? MouseButton.Right : 0) |
|
||||
(Input.GetButton("Fire3") ? MouseButton.Middle : 0)
|
||||
;
|
||||
|
||||
var camera = viewCamera ? viewCamera : Camera.main;
|
||||
|
||||
var scrollDelta = Input.mouseScrollDelta;
|
||||
|
||||
//Don't double-count scrolling if we are processing the mouse too.
|
||||
if (enableMouseInput) scrollDelta = Vector2.zero;
|
||||
|
||||
FeedPointerState(new PointerState {
|
||||
id = 2,
|
||||
is2D = false,
|
||||
position3D = new Ray(camera.transform.position, camera.transform.forward),
|
||||
activeButtons = buttons,
|
||||
scrollDelta = scrollDelta,
|
||||
});
|
||||
}
|
||||
|
||||
#if UNITY_2017_2_OR_NEWER
|
||||
protected VRBrowserHand[] vrHands = null;
|
||||
protected virtual void FeedVRPointers() {
|
||||
if (vrHands == null) {
|
||||
vrHands = FindObjectsOfType<VRBrowserHand>();
|
||||
if (vrHands.Length == 0 && XRSettings.enabled) {
|
||||
Debug.LogWarning("VR input is enabled, but no VRBrowserHands were found in the scene", this);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < vrHands.Length; i++) {
|
||||
if (!vrHands[i].Tracked) continue;
|
||||
|
||||
FeedPointerState(new PointerState {
|
||||
id = 100 + i,
|
||||
is2D = false,
|
||||
position3D = new Ray(vrHands[i].transform.position, vrHands[i].transform.forward),
|
||||
activeButtons = vrHands[i].DepressedButtons,
|
||||
scrollDelta = vrHands[i].ScrollDelta,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
public virtual bool MouseHasFocus { get; protected set; }
|
||||
public virtual Vector2 MousePosition { get; protected set; }
|
||||
public virtual MouseButton MouseButtons { get; protected set; }
|
||||
public virtual Vector2 MouseScroll { get; protected set; }
|
||||
public virtual bool KeyboardHasFocus { get; protected set; }
|
||||
public virtual List<Event> KeyEvents { get { return keyEvents.Events; } }
|
||||
public virtual BrowserCursor BrowserCursor { get; protected set; }
|
||||
public virtual BrowserInputSettings InputSettings { get; protected set; }
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7bd56d627d06bc64382a847aa240ae1d
|
||||
timeCreated: 1495912815
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,126 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/** Attach this script to a GUI Image to use a browser on it. */
|
||||
[RequireComponent(typeof(RawImage))]
|
||||
public class PointerUIGUI :
|
||||
PointerUIBase,
|
||||
IBrowserUI,
|
||||
ISelectHandler, IDeselectHandler,
|
||||
IPointerEnterHandler, IPointerExitHandler,
|
||||
IPointerDownHandler
|
||||
{
|
||||
protected RawImage myImage;
|
||||
|
||||
public bool enableInput = true;
|
||||
public bool automaticResize = true;
|
||||
|
||||
public override void Awake() {
|
||||
base.Awake();
|
||||
myImage = GetComponent<RawImage>();
|
||||
|
||||
browser.afterResize += UpdateTexture;
|
||||
// BrowserCursor.cursorChange += () => {
|
||||
// SetCursor(BrowserCursor);
|
||||
// };
|
||||
|
||||
rTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
protected void OnEnable() {
|
||||
if (automaticResize) StartCoroutine(WatchResize());
|
||||
}
|
||||
|
||||
/** Automatically resizes the browser to match the size of this object. */
|
||||
private IEnumerator WatchResize() {
|
||||
Rect currentSize = new Rect();
|
||||
|
||||
while (enabled) {
|
||||
var rect = rTransform.rect;
|
||||
|
||||
if (rect.size.x <= 0 || rect.size.y <= 0) rect.size = new Vector2(512, 512);
|
||||
if (rect.size != currentSize.size) {
|
||||
browser.Resize((int)rect.size.x, (int)rect.size.y);
|
||||
currentSize = rect;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateTexture(Texture2D texture) {
|
||||
myImage.texture = texture;
|
||||
myImage.uvRect = new Rect(0, 0, 1, 1);
|
||||
}
|
||||
|
||||
protected BaseRaycaster raycaster;
|
||||
protected RectTransform rTransform;
|
||||
// protected List<RaycastResult> raycastResults = new List<RaycastResult>();
|
||||
|
||||
protected override Vector2 MapPointerToBrowser(Vector2 screenPosition, int pointerId) {
|
||||
if (!raycaster) raycaster = GetComponentInParent<BaseRaycaster>();
|
||||
|
||||
Vector2 pos;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
(RectTransform)transform, screenPosition, raycaster.eventCamera, out pos
|
||||
);
|
||||
pos.x = pos.x / rTransform.rect.width + rTransform.pivot.x;
|
||||
pos.y = pos.y / rTransform.rect.height + rTransform.pivot.y;
|
||||
return pos;
|
||||
}
|
||||
|
||||
protected override Vector2 MapRayToBrowser(Ray worldRay, int pointerId) {
|
||||
var evs = EventSystem.current;
|
||||
if (!evs) return new Vector2(float.NaN, float.NaN);
|
||||
|
||||
//todo: world-space GUI
|
||||
return new Vector2(float.NaN, float.NaN);
|
||||
}
|
||||
|
||||
public override void GetCurrentHitLocation(out Vector3 pos, out Quaternion rot) {
|
||||
//todo: world space GUI
|
||||
pos = new Vector3(float.NaN, float.NaN, float.NaN);
|
||||
rot = Quaternion.identity;
|
||||
}
|
||||
|
||||
|
||||
protected bool _mouseHasFocus;
|
||||
public override bool MouseHasFocus {
|
||||
get { return _mouseHasFocus && enableInput; }
|
||||
protected set { _mouseHasFocus = value; }
|
||||
}
|
||||
protected bool _keyboardHasFocus;
|
||||
public override bool KeyboardHasFocus { get { return _keyboardHasFocus && enableInput; } }
|
||||
|
||||
public void OnSelect(BaseEventData eventData) {
|
||||
_keyboardHasFocus = true;
|
||||
Input.imeCompositionMode = IMECompositionMode.Off;//CEF will handle the IME
|
||||
}
|
||||
|
||||
public void OnDeselect(BaseEventData eventData) {
|
||||
_keyboardHasFocus = false;
|
||||
Input.imeCompositionMode = IMECompositionMode.Auto;
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData) {
|
||||
_mouseHasFocus = true;
|
||||
// SetCursor(BrowserCursor);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData) {
|
||||
_mouseHasFocus = false;
|
||||
// SetCursor(null);
|
||||
}
|
||||
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData) {
|
||||
EventSystem.current.SetSelectedGameObject(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9f0449828438f1c4eb0712205cc11bb7
|
||||
timeCreated: 1495924470
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// A BrowserUI that tracks pointer interaction through a camera to a mesh of some sort.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(MeshCollider))]
|
||||
public class PointerUIMesh : PointerUIBase {
|
||||
protected MeshCollider meshCollider;
|
||||
|
||||
protected Dictionary<int, RaycastHit> rayHits = new Dictionary<int, RaycastHit>();
|
||||
|
||||
[Tooltip("Which layers should UI rays collide with (and be able to hit)?")]
|
||||
public LayerMask layerMask = -1;
|
||||
|
||||
public override void Awake() {
|
||||
base.Awake();
|
||||
meshCollider = GetComponent<MeshCollider>();
|
||||
}
|
||||
|
||||
protected override Vector2 MapPointerToBrowser(Vector2 screenPosition, int pointerId) {
|
||||
var camera = viewCamera ? viewCamera : Camera.main;
|
||||
return MapRayToBrowser(camera.ScreenPointToRay(screenPosition), pointerId);
|
||||
}
|
||||
|
||||
protected override Vector2 MapRayToBrowser(Ray worldRay, int pointerId) {
|
||||
RaycastHit hit;
|
||||
var rayHit = Physics.Raycast(worldRay, out hit, maxDistance, layerMask);
|
||||
|
||||
//store hit data for GetCurrentHitLocation
|
||||
rayHits[pointerId] = hit;
|
||||
|
||||
if (!rayHit || hit.collider.transform != meshCollider.transform) {
|
||||
//not aimed at it
|
||||
return new Vector3(float.NaN, float.NaN);
|
||||
} else {
|
||||
return hit.textureCoord;
|
||||
}
|
||||
}
|
||||
|
||||
public override void GetCurrentHitLocation(out Vector3 pos, out Quaternion rot) {
|
||||
if (currentPointerId == 0) {
|
||||
//no pointer
|
||||
pos = new Vector3(float.NaN, float.NaN, float.NaN);
|
||||
rot = Quaternion.identity;
|
||||
return;
|
||||
}
|
||||
|
||||
var hitInfo = rayHits[currentPointerId];
|
||||
|
||||
//We need to know which way is up, so the cursor has the correct "up".
|
||||
//There's a couple ways to do this:
|
||||
//1. Use the barycentric coordinates and some math to figure out what direction the collider's
|
||||
// v (from the uv) is getting bigger/smaller, then do some math to find out what direction
|
||||
// that is in world space.
|
||||
//2. Just use the collider's local orientation's up. This isn't accurate on highly
|
||||
// distorted meshes, but is much simpler to calculate.
|
||||
//For now, we use method 2.
|
||||
var up = hitInfo.collider.transform.up;
|
||||
|
||||
pos = hitInfo.point;
|
||||
rot = Quaternion.LookRotation(-hitInfo.normal, up);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 70425c8c18e6a674da5c39ca0c09003c
|
||||
timeCreated: 1495915500
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,100 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using NativeCookie = ZenFulcrum.EmbeddedBrowser.BrowserNative.NativeCookie;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
public class Cookie {
|
||||
|
||||
public static void Init() {
|
||||
//Empty function on this class to call so we can get the cctor to call on the correct thread.
|
||||
//(Regex construction tends to crash if it tries to run from certain threads.)
|
||||
}
|
||||
|
||||
|
||||
private CookieManager cookies;
|
||||
|
||||
private NativeCookie original;
|
||||
|
||||
public string name = "", value = "", domain = "", path = "";
|
||||
/** Creation/access time of the cookie. Mostly untested/unsupported at present. */
|
||||
public DateTime creation, lastAccess;
|
||||
/** Null for normal cookies, a time for cookies that expire. Mostly untested/unsupported at present. */
|
||||
public DateTime? expires;
|
||||
public bool secure, httpOnly;
|
||||
|
||||
public Cookie(CookieManager cookies) {
|
||||
this.cookies = cookies;
|
||||
}
|
||||
|
||||
internal Cookie(CookieManager cookies, NativeCookie cookie) {
|
||||
this.cookies = cookies;
|
||||
original = cookie;
|
||||
Copy(original, this);
|
||||
}
|
||||
|
||||
/** Deletes this cookie from the browser. */
|
||||
public void Delete() {
|
||||
if (original == null) return;
|
||||
|
||||
BrowserNative.zfb_editCookie(cookies.browser.browserId, original, BrowserNative.CookieAction.Delete);
|
||||
original = null;
|
||||
}
|
||||
|
||||
/** Updates any changes to this cookie in the browser, creating the cookie if it's new. */
|
||||
public void Update() {
|
||||
if (original != null) Delete();
|
||||
|
||||
original = new NativeCookie();
|
||||
Copy(this, original);
|
||||
|
||||
BrowserNative.zfb_editCookie(cookies.browser.browserId, original, BrowserNative.CookieAction.Create);
|
||||
}
|
||||
|
||||
static readonly Regex dateRegex = new Regex(@"(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}).(\d{3})");
|
||||
|
||||
public static void Copy(NativeCookie src, Cookie dest) {
|
||||
dest.name = src.name;
|
||||
dest.value = src.value;
|
||||
dest.domain = src.domain;
|
||||
dest.path = src.path;
|
||||
|
||||
Func<string, DateTime> convert = s => {
|
||||
var m = dateRegex.Match(s);
|
||||
|
||||
return new DateTime(
|
||||
int.Parse(m.Groups[1].ToString()),
|
||||
int.Parse(m.Groups[2].ToString()),
|
||||
int.Parse(m.Groups[3].ToString()),
|
||||
int.Parse(m.Groups[4].ToString()),
|
||||
int.Parse(m.Groups[5].ToString()),
|
||||
int.Parse(m.Groups[6].ToString()),
|
||||
int.Parse(m.Groups[7].ToString())
|
||||
);
|
||||
};
|
||||
|
||||
dest.creation = convert(src.creation);
|
||||
dest.expires = src.expires == null ? (DateTime?)null : convert(src.expires);
|
||||
dest.lastAccess = convert(src.lastAccess);
|
||||
|
||||
dest.secure = src.secure != 0;
|
||||
dest.httpOnly = src.httpOnly != 0;
|
||||
}
|
||||
|
||||
public static void Copy(Cookie src, NativeCookie dest) {
|
||||
dest.name = src.name;
|
||||
dest.value = src.value;
|
||||
dest.domain = src.domain;
|
||||
dest.path = src.path;
|
||||
|
||||
Func<DateTime, string> convert = s => s.ToString("yyyy-MM-dd hh:mm:ss.fff");
|
||||
|
||||
dest.creation = convert(src.creation);
|
||||
dest.expires = src.expires == null ? null : convert(src.expires.Value);
|
||||
dest.lastAccess = convert(src.lastAccess);
|
||||
|
||||
dest.secure = src.secure ? (byte)1 : (byte)0;
|
||||
dest.httpOnly = src.httpOnly ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f7a2a645481f96e4682c86cc9b22dff9
|
||||
timeCreated: 1478892646
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,94 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using AOT;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
public class CookieManager {
|
||||
internal readonly Browser browser;
|
||||
|
||||
|
||||
public CookieManager(Browser browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
private class CookieFetch {
|
||||
public BrowserNative.GetCookieFunc nativeCB;
|
||||
public Promise<List<Cookie>> promise;
|
||||
public CookieManager manager;
|
||||
public List<Cookie> result;
|
||||
}
|
||||
|
||||
private static CookieFetch currentFetch;
|
||||
|
||||
/**
|
||||
* Returns a list of all cookies in the browser across all domains.
|
||||
*
|
||||
* Note that cookies are shared between browser instances.
|
||||
*
|
||||
* If the browser is not ready yet (browser.IsReady or WhenReady()) this will return an empty list.
|
||||
*
|
||||
* This method is not reentrant! You must wait for the returned promise to resolve before calling it again,
|
||||
* even on a differnet object.
|
||||
*/
|
||||
public IPromise<List<Cookie>> GetCookies() {
|
||||
if (currentFetch != null) {
|
||||
//This method Wait for the previous promise to resolve, then make your call.
|
||||
//If this limitation actually affects you, let me know.
|
||||
throw new InvalidOperationException("GetCookies is not reentrant");
|
||||
}
|
||||
|
||||
Cookie.Init();
|
||||
|
||||
var result = new List<Cookie>();
|
||||
if (!browser.IsReady || !browser.enabled) return Promise<List<Cookie>>.Resolved(result);
|
||||
var promise = new Promise<List<Cookie>>();
|
||||
|
||||
BrowserNative.GetCookieFunc cookieFunc = CB_GetCookieFunc;
|
||||
BrowserNative.zfb_getCookies(browser.browserId, cookieFunc);
|
||||
|
||||
currentFetch = new CookieFetch {
|
||||
promise = promise,
|
||||
nativeCB = cookieFunc,
|
||||
manager = this,
|
||||
result = result,
|
||||
};
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(BrowserNative.GetCookieFunc))]
|
||||
private static void CB_GetCookieFunc(BrowserNative.NativeCookie cookie) {
|
||||
try {
|
||||
if (cookie == null) {
|
||||
var result = currentFetch.result;
|
||||
var promise = currentFetch.promise;
|
||||
currentFetch.manager.browser.RunOnMainThread(() => promise.Resolve(result));
|
||||
currentFetch = null;
|
||||
return;
|
||||
}
|
||||
|
||||
currentFetch.result.Add(new Cookie(currentFetch.manager, cookie));
|
||||
|
||||
} catch (Exception ex) {
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all cookies in the browser.
|
||||
*/
|
||||
public void ClearAll() {
|
||||
if (browser.DeferUnready(ClearAll)) return;
|
||||
|
||||
BrowserNative.zfb_clearCookies(browser.browserId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 536726be6d65c4f4fa53116569aa4be5
|
||||
timeCreated: 1478910267
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Helper for browser dialog boxes, like alert(). You don't need to use this directly, it will
|
||||
* automatically be added where it's needed.
|
||||
*/
|
||||
[RequireComponent(typeof(Browser))]
|
||||
public class DialogHandler : MonoBehaviour {
|
||||
protected static string dialogPage;
|
||||
|
||||
public delegate void DialogCallback(bool affirm, string text1, string text2);
|
||||
public delegate void MenuCallback(int commandId);
|
||||
|
||||
public static DialogHandler Create(Browser parent, DialogCallback dialogCallback, MenuCallback contextCallback) {
|
||||
if (dialogPage == null) {
|
||||
dialogPage = Resources.Load<TextAsset>("Browser/Dialogs").text;
|
||||
}
|
||||
|
||||
|
||||
var go = new GameObject("Browser Dialog for " + parent.name);
|
||||
var handler = go.AddComponent<DialogHandler>();
|
||||
|
||||
handler.parentBrowser = parent;
|
||||
handler.dialogCallback = dialogCallback;
|
||||
|
||||
|
||||
var db = handler.dialogBrowser = handler.GetComponent<Browser>();
|
||||
|
||||
db.UIHandler = parent.UIHandler;
|
||||
db.EnableRendering = false;
|
||||
db.EnableInput = false;
|
||||
db.allowContextMenuOn = BrowserNative.ContextMenuOrigin.Editable;
|
||||
//Use the parent texture. Except, we don't actually use it. So
|
||||
//mostly we just mimic the size and don't consume more texture memory.
|
||||
db.Resize(parent.Texture);
|
||||
db.LoadHTML(dialogPage, "zfb://dialog");
|
||||
db.UIHandler = parent.UIHandler;
|
||||
|
||||
db.RegisterFunction("reportDialogResult", args => {
|
||||
dialogCallback(args[0], args[1], args[2]);
|
||||
handler.Hide();
|
||||
});
|
||||
db.RegisterFunction("reportContextMenuResult", args => {
|
||||
contextCallback(args[0]);
|
||||
handler.Hide();
|
||||
});
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
protected Browser parentBrowser;
|
||||
protected Browser dialogBrowser;
|
||||
protected DialogCallback dialogCallback;
|
||||
protected MenuCallback contextCallback;
|
||||
|
||||
public void HandleDialog(BrowserNative.DialogType type, string text, string promptDefault = null) {
|
||||
if (type == BrowserNative.DialogType.DLT_HIDE) {
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
Show();
|
||||
|
||||
//Debug.Log("HandleDialog " + type + " text " + text + " prompt " + promptDefault);
|
||||
|
||||
switch (type) {
|
||||
case BrowserNative.DialogType.DLT_ALERT:
|
||||
dialogBrowser.CallFunction("showAlert", text);
|
||||
break;
|
||||
case BrowserNative.DialogType.DLT_CONFIRM:
|
||||
dialogBrowser.CallFunction("showConfirm", text);
|
||||
break;
|
||||
case BrowserNative.DialogType.DLT_PROMPT:
|
||||
dialogBrowser.CallFunction("showPrompt", text, promptDefault);
|
||||
break;
|
||||
case BrowserNative.DialogType.DLT_PAGE_UNLOAD:
|
||||
dialogBrowser.CallFunction("showConfirmNav", text);
|
||||
break;
|
||||
case BrowserNative.DialogType.DLT_PAGE_RELOAD:
|
||||
dialogBrowser.CallFunction("showConfirmReload", text);
|
||||
break;
|
||||
case BrowserNative.DialogType.DLT_GET_AUTH:
|
||||
dialogBrowser.CallFunction("showAuthPrompt", text);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException("type", type, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Show() {
|
||||
parentBrowser.SetOverlay(dialogBrowser);
|
||||
parentBrowser.EnableInput = false;
|
||||
dialogBrowser.EnableInput = true;
|
||||
dialogBrowser.UpdateCursor();
|
||||
}
|
||||
|
||||
public void Hide() {
|
||||
parentBrowser.SetOverlay(null);
|
||||
parentBrowser.EnableInput = true;
|
||||
dialogBrowser.EnableInput = false;
|
||||
parentBrowser.UpdateCursor();
|
||||
if (dialogBrowser.IsLoaded) dialogBrowser.CallFunction("reset");
|
||||
}
|
||||
|
||||
public void HandleContextMenu(string menuJSON, int x, int y) {
|
||||
if (menuJSON == null) {
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
Show();
|
||||
|
||||
dialogBrowser.CallFunction("showContextMenu", menuJSON, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8c5bf0b11246b1f42a262f8a64026d33
|
||||
timeCreated: 1449001312
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,226 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Helper class for tracking and managing downloads.
|
||||
* You can manage and handle downloads without this, but you may find it useful for dealing with the more
|
||||
* common file downloading use cases.
|
||||
*
|
||||
* Usage: create one and call manager.ManageDownloads(browser) on a browser you want it to handle.
|
||||
* or throw one in the scene and set "manageAllBrowsers" to true (before loading the scene) for it
|
||||
* to automatically hook into all browsers that start in the scene.
|
||||
*/
|
||||
public class DownloadManager : MonoBehaviour {
|
||||
|
||||
[Tooltip("If true, this will find all the browser in the scene at startup and take control of their downloads.")]
|
||||
public bool manageAllBrowsers = false;
|
||||
|
||||
[Tooltip("If true, a \"Save as\" style dialog will be given for all downloads.")]
|
||||
public bool promptForFileNames;
|
||||
|
||||
[Tooltip("Where to save files. If null or blank, defaults to the user's downloads directory.")]
|
||||
public string saveFolder;
|
||||
|
||||
[Tooltip("If given this text element will be updated with download status info.")]
|
||||
public Text statusBar;
|
||||
|
||||
public class Download {
|
||||
public Browser browser;
|
||||
public int downloadId;
|
||||
public string name;
|
||||
public string path;
|
||||
public int speed;
|
||||
public int percent;
|
||||
public string status;
|
||||
}
|
||||
|
||||
public List<Download> downloads = new List<Download>();
|
||||
|
||||
public void Awake() {
|
||||
if (manageAllBrowsers) {
|
||||
foreach (var browser in FindObjectsOfType<Browser>()) {
|
||||
ManageDownloads(browser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ManageDownloads(Browser browser) {
|
||||
browser.onDownloadStarted = (id, info) => {
|
||||
HandleDownloadStarted(browser, id, info);
|
||||
};
|
||||
browser.onDownloadStatus += (id, info) => {
|
||||
HandleDownloadStatus(browser, id, info);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private void HandleDownloadStarted(Browser browser, int downloadId, JSONNode info) {
|
||||
//Debug.Log("Download requested: " + info.AsJSON);
|
||||
|
||||
var download = new Download {
|
||||
browser = browser,
|
||||
downloadId = downloadId,
|
||||
name = info["suggestedName"],
|
||||
};
|
||||
|
||||
|
||||
if (promptForFileNames) {
|
||||
browser.DownloadCommand(downloadId, BrowserNative.DownloadAction.Begin, null);
|
||||
} else {
|
||||
DirectoryInfo downloadFolder;
|
||||
if (string.IsNullOrEmpty(saveFolder)) {
|
||||
downloadFolder = new DirectoryInfo(GetUserDownloadFolder());
|
||||
} else {
|
||||
downloadFolder = new DirectoryInfo(saveFolder);
|
||||
if (!downloadFolder.Exists) downloadFolder.Create();
|
||||
}
|
||||
|
||||
var filePath = downloadFolder.FullName + "/" + new FileInfo(info["suggestedName"]).Name;
|
||||
while (File.Exists(filePath)) {
|
||||
var ext = Path.GetExtension(filePath);
|
||||
var left = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
var time = DateTime.Now.ToString("yyyy-MM-dd hh_mm_ss");
|
||||
filePath = downloadFolder.FullName + "/" + left + " " + time + ext;
|
||||
}
|
||||
browser.DownloadCommand(downloadId, BrowserNative.DownloadAction.Begin, filePath);
|
||||
}
|
||||
|
||||
downloads.Add(download);
|
||||
}
|
||||
|
||||
private void HandleDownloadStatus(Browser browser, int downloadId, JSONNode info) {
|
||||
//Debug.Log("Download status: " + info.AsJSON);
|
||||
|
||||
for (int i = 0; i < downloads.Count; i++) {
|
||||
if (downloads[i].browser != browser || downloads[i].downloadId != downloadId) continue;
|
||||
|
||||
var download = downloads[i];
|
||||
|
||||
download.status = info["status"];
|
||||
download.speed = info["speed"];
|
||||
download.percent = info["percentComplete"];
|
||||
if (!string.IsNullOrEmpty(info["fullPath"])) download.name = Path.GetFileName(info["fullPath"]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update() {
|
||||
if (statusBar) {
|
||||
statusBar.text = Status;
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseAll() {
|
||||
for (int i = 0; i < downloads.Count; i++) {
|
||||
if (downloads[i].status == "working") {
|
||||
downloads[i].browser.DownloadCommand(downloads[i].downloadId, BrowserNative.DownloadAction.Pause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void ResumeAll() {
|
||||
for (int i = 0; i < downloads.Count; i++) {
|
||||
if (downloads[i].status == "working") {
|
||||
downloads[i].browser.DownloadCommand(downloads[i].downloadId, BrowserNative.DownloadAction.Resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelAll() {
|
||||
for (int i = 0; i < downloads.Count; i++) {
|
||||
if (downloads[i].status == "working") {
|
||||
downloads[i].browser.DownloadCommand(downloads[i].downloadId, BrowserNative.DownloadAction.Cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearAll() {
|
||||
CancelAll();
|
||||
downloads.Clear();
|
||||
}
|
||||
|
||||
private StringBuilder sb = new StringBuilder();
|
||||
/** Returns a string summarizing things that are downloading. */
|
||||
public string Status {
|
||||
get {
|
||||
if (downloads.Count == 0) return "";
|
||||
|
||||
sb.Length = 0;
|
||||
var rate = 0;
|
||||
for (int i = downloads.Count - 1; i >= 0; i--) {
|
||||
if (sb.Length > 0) sb.Append(", ");
|
||||
sb.Append(downloads[i].name);
|
||||
|
||||
if (downloads[i].status == "working") {
|
||||
if (downloads[i].percent >= 0) sb.Append(" (").Append(downloads[i].percent).Append("%)");
|
||||
else sb.Append(" (??%)");
|
||||
rate += downloads[i].speed;
|
||||
} else {
|
||||
sb.Append(" (").Append(downloads[i].status).Append(")");
|
||||
}
|
||||
}
|
||||
|
||||
var ret = "Downloads";
|
||||
if (rate > 0) {
|
||||
ret += " (" + Mathf.Round(rate / (1024f * 1024) * 100) / 100f + "MiB/s)";
|
||||
}
|
||||
|
||||
return ret + ": " + sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the user's download folder, creating it if needed. */
|
||||
public static string GetUserDownloadFolder() {
|
||||
switch (System.Environment.OSVersion.Platform) {
|
||||
case PlatformID.Win32NT: {
|
||||
|
||||
IntPtr path;
|
||||
var r = SHGetKnownFolderPath(
|
||||
new Guid("{374DE290-123F-4565-9164-39C4925E467B}"), //downloads
|
||||
0x8000, //KF_FLAG_CREATE
|
||||
IntPtr.Zero, //current user
|
||||
out path
|
||||
);
|
||||
|
||||
if (r == 0) {
|
||||
var ret = Marshal.PtrToStringUni(path);
|
||||
Marshal.FreeCoTaskMem(path);
|
||||
return ret;
|
||||
} else {
|
||||
throw new Exception(
|
||||
"Failed to get user download directory",
|
||||
new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error())
|
||||
);
|
||||
}
|
||||
}
|
||||
case PlatformID.Unix: {
|
||||
var path = System.Environment.GetEnvironmentVariable("HOME") + "/Downloads";
|
||||
var di = new DirectoryInfo(path);
|
||||
if (!di.Exists) di.Create();
|
||||
return path;
|
||||
}
|
||||
case PlatformID.MacOSX:
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("Shell32.dll")]
|
||||
private static extern int SHGetKnownFolderPath(
|
||||
[MarshalAs(UnmanagedType.LPStruct)]Guid rfid, uint dwFlags, IntPtr hToken,
|
||||
out IntPtr ppszPath
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7d247427dfff6304db2b292811004b23
|
||||
timeCreated: 1510867277
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4c5a65551ba9c9949a3b3fefeb7fc1bd
|
||||
folderAsset: yes
|
||||
timeCreated: 1447281187
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
[CustomEditor(typeof(Browser))]
|
||||
[CanEditMultipleObjects]
|
||||
public class BrowserEditor : Editor {
|
||||
|
||||
private static string script = "document.body.style.background = 'red';\n";
|
||||
private static string html = "Hello, <i>world</i>!\n";
|
||||
|
||||
private static string[] commandNames;
|
||||
private static BrowserNative.FrameCommand[] commandValues;
|
||||
|
||||
|
||||
static BrowserEditor() {
|
||||
var els = Enum.GetValues(typeof(BrowserNative.FrameCommand));
|
||||
commandNames = new string[els.Length];
|
||||
commandValues = new BrowserNative.FrameCommand[els.Length];
|
||||
int i = 0;
|
||||
foreach (BrowserNative.FrameCommand cmd in els) {
|
||||
commandNames[i] = cmd.ToString();
|
||||
commandValues[i] = cmd;
|
||||
++i;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override bool RequiresConstantRepaint() {
|
||||
//The buttons get stale if we don't keep repainting them.
|
||||
return Application.isPlaying;
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI() {
|
||||
base.OnInspectorGUI();
|
||||
|
||||
if (Application.isPlaying && !serializedObject.isEditingMultipleObjects) {
|
||||
RenderActions();
|
||||
} else if (!Application.isPlaying) {
|
||||
GUILayout.Label("Additional options available in play mode");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void RenderActions() {
|
||||
var browser = (Browser)target;
|
||||
|
||||
if (!browser.IsReady) {
|
||||
GUILayout.Label("Starting...");
|
||||
return;
|
||||
}
|
||||
|
||||
GUILayout.BeginVertical("box");
|
||||
GUILayout.Label("Apply items above:");
|
||||
|
||||
GUILayout.BeginHorizontal("box");
|
||||
{
|
||||
if (GUILayout.Button("Go to URL")) browser.LoadURL(serializedObject.FindProperty("_url").stringValue, false);
|
||||
if (GUILayout.Button("Force to URL")) browser.Url = serializedObject.FindProperty("_url").stringValue;
|
||||
if (GUILayout.Button("Resize")) {
|
||||
browser.Resize(
|
||||
serializedObject.FindProperty("_width").intValue,
|
||||
serializedObject.FindProperty("_height").intValue
|
||||
);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Set Zoom")) browser.Zoom = serializedObject.FindProperty("_zoom").floatValue;
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Label("Actions:");
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUI.enabled = browser.CanGoBack;
|
||||
if (GUILayout.Button("Go back")) browser.GoBack();
|
||||
GUI.enabled = browser.CanGoForward;
|
||||
if (GUILayout.Button("Go forward")) browser.GoForward();
|
||||
GUI.enabled = true;
|
||||
|
||||
|
||||
if (browser.IsLoadingRaw) {
|
||||
if (GUILayout.Button("Stop")) browser.Stop();
|
||||
} else {
|
||||
if (GUILayout.Button("Reload")) browser.Reload();
|
||||
}
|
||||
if (GUILayout.Button("Force Reload")) browser.Reload(true);
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
if (GUILayout.Button("Show Dev Tools")) browser.ShowDevTools();
|
||||
if (GUILayout.Button("Hide Dev Tools")) browser.ShowDevTools(false);
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
|
||||
GUILayout.Label("Script:");
|
||||
script = EditorGUILayout.TextArea(script);
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Eval JavaScript")) {
|
||||
browser.EvalJS(script, "editor command");
|
||||
}
|
||||
if (GUILayout.Button("Eval JavaScript CSP")) {
|
||||
browser.EvalJSCSP(script, "editor command");
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
int pVal = EditorGUILayout.Popup("Send Command:", -1, commandNames);
|
||||
if (pVal != -1) {
|
||||
browser.SendFrameCommand(commandValues[pVal]);
|
||||
}
|
||||
|
||||
GUILayout.Label("HTML:");
|
||||
html = EditorGUILayout.TextArea(html);
|
||||
if (GUILayout.Button("Load HTML")) {
|
||||
browser.LoadHTML(html);
|
||||
}
|
||||
|
||||
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28d4a442795171f4b92b03a99fcbdc6f
|
||||
timeCreated: 1447280847
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,13 @@
|
|||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
[CustomPropertyDrawer(typeof(FlagsFieldAttribute))]
|
||||
public class FlagsEditor : PropertyDrawer {
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
property.intValue = EditorGUI.MaskField(position, label, property.intValue, property.enumNames);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4846ded01fb9d6b489529816869b1d20
|
||||
timeCreated: 1450462477
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,332 @@
|
|||
//This file is partially subject to Chromium's BSD license, read the class notes for more details.
|
||||
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using CursorType = ZenFulcrum.EmbeddedBrowser.BrowserNative.CursorType;
|
||||
|
||||
/**
|
||||
* Utility for generating the cursor icons.
|
||||
*
|
||||
* This isn't really here for general usage, but if you're willing to read the source and
|
||||
* fiddle with things this may give you a head start from starting with nothing.
|
||||
*
|
||||
* The default icons are pulled from
|
||||
* https://chromium.googlesource.com/chromium/src.git/+/master/ui/resources/default_100_percent/common/pointers/
|
||||
* https://chromium.googlesource.com/chromium/src.git/+/master/ui/resources/ui_resources.grd
|
||||
* and
|
||||
* https://chromium.googlesource.com/chromium/src.git/+/master/ui/base/cursor/cursors_aura.cc
|
||||
* This tool is used with a local directory, {IconGenerator.path}, filled with those icons.
|
||||
*
|
||||
* You also need to add a "loading.png" to the folder.
|
||||
*
|
||||
* To use this script, update the local path to your icons, define ZF_ICON_GENERATOR, and run it in
|
||||
* from Assets->ZF Browser->Generate Icons.
|
||||
*/
|
||||
[ExecuteInEditMode]
|
||||
public class IconGenerator {
|
||||
private const string path = @"/my/path/to/chromium/ui-resources/default_100_percent/common/pointers";
|
||||
private const string destAsset = "ZFBrowser/Resources/Browser/Cursors";
|
||||
|
||||
public static bool useBig = false;
|
||||
|
||||
#if ZF_ICON_GENERATOR
|
||||
[MenuItem("Assets/ZF Browser/Generate Icons")]
|
||||
#endif
|
||||
public static void GenerateIcons() {
|
||||
var icons = new SortedDictionary<string, Texture2D>();
|
||||
|
||||
var w = -1;
|
||||
var h = -1;
|
||||
|
||||
foreach (var file in Directory.GetFiles(path)) {
|
||||
if (useBig && !file.Contains("_big.png")) continue;
|
||||
if (!useBig && file.Contains("_big.png")) continue;
|
||||
|
||||
var tex = new Texture2D(0, 0);
|
||||
tex.LoadImage(File.ReadAllBytes(file));
|
||||
|
||||
if (w < 0) {
|
||||
w = tex.width;
|
||||
h = tex.height;
|
||||
} else if (w != tex.width || h != tex.height) {
|
||||
throw new Exception("Icons are not all the same size. This differs: " + file);
|
||||
}
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(file);
|
||||
if (useBig) name = name.Substring(0, name.Length - 4);
|
||||
icons[name] = tex;
|
||||
}
|
||||
|
||||
//Also add one for "cursor: none"
|
||||
icons["_none_"] = null;
|
||||
|
||||
var res = new Texture2D(w * icons.Count, h, TextureFormat.ARGB32, false);
|
||||
|
||||
var descData = new StringBuilder();
|
||||
var namesToPositions = new Dictionary<string, int>();
|
||||
var i = 0;
|
||||
foreach (var kvp in icons) {
|
||||
if (kvp.Value == null) {
|
||||
Fill(new Color(0, 0, 0, 0), res, i * w, 0, w, h);
|
||||
} else {
|
||||
Copy(kvp.Value, res, i * w, 0);
|
||||
}
|
||||
namesToPositions[kvp.Key] = i++;
|
||||
}
|
||||
|
||||
foreach (var kvp in mapping) {
|
||||
var pos = -1;
|
||||
try {
|
||||
if (kvp.Value.name != "_custom_") pos = namesToPositions[kvp.Value.name];
|
||||
} catch (KeyNotFoundException) {
|
||||
throw new KeyNotFoundException("No file found for " + kvp.Value.name);
|
||||
}
|
||||
|
||||
if (descData.Length != 0) descData.Append("\n");
|
||||
|
||||
var hotspot = kvp.Value.hotspot;
|
||||
if (!useBig) {
|
||||
hotspot.x = Mathf.Round(hotspot.x * .5f) - 3;
|
||||
hotspot.y = Mathf.Round(kvp.Value.hotspot.y * .5f) - 4;
|
||||
}
|
||||
|
||||
descData
|
||||
.Append(kvp.Key).Append(",")
|
||||
.Append(pos).Append(",")
|
||||
.Append(hotspot.x).Append(",")
|
||||
.Append(hotspot.y)
|
||||
;
|
||||
|
||||
}
|
||||
|
||||
var resName = Application.dataPath + "/" + destAsset;
|
||||
File.WriteAllBytes(
|
||||
resName + ".png",
|
||||
res.EncodeToPNG()
|
||||
);
|
||||
File.WriteAllText(
|
||||
resName + ".csv",
|
||||
descData.ToString()
|
||||
);
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log("Wrote icons files to " + resName + ".(png|csv) size: " + w + "x" + h);
|
||||
}
|
||||
|
||||
private static void Fill(Color color, Texture2D dest, int sx, int sy, int w, int h) {
|
||||
for (int x = sx; x < w; ++x) {
|
||||
for (int y = sy; y < h; ++y) {
|
||||
dest.SetPixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Copy(Texture2D src, Texture2D dest, int destX, int destY) {
|
||||
//slow, but fine for a utility
|
||||
for (int x = 0; x < src.width; ++x) {
|
||||
for (int y = 0; y < src.height; ++y) {
|
||||
dest.SetPixel(x + destX, y + destY, src.GetPixel(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CursorInfo {
|
||||
public CursorInfo(string name, Vector2 hotspot) {
|
||||
this.name = name;
|
||||
this.hotspot = hotspot;
|
||||
}
|
||||
public string name;
|
||||
public Vector2 hotspot;
|
||||
}
|
||||
|
||||
private static Dictionary<CursorType, CursorInfo> mapping = new Dictionary<CursorType, CursorInfo>() {
|
||||
//Hotspots in for the default Chromium cursors can be found in ui/base/cursor/cursors_aura.cc, this is adapted
|
||||
//from there.
|
||||
//Note that we are always using the 2x (_big) icons.
|
||||
{
|
||||
//{19, 11}, {38, 22}} alias kCursorAlias IDR_AURA_CURSOR_ALIAS CT_ALIAS
|
||||
CursorType.Alias,
|
||||
new CursorInfo("alias", new Vector2(19, 11))
|
||||
}, {
|
||||
//{30, 30}, {60, 60}} cell kCursorCell IDR_AURA_CURSOR_CELL CT_CELL
|
||||
CursorType.Cell,
|
||||
new CursorInfo("cell", new Vector2(30, 30))
|
||||
}, {
|
||||
//{35, 29}, {70, 58}} sb_h_double_arrow kCursorColumnResize IDR_AURA_CURSOR_COL_RESIZE CT_COLUMNRESIZE
|
||||
CursorType.ColumnResize,
|
||||
new CursorInfo("sb_h_double_arrow", new Vector2(35, 29))
|
||||
}, {
|
||||
//{11, 11}, {22, 22}} context_menu kCursorContextMenu IDR_AURA_CURSOR_CONTEXT_MENU CT_CONTEXTMENU
|
||||
CursorType.ContextMenu,
|
||||
new CursorInfo("context_menu", new Vector2(11, 11))
|
||||
}, {
|
||||
//{10, 10}, {20, 20}} copy kCursorCopy IDR_AURA_CURSOR_COPY CT_COPY
|
||||
CursorType.Copy,
|
||||
new CursorInfo("copy", new Vector2(10, 10))
|
||||
}, {
|
||||
//{31, 30}, {62, 60}} crosshair kCursorCross IDR_AURA_CURSOR_CROSSHAIR CT_CROSS
|
||||
CursorType.Cross,
|
||||
new CursorInfo("crosshair", new Vector2(31, 30))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} custom kCursorCustom IDR_NONE CT_CUSTOM
|
||||
CursorType.Custom,
|
||||
new CursorInfo("_custom_", new Vector2(-1, -1))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorEastPanning IDR_NONE CT_EASTPANNING
|
||||
CursorType.EastPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{35, 29}, {70, 58}} sb_h_double_arrow kCursorEastResize IDR_AURA_CURSOR_EAST_RESIZE CT_EASTRESIZE
|
||||
CursorType.EastResize,
|
||||
new CursorInfo("sb_h_double_arrow", new Vector2(35, 29))
|
||||
}, {
|
||||
//{35, 29}, {70, 58}} sb_h_double_arrow kCursorEastWestResize IDR_AURA_CURSOR_EAST_WEST_RESIZE CT_EASTWESTRESIZE
|
||||
CursorType.EastWestResize,
|
||||
new CursorInfo("sb_h_double_arrow", new Vector2(35, 29))
|
||||
}, {
|
||||
//{21, 11}, {42, 22}} fleur kCursorGrab IDR_AURA_CURSOR_GRAB CT_GRAB
|
||||
CursorType.Grab,
|
||||
new CursorInfo("fleur", new Vector2(21, 11))
|
||||
}, {
|
||||
//{20, 12}, {40, 24}} hand3 kCursorGrabbing IDR_AURA_CURSOR_GRABBING CT_GRABBING
|
||||
CursorType.Grabbing,
|
||||
new CursorInfo("hand3", new Vector2(20, 12))
|
||||
}, {
|
||||
//{25, 7}, {50, 14}} hand2 kCursorHand IDR_AURA_CURSOR_HAND CT_HAND
|
||||
CursorType.Hand,
|
||||
new CursorInfo("hand2", new Vector2(25, 7))
|
||||
}, {
|
||||
//{10, 11}, {20, 22}} help kCursorHelp IDR_AURA_CURSOR_HELP CT_HELP
|
||||
CursorType.Help,
|
||||
new CursorInfo("help", new Vector2(10, 11))
|
||||
}, {
|
||||
//{30, 32}, {60, 64}} xterm kCursorIBeam IDR_AURA_CURSOR_IBEAM CT_IBEAM
|
||||
CursorType.IBeam,
|
||||
new CursorInfo("xterm", new Vector2(30, 32))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorMiddlePanning IDR_NONE CT_MIDDLEPANNING
|
||||
CursorType.MiddlePanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{32, 31}, {64, 62}} move kCursorMove IDR_AURA_CURSOR_MOVE CT_MOVE
|
||||
CursorType.Move,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{10, 10}, {20, 20}} nodrop kCursorNoDrop IDR_AURA_CURSOR_NO_DROP CT_NODROP
|
||||
CursorType.NoDrop,
|
||||
new CursorInfo("nodrop", new Vector2(10, 10))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorNone IDR_NONE CT_NONE
|
||||
CursorType.None,
|
||||
new CursorInfo("_none_", new Vector2(0, 0))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorNorthEastPanning IDR_NONE CT_NORTHEASTPANNING
|
||||
CursorType.NorthEastPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{31, 28}, {62, 56}} top_right_corner kCursorNorthEastResize IDR_AURA_CURSOR_NORTH_EAST_RESIZE CT_NORTHEASTRESIZE
|
||||
CursorType.NorthEastResize,
|
||||
new CursorInfo("top_right_corner", new Vector2(31, 28))
|
||||
}, {
|
||||
//{32, 30}, {64, 60}} top_right_corner kCursorNorthEastSouthWestResize IDR_AURA_CURSOR_NORTH_EAST_SOUTH_WEST_RESIZE CT_NORTHEASTSOUTHWESTRESIZE
|
||||
CursorType.NorthEastSouthWestResize,
|
||||
new CursorInfo("top_right_corner", new Vector2(32, 30))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorNorthPanning IDR_NONE CT_NORTHPANNING
|
||||
CursorType.NorthPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{29, 32}, {58, 64}} sb_v_double_arrow kCursorNorthResize IDR_AURA_CURSOR_NORTH_RESIZE CT_NORTHRESIZE
|
||||
CursorType.NorthResize,
|
||||
new CursorInfo("sb_v_double_arrow", new Vector2(29, 32))
|
||||
}, {
|
||||
//{29, 32}, {58, 64}} sb_v_double_arrow kCursorNorthSouthResize IDR_AURA_CURSOR_NORTH_SOUTH_RESIZE CT_NORTHSOUTHRESIZE
|
||||
CursorType.NorthSouthResize,
|
||||
new CursorInfo("sb_v_double_arrow", new Vector2(29, 32))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorNorthWestPanning IDR_NONE CT_NORTHWESTPANNING
|
||||
CursorType.NorthWestPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{28, 28}, {56, 56}} top_left_corner kCursorNorthWestResize IDR_AURA_CURSOR_NORTH_WEST_RESIZE CT_NORTHWESTRESIZE
|
||||
CursorType.NorthWestResize,
|
||||
new CursorInfo("top_left_corner", new Vector2(28, 28))
|
||||
}, {
|
||||
//{32, 31}, {64, 62}} top_left_corner kCursorNorthWestSouthEastResize IDR_AURA_CURSOR_NORTH_WEST_SOUTH_EAST_RESIZE CT_NORTHWESTSOUTHEASTRESIZE
|
||||
CursorType.NorthWestSouthEastResize,
|
||||
new CursorInfo("top_left_corner", new Vector2(32, 31))
|
||||
}, {
|
||||
//{10, 10}, {20, 20}} nodrop kCursorNotAllowed IDR_AURA_CURSOR_NO_DROP CT_NOTALLOWED
|
||||
CursorType.NotAllowed,
|
||||
new CursorInfo("nodrop", new Vector2(10, 10))
|
||||
}, {
|
||||
//{10, 10}, {20, 20}} left_ptr kCursorPointer IDR_AURA_CURSOR_PTR CT_POINTER
|
||||
CursorType.Pointer,
|
||||
new CursorInfo("left_ptr", new Vector2(10, 10))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorProgress IDR_NONE CT_PROGRESS
|
||||
CursorType.Progress,
|
||||
new CursorInfo("loading", new Vector2(32, 32))
|
||||
}, {
|
||||
//{29, 32}, {58, 64}} sb_v_double_arrow kCursorRowResize IDR_AURA_CURSOR_ROW_RESIZE CT_ROWRESIZE
|
||||
CursorType.RowResize,
|
||||
new CursorInfo("sb_v_double_arrow", new Vector2(29, 32))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorSouthEastPanning IDR_NONE CT_SOUTHEASTPANNING
|
||||
CursorType.SouthEastPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{28, 28}, {56, 56}} top_left_corner kCursorSouthEastResize IDR_AURA_CURSOR_SOUTH_EAST_RESIZE CT_SOUTHEASTRESIZE
|
||||
CursorType.SouthEastResize,
|
||||
new CursorInfo("top_left_corner", new Vector2(28, 28))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorSouthPanning IDR_NONE CT_SOUTHPANNING
|
||||
CursorType.SouthPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{29, 32}, {58, 64}} sb_v_double_arrow kCursorSouthResize IDR_AURA_CURSOR_SOUTH_RESIZE CT_SOUTHRESIZE
|
||||
CursorType.SouthResize,
|
||||
new CursorInfo("sb_v_double_arrow", new Vector2(29, 32))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorSouthWestPanning IDR_NONE CT_SOUTHWESTPANNING
|
||||
CursorType.SouthWestPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{31, 28}, {62, 56}} top_right_corner kCursorSouthWestResize IDR_AURA_CURSOR_SOUTH_WEST_RESIZE CT_SOUTHWESTRESIZE
|
||||
CursorType.SouthWestResize,
|
||||
new CursorInfo("top_right_corner", new Vector2(31, 28))
|
||||
}, {
|
||||
//{32, 30}, {64, 60}} xterm_horiz kCursorVerticalText IDR_AURA_CURSOR_XTERM_HORIZ CT_VERTICALTEXT
|
||||
CursorType.VerticalText,
|
||||
new CursorInfo("xterm_horiz", new Vector2(32, 30))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorWait IDR_NONE CT_WAIT
|
||||
CursorType.Wait,
|
||||
new CursorInfo("loading", new Vector2(32, 32))
|
||||
}, {
|
||||
//{??, ??}, {??, ??}} _unknown_ kCursorWestPanning IDR_NONE CT_WESTPANNING
|
||||
CursorType.WestPanning,
|
||||
new CursorInfo("move", new Vector2(32, 31))
|
||||
}, {
|
||||
//{35, 29}, {70, 58}} sb_h_double_arrow kCursorWestResize IDR_AURA_CURSOR_WEST_RESIZE CT_WESTRESIZE
|
||||
CursorType.WestResize,
|
||||
new CursorInfo("sb_h_double_arrow", new Vector2(35, 29))
|
||||
}, {
|
||||
//{25, 26}, {50, 52}} zoom_in kCursorZoomIn IDR_AURA_CURSOR_ZOOM_IN CT_ZOOMIN
|
||||
CursorType.ZoomIn,
|
||||
new CursorInfo("zoom_in", new Vector2(25, 26))
|
||||
}, {
|
||||
//{26, 26}, {52, 52}} zoom_out kCursorZoomOut IDR_AURA_CURSOR_ZOOM_OUT CT_ZOOMOUT
|
||||
CursorType.ZoomOut,
|
||||
new CursorInfo("zoom_out", new Vector2(26, 26))
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7924e40fd70097143810a7ad82727b47
|
||||
timeCreated: 1447967713
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Callbacks;
|
||||
using UnityEngine;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Getting CEF running on a build result requires some fiddling to get all the files in the right place.
|
||||
*/
|
||||
class PostBuildStandalone {
|
||||
|
||||
static readonly List<string> byBinFiles = new List<string>() {
|
||||
"natives_blob.bin",
|
||||
"snapshot_blob.bin",
|
||||
"v8_context_snapshot.bin",
|
||||
"icudtl.dat",
|
||||
};
|
||||
|
||||
[PostProcessBuild(10)]
|
||||
public static void PostprocessLinuxOrWindowsBuild(BuildTarget target, string buildFile) {
|
||||
//prereq
|
||||
var windows = target == BuildTarget.StandaloneWindows || target == BuildTarget.StandaloneWindows64;
|
||||
var linux = target == BuildTarget.StandaloneLinux || target == BuildTarget.StandaloneLinuxUniversal || target == BuildTarget.StandaloneLinux64;
|
||||
|
||||
if (!windows && !linux) return;
|
||||
|
||||
if (target == BuildTarget.StandaloneLinux || target == BuildTarget.StandaloneLinuxUniversal) {
|
||||
throw new Exception("ZFBrowser: Only x86_64 Linux is supported");
|
||||
}
|
||||
|
||||
|
||||
//base info
|
||||
string buildType;
|
||||
if (windows) buildType = "w" + (target == BuildTarget.StandaloneWindows64 ? "64" : "32");
|
||||
else buildType = "l64";
|
||||
|
||||
Debug.Log("ZFBrowser: Post processing " + buildFile + " as " + buildType);
|
||||
|
||||
string buildName;
|
||||
if (windows) buildName = Regex.Match(buildFile, @"/([^/]+)\.exe$").Groups[1].Value;
|
||||
else buildName = Regex.Match(buildFile, @"\/([^\/]+?)(\.x86(_64)?)?$").Groups[1].Value;
|
||||
|
||||
var buildPath = Directory.GetParent(buildFile);
|
||||
var dataPath = buildPath + "/" + buildName + "_Data";
|
||||
var pluginsPath = dataPath + "/Plugins/";
|
||||
|
||||
|
||||
//start copying
|
||||
|
||||
|
||||
//can't use FileLocations because we may not be building the same type as the editor
|
||||
var platformPluginsSrc = ZFFolder + "/Plugins/" + buildType;
|
||||
|
||||
//(Unity will copy the .dll and .so files for us)
|
||||
|
||||
//Copy "root" .bin files
|
||||
foreach (var file in byBinFiles) {
|
||||
File.Copy(platformPluginsSrc + "/" + file, pluginsPath + file, true);
|
||||
}
|
||||
|
||||
File.Copy(ZFFolder + "/ThirdPartyNotices.txt", pluginsPath + "/ThirdPartyNotices.txt", true);
|
||||
|
||||
//Copy the needed resources
|
||||
var resSrcDir = platformPluginsSrc + "/CEFResources";
|
||||
foreach (var filePath in Directory.GetFiles(resSrcDir)) {
|
||||
var fileName = new FileInfo(filePath).Name;
|
||||
if (fileName.EndsWith(".meta")) continue;
|
||||
|
||||
File.Copy(filePath, pluginsPath + fileName, true);
|
||||
}
|
||||
|
||||
//Slave process (doesn't get automatically copied by Unity like the shared libs)
|
||||
var exeExt = windows ? ".exe" : "";
|
||||
File.Copy(
|
||||
platformPluginsSrc + "/" + FileLocations.SlaveExecutable + exeExt,
|
||||
pluginsPath + FileLocations.SlaveExecutable + exeExt,
|
||||
true
|
||||
);
|
||||
if (linux) MakeExecutable(pluginsPath + FileLocations.SlaveExecutable + exeExt);
|
||||
|
||||
//Locales
|
||||
var localesSrcDir = platformPluginsSrc + "/CEFResources/locales";
|
||||
var localesDestDir = dataPath + "/Plugins/locales";
|
||||
Directory.CreateDirectory(localesDestDir);
|
||||
foreach (var filePath in Directory.GetFiles(localesSrcDir)) {
|
||||
var fileName = new FileInfo(filePath).Name;
|
||||
if (fileName.EndsWith(".meta")) continue;
|
||||
File.Copy(filePath, localesDestDir + "/" + fileName, true);
|
||||
}
|
||||
|
||||
//Newer versions of Unity put the shared libs in the wrong place. Move them to where we expect them.
|
||||
if (linux && File.Exists(pluginsPath + "x86_64/zf_cef.so")) {
|
||||
foreach (var libFile in new[] {"zf_cef.so", "libEGL.so", "libGLESv2.so", "libwidevinecdmadapter.so", "libZFProxyWeb.so"}) {
|
||||
ForceMove(pluginsPath + "x86_64/" + libFile, pluginsPath + libFile);
|
||||
}
|
||||
}
|
||||
|
||||
WriteBrowserAssets(dataPath + "/" + StandaloneWebResources.DefaultPath);
|
||||
}
|
||||
|
||||
[PostProcessBuild(10)]
|
||||
public static void PostprocessMacBuild(BuildTarget target, string buildFile) {
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
if (target != BuildTarget.StandaloneOSX) return;
|
||||
#else
|
||||
if (target == BuildTarget.StandaloneOSXUniversal || target == BuildTarget.StandaloneOSXIntel) {
|
||||
throw new Exception("Only x86_64 is supported");
|
||||
}
|
||||
if (target != BuildTarget.StandaloneOSXIntel64) return;
|
||||
#endif
|
||||
|
||||
Debug.Log("Post processing " + buildFile);
|
||||
|
||||
//var buildName = Regex.Match(buildFile, @"\/([^\/]+?)\.app$").Groups[1].Value;
|
||||
var buildPath = buildFile;
|
||||
var platformPluginsSrc = ZFFolder + "/Plugins/m64";
|
||||
|
||||
//Copy app bits
|
||||
CopyDirectory(
|
||||
platformPluginsSrc + "/BrowserLib.app/Contents/Frameworks/Chromium Embedded Framework.framework",
|
||||
buildPath + "/Contents/Frameworks/Chromium Embedded Framework.framework"
|
||||
);
|
||||
CopyDirectory(
|
||||
platformPluginsSrc + "/BrowserLib.app/Contents/Frameworks/ZFGameBrowser.app",
|
||||
buildPath + "/Contents/Frameworks/ZFGameBrowser.app"
|
||||
);
|
||||
|
||||
MakeExecutable(buildPath + "/BrowserLib.app/Contents/Frameworks/ZFGameBrowser.app/Contents/MacOS/ZFGameBrowser");
|
||||
|
||||
if (!Directory.Exists(buildPath + "/Contents/Plugins")) Directory.CreateDirectory(buildPath + "/Contents/Plugins");
|
||||
File.Copy(platformPluginsSrc + "/libZFProxyWeb.dylib", buildPath + "/Contents/Plugins/libZFProxyWeb.dylib", true);
|
||||
|
||||
File.Copy(ZFFolder + "/ThirdPartyNotices.txt", buildPath + "/ThirdPartyNotices.txt", true);
|
||||
|
||||
//BrowserAssets
|
||||
WriteBrowserAssets(buildPath + "/Contents/" + StandaloneWebResources.DefaultPath);
|
||||
}
|
||||
|
||||
|
||||
private static void WriteBrowserAssets(string path) {
|
||||
//Debug.Log("Writing browser assets to " + path);
|
||||
|
||||
var htmlDir = Application.dataPath + "/../BrowserAssets";
|
||||
var allData = new Dictionary<string, byte[]>();
|
||||
if (Directory.Exists(htmlDir)) {
|
||||
foreach (var file in Directory.GetFiles(htmlDir, "*", SearchOption.AllDirectories)) {
|
||||
var localPath = file.Substring(htmlDir.Length).Replace("\\", "/");
|
||||
allData[localPath] = File.ReadAllBytes(file);
|
||||
}
|
||||
}
|
||||
|
||||
var wr = new StandaloneWebResources(path);
|
||||
wr.WriteData(allData);
|
||||
}
|
||||
|
||||
private static void ForceMove(string src, string dest) {
|
||||
if (File.Exists(dest)) File.Delete(dest);
|
||||
File.Move(src, dest);
|
||||
}
|
||||
|
||||
private static string ZFFolder {
|
||||
get {
|
||||
var path = new System.Diagnostics.StackTrace(true).GetFrame(0).GetFileName();
|
||||
path = Directory.GetParent(path).Parent.Parent.FullName;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string src, string dest) {
|
||||
foreach (var dir in Directory.GetDirectories(src, "*", SearchOption.AllDirectories)) {
|
||||
Directory.CreateDirectory(dir.Replace(src, dest));
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(src, "*", SearchOption.AllDirectories)) {
|
||||
if (file.EndsWith(".meta")) continue;
|
||||
File.Copy(file, file.Replace(src, dest), true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MakeExecutable(string fileName) {
|
||||
#if UNITY_EDITOR_WIN
|
||||
Debug.LogWarning("ZFBrowser: Be sure to mark the file \"" + fileName + "\" as executable (chmod +x) when you distribute it. If it's not executable the browser won't work.");
|
||||
#else
|
||||
//dec 493 = oct 755 = -rwxr-xr-x
|
||||
chmod(fileName, 493);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
[DllImport("__Internal")] static extern int symlink(string destStr, string symFile);
|
||||
[DllImport("__Internal")] static extern int chmod(string file, int mode);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 821c048fda6531349be543e9b923a328
|
||||
timeCreated: 1449181504
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "ZFBrowser-Editor",
|
||||
"references": [
|
||||
"ZFBrowser"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": []
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fe934ebeb18e2174a8b814db25fbce70
|
||||
timeCreated: 1518479097
|
||||
licenseType: Store
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
using UnityEngine.Networking;
|
||||
#endif
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* WebResources implementation that grabs resources directly from Assets/../BrowserAssets.
|
||||
*/
|
||||
class EditorWebResources : WebResources {
|
||||
protected string basePath;
|
||||
|
||||
public EditorWebResources() {
|
||||
//NB: If you try to read Application.dataPath later you may not be on the main thread and it won't work.
|
||||
basePath = Path.GetDirectoryName(Application.dataPath) + "/BrowserAssets";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for "../asdf", "asdf/..\asdf", etc.
|
||||
/// </summary>
|
||||
private readonly Regex matchDots = new Regex(@"(^|[/\\])\.[2,]($|[/\\])");
|
||||
|
||||
public override void HandleRequest(int id, string url) {
|
||||
var parsedURL = new Uri(url);
|
||||
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
var path = UnityWebRequest.UnEscapeURL(parsedURL.AbsolutePath);
|
||||
#else
|
||||
var path = WWW.UnEscapeURL(parsedURL.AbsolutePath);
|
||||
#endif
|
||||
|
||||
if (matchDots.IsMatch(path)) {
|
||||
SendError(id, "Invalid path", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
var file = new FileInfo(Application.dataPath + "/../BrowserAssets/" + path);
|
||||
|
||||
if (!file.Exists) {
|
||||
SendError(id, "Not found", 404);
|
||||
return;
|
||||
}
|
||||
|
||||
//fixme: send 404 if file capitalization doesn't match. (Unfortunately, not a quick one-liner)
|
||||
|
||||
SendFile(id, file);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 59d86905d3bcefc4586e70747332bb6f
|
||||
timeCreated: 1449617902
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,238 @@
|
|||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
#define ZF_OSX
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Helper/worker class for displaying an external keyboard and
|
||||
/// sending the input to a browser.
|
||||
///
|
||||
/// Don't put this script on your target browser directly, add a separate browser
|
||||
/// to the scene and attach it to that instead.
|
||||
///
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Browser))]
|
||||
[RequireComponent(typeof(PointerUIBase))]
|
||||
public class ExternalKeyboard : MonoBehaviour {
|
||||
|
||||
[Tooltip("Set to true before startup to have the keyboard automatically hook to the browser with the most recently focused text field.")]
|
||||
public bool automaticFocus;
|
||||
|
||||
[Tooltip("Browser to start as the focused browser for this keyboard. Not really needed if automaticFocus is on.")]
|
||||
public Browser initialBrowser;
|
||||
|
||||
[Tooltip("Set to true to have the keyboard automatically hide when we don't have a text entry box to type into.")]
|
||||
public bool hideWhenUnneeded = true;
|
||||
|
||||
protected PointerUIBase activeBrowserUI;
|
||||
protected Browser keyboardBrowser;
|
||||
protected bool forcingFocus;
|
||||
|
||||
protected Browser _activeBrowser;
|
||||
/// <summary>
|
||||
/// Browser that gets input if we press keys on the keyboard.
|
||||
/// </summary>
|
||||
protected virtual Browser ActiveBrowser {
|
||||
get { return _activeBrowser; }
|
||||
set {
|
||||
_SetActiveBrowser(value);
|
||||
DoFocus(_activeBrowser);
|
||||
}
|
||||
}
|
||||
|
||||
protected void _SetActiveBrowser(Browser browser) {
|
||||
if (ActiveBrowser) {
|
||||
if (activeBrowserUI && forcingFocus) {
|
||||
activeBrowserUI.ForceKeyboardHasFocus(false);
|
||||
forcingFocus = false;
|
||||
}
|
||||
}
|
||||
|
||||
_activeBrowser = browser;
|
||||
activeBrowserUI = ActiveBrowser.GetComponent<PointerUIBase>();
|
||||
if (!activeBrowserUI) {
|
||||
//We can't focus the browser when we type, so the typed letters won't appear as we type.
|
||||
Debug.LogWarning("Browser does not haver a PointerUI, external keyboard may not work properly.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the focus of the keyboard changes.
|
||||
/// The browser is the browser we are focused on (may or may not be different), editable will be true if we
|
||||
/// are expected to type in the new focus, false if not.
|
||||
/// </summary>
|
||||
public event Action<Browser, bool> onFocusChange = (browser, editable) => {};
|
||||
|
||||
public void Awake() {
|
||||
var keyboardPage = Resources.Load<TextAsset>("Browser/Keyboard").text;
|
||||
|
||||
keyboardBrowser = GetComponent<Browser>();
|
||||
|
||||
keyboardBrowser.onBrowserFocus += OnBrowserFocus;
|
||||
keyboardBrowser.LoadHTML(keyboardPage);
|
||||
keyboardBrowser.RegisterFunction("textTyped", TextTyped);
|
||||
keyboardBrowser.RegisterFunction("commandEntered", CommandEntered);
|
||||
|
||||
if (initialBrowser) ActiveBrowser = initialBrowser;
|
||||
|
||||
if (automaticFocus) {
|
||||
StartCoroutine(FindAndListenForBrowsers());
|
||||
}
|
||||
}
|
||||
|
||||
protected IEnumerator FindAndListenForBrowsers() {
|
||||
yield return null;
|
||||
foreach (var browser in FindObjectsOfType<Browser>()) {
|
||||
if (browser == keyboardBrowser) continue;
|
||||
ObserveBrowser(browser);
|
||||
}
|
||||
Browser.onAnyBrowserCreated += ObserveBrowser;
|
||||
//in theory we shouldn't need to deal with browsers being destroyed since the whole callback chain should get cleaned up
|
||||
//(might need some more work if you repeatedly destroy and recreate keyboards, though)
|
||||
}
|
||||
|
||||
protected void ObserveBrowser(Browser browser) {
|
||||
browser.onNodeFocus += (tagName, editable, value) => {
|
||||
if (!this) return;
|
||||
if (!browser.focusState.hasMouseFocus) return;
|
||||
|
||||
DoFocus(browser);
|
||||
};
|
||||
|
||||
var pointerUI = browser.GetComponent<PointerUIBase>();
|
||||
if (pointerUI) {
|
||||
pointerUI.onClick += () => {
|
||||
DoFocus(browser);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected void DoFocus(Browser browser) {
|
||||
if (browser != ActiveBrowser) {
|
||||
_SetActiveBrowser(browser);
|
||||
}
|
||||
|
||||
bool visible;
|
||||
if (browser) visible = browser.focusState.focusedNodeEditable;
|
||||
else visible = false;
|
||||
SetVisible(visible);
|
||||
|
||||
onFocusChange(_activeBrowser, visible);
|
||||
}
|
||||
|
||||
protected void SetVisible(bool visible) {
|
||||
var renderer = GetComponent<Renderer>();
|
||||
if (renderer) renderer.enabled = visible;
|
||||
var collider = GetComponent<Collider>();
|
||||
if (collider) collider.enabled = visible;
|
||||
}
|
||||
|
||||
protected void OnBrowserFocus(bool mouseFocused, bool kbFocused) {
|
||||
//when our keyboard is focused, focus the browser we will be typing into.
|
||||
if (!activeBrowserUI) return;
|
||||
|
||||
if ((mouseFocused || kbFocused) && !forcingFocus) {
|
||||
activeBrowserUI.ForceKeyboardHasFocus(true);
|
||||
forcingFocus = true;
|
||||
}
|
||||
|
||||
if (!(mouseFocused || kbFocused) && forcingFocus) {
|
||||
activeBrowserUI.ForceKeyboardHasFocus(false);
|
||||
forcingFocus = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void CommandEntered(JSONNode args) {
|
||||
if (!ActiveBrowser) return;
|
||||
|
||||
string command = args[0];
|
||||
bool shiftPressed = args[1];
|
||||
|
||||
if (shiftPressed) ActiveBrowser.PressKey(KeyCode.LeftShift, KeyAction.Press);
|
||||
|
||||
|
||||
#if ZF_OSX
|
||||
const KeyCode wordShifter = KeyCode.LeftAlt;
|
||||
#else
|
||||
const KeyCode wordShifter = KeyCode.LeftControl;
|
||||
#endif
|
||||
|
||||
switch (command) {
|
||||
case "backspace":
|
||||
ActiveBrowser.PressKey(KeyCode.Backspace);
|
||||
break;
|
||||
case "copy":
|
||||
ActiveBrowser.SendFrameCommand(BrowserNative.FrameCommand.Copy);
|
||||
break;
|
||||
case "cut":
|
||||
ActiveBrowser.SendFrameCommand(BrowserNative.FrameCommand.Cut);
|
||||
break;
|
||||
case "delete":
|
||||
ActiveBrowser.PressKey(KeyCode.Delete);
|
||||
break;
|
||||
case "down":
|
||||
ActiveBrowser.PressKey(KeyCode.DownArrow);
|
||||
break;
|
||||
case "end":
|
||||
ActiveBrowser.PressKey(KeyCode.End);
|
||||
break;
|
||||
case "home":
|
||||
ActiveBrowser.PressKey(KeyCode.Home);
|
||||
break;
|
||||
case "insert":
|
||||
ActiveBrowser.PressKey(KeyCode.Insert);
|
||||
break;
|
||||
case "left":
|
||||
ActiveBrowser.PressKey(KeyCode.LeftArrow);
|
||||
break;
|
||||
case "pageDown":
|
||||
ActiveBrowser.PressKey(KeyCode.PageDown);
|
||||
break;
|
||||
case "pageUp":
|
||||
ActiveBrowser.PressKey(KeyCode.PageUp);
|
||||
break;
|
||||
case "paste":
|
||||
ActiveBrowser.SendFrameCommand(BrowserNative.FrameCommand.Paste);
|
||||
break;
|
||||
case "redo":
|
||||
ActiveBrowser.SendFrameCommand(BrowserNative.FrameCommand.Redo);
|
||||
break;
|
||||
case "right":
|
||||
ActiveBrowser.PressKey(KeyCode.RightArrow);
|
||||
break;
|
||||
case "selectAll":
|
||||
ActiveBrowser.SendFrameCommand(BrowserNative.FrameCommand.SelectAll);
|
||||
break;
|
||||
case "undo":
|
||||
ActiveBrowser.SendFrameCommand(BrowserNative.FrameCommand.Undo);
|
||||
break;
|
||||
case "up":
|
||||
ActiveBrowser.PressKey(KeyCode.UpArrow);
|
||||
break;
|
||||
case "wordLeft":
|
||||
ActiveBrowser.PressKey(wordShifter, KeyAction.Press);
|
||||
ActiveBrowser.PressKey(KeyCode.LeftArrow);
|
||||
ActiveBrowser.PressKey(wordShifter, KeyAction.Release);
|
||||
break;
|
||||
case "wordRight":
|
||||
ActiveBrowser.PressKey(wordShifter, KeyAction.Press);
|
||||
ActiveBrowser.PressKey(KeyCode.RightArrow);
|
||||
ActiveBrowser.PressKey(wordShifter, KeyAction.Release);
|
||||
break;
|
||||
}
|
||||
|
||||
if (shiftPressed) ActiveBrowser.PressKey(KeyCode.LeftShift, KeyAction.Release);
|
||||
}
|
||||
|
||||
protected void TextTyped(JSONNode args) {
|
||||
if (!ActiveBrowser) return;
|
||||
|
||||
ActiveBrowser.TypeText(args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: faa68da6a73a843439bee4b722ce18b4
|
||||
timeCreated: 1511320118
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,144 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
public static class FileLocations {
|
||||
public const string SlaveExecutable = "ZFGameBrowser";
|
||||
|
||||
private static CEFDirs _dirs;
|
||||
public static CEFDirs Dirs {
|
||||
get { return _dirs ?? (_dirs = GetCEFDirs()); }
|
||||
}
|
||||
|
||||
public class CEFDirs {
|
||||
/** Where to find cef.pak, et al */
|
||||
public string resourcesPath;
|
||||
/** Where to find .dll, .so, natives_blob.bin, etc */
|
||||
public string binariesPath;
|
||||
/** Where to find en-US.pak et al */
|
||||
public string localesPath;
|
||||
/** The executable to run for browser processes. */
|
||||
public string subprocessFile;
|
||||
/** Editor/application log file */
|
||||
public string logFile;
|
||||
}
|
||||
|
||||
private static CEFDirs GetCEFDirs() {
|
||||
#if UNITY_EDITOR
|
||||
//In the editor we don't know exactly where we are at, but we can look up one of our scripts and move from there
|
||||
var guids = AssetDatabase.FindAssets("EditorWebResources");
|
||||
if (guids.Length != 1) throw new FileNotFoundException("Failed to locate a single EditorWebResources file");
|
||||
string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
var baseDir = Directory.GetParent(scriptPath).Parent.FullName + "/Plugins";
|
||||
string resourcesPath, localesDir;
|
||||
var platformDir = baseDir;
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
#if UNITY_EDITOR_64
|
||||
platformDir += "/w64";
|
||||
#else
|
||||
platformDir += "/w32";
|
||||
#endif
|
||||
|
||||
resourcesPath = platformDir + "/CEFResources";
|
||||
localesDir = resourcesPath + "/locales";
|
||||
|
||||
//Silly MS.
|
||||
resourcesPath = resourcesPath.Replace("/", "\\");
|
||||
localesDir = localesDir.Replace("/", "\\");
|
||||
platformDir = platformDir.Replace("/", "\\");
|
||||
|
||||
var subprocessFile = platformDir + "/" + SlaveExecutable + ".exe";
|
||||
var logFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "/Unity/Editor/Editor.log";
|
||||
|
||||
#elif UNITY_EDITOR_LINUX
|
||||
#if UNITY_EDITOR_64
|
||||
platformDir += "/l64";
|
||||
#else
|
||||
platformDir += "/w32";
|
||||
#endif
|
||||
|
||||
resourcesPath = platformDir + "/CEFResources";
|
||||
localesDir = resourcesPath + "/locales";
|
||||
|
||||
var subprocessFile = platformDir + "/" + SlaveExecutable;
|
||||
var logFile = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/unity3d/Editor.log";
|
||||
|
||||
#elif UNITY_EDITOR_OSX
|
||||
platformDir += "/m64";
|
||||
|
||||
resourcesPath = platformDir + "/BrowserLib.app/Contents/Frameworks/Chromium Embedded Framework.framework/Resources";
|
||||
localesDir = resourcesPath;
|
||||
|
||||
//Chromium's base::mac::GetAppBundlePath will walk up the tree until it finds an ".app" folder and start
|
||||
//looking for pieces from there. That's why everything is hidden in a fake "BrowserLib.app"
|
||||
//folder that's not actually an app.
|
||||
var subprocessFile = platformDir + "/BrowserLib.app/Contents/Frameworks/" + SlaveExecutable + ".app/Contents/MacOS/" + SlaveExecutable;
|
||||
|
||||
var logFile = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/Library/Logs/Unity/Editor.log";
|
||||
#else
|
||||
//If you want to build your app without ZFBrowser on some platforms change this to an exception or
|
||||
//tweak the .asmdef
|
||||
#error ZFBrowser is not supported on this platform
|
||||
#endif
|
||||
|
||||
|
||||
return new CEFDirs() {
|
||||
resourcesPath = resourcesPath,
|
||||
binariesPath = platformDir,
|
||||
localesPath = localesDir,
|
||||
subprocessFile = subprocessFile,
|
||||
logFile = logFile,
|
||||
};
|
||||
|
||||
#elif UNITY_STANDALONE_WIN
|
||||
var resourcesPath = Application.dataPath + "/Plugins";
|
||||
|
||||
var logFile = Application.dataPath + "/output_log.txt";
|
||||
#if UNITY_2017_2_OR_NEWER
|
||||
var appLowDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "Low/" + Application.companyName + "/" + Application.productName;
|
||||
if (Directory.Exists(appLowDir)) {
|
||||
logFile = appLowDir + "/output_log.txt";
|
||||
}
|
||||
#endif
|
||||
|
||||
return new CEFDirs() {
|
||||
resourcesPath = resourcesPath,
|
||||
binariesPath = resourcesPath,
|
||||
localesPath = resourcesPath + "/locales",
|
||||
subprocessFile = resourcesPath + "/" + SlaveExecutable + ".exe",
|
||||
logFile = logFile,
|
||||
};
|
||||
#elif UNITY_STANDALONE_LINUX
|
||||
var resourcesPath = Application.dataPath + "/Plugins";
|
||||
return new CEFDirs() {
|
||||
resourcesPath = resourcesPath,
|
||||
binariesPath = resourcesPath,
|
||||
localesPath = resourcesPath + "/locales",
|
||||
subprocessFile = resourcesPath + "/" + SlaveExecutable,
|
||||
logFile = "/dev/null",
|
||||
// Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/unity3d/" +
|
||||
// Application.companyName + "/" + Application.productName + "/Player.log",
|
||||
};
|
||||
#elif UNITY_STANDALONE_OSX
|
||||
return new CEFDirs() {
|
||||
resourcesPath = Application.dataPath + "/Frameworks/Chromium Embedded Framework.framework/Resources",
|
||||
binariesPath = Application.dataPath + "/Plugins",
|
||||
localesPath = Application.dataPath + "/Frameworks/Chromium Embedded Framework.framework/Resources",
|
||||
subprocessFile = Application.dataPath + "/Frameworks/ZFGameBrowser.app/Contents/MacOS/" + SlaveExecutable,
|
||||
logFile = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/Library/Logs/Unity/Player.log",
|
||||
};
|
||||
#else
|
||||
#error Web textures are not supported on this platform
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6fee78664a3055740bb2c20fa9c2d38a
|
||||
timeCreated: 1454001980
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,21 @@
|
|||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Handler for creating new windows. (See Browser.NewWindowAction.NewBrowser)
|
||||
///
|
||||
/// When the browser needs to open a new window, myHandler.CreateBrowser will be called. Create a
|
||||
/// new browser how and where you will, then return it. The new Browser will be filled with
|
||||
/// the new page.
|
||||
///
|
||||
/// Call browser.SetNewWindowHandler(NewWindowAction.NewBrowser, myClass) to have a browser use it.
|
||||
/// </summary>
|
||||
public interface INewWindowHandler {
|
||||
|
||||
/**
|
||||
* Creates a new Browser object to hold a new page.
|
||||
* The returned Browser object will then be linked and load the new page.
|
||||
*/
|
||||
Browser CreateBrowser(Browser parent);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3ee8fedccb68f1a46965dec4d88bd321
|
||||
timeCreated: 1449697159
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,269 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Stand-in class for a JavaScript value that can be one of many different types.
|
||||
*
|
||||
* Bad lookups are safe. That is, if you try to look up something that doesn't exist you will not get an exception,
|
||||
* but an "invalid" node. Use Check() if you want an exception on invalid lookups:
|
||||
*
|
||||
* var node = JSONNode.Parse(@"{""a"":1}");
|
||||
* node["a"].IsValid == true;
|
||||
* node["bob"].IsValid == false
|
||||
* node["bob"]["foo"].IsValid == false //but doesn't throw an exception
|
||||
* node["a"].Check() //okay
|
||||
* node["bob"].Check() //throw exception
|
||||
*
|
||||
* Values can be implicitly converted to JSONNodes and back. That means you don't have to use properties like
|
||||
* "IntValue" and "StringValue". Simply try to use the node as that type and it will convert to the value
|
||||
* if it's that type or return a default value if it isn't:
|
||||
*
|
||||
* var node = JSONNode.Parse(@"{""a"":1, ""b"": ""apples""}");
|
||||
* int a = node["a"];
|
||||
* a == 1;
|
||||
* string b = node["b"];
|
||||
* b == "apples";
|
||||
* string str = node["a"];
|
||||
* str == null; //null is the default value for a string.
|
||||
*
|
||||
* You can also use new JSONNode(value) for many different types, though often it's easier to just assign a
|
||||
* value and let it auto-convert.
|
||||
*
|
||||
* Because they act a little special, use node.IsNull and node.IsValid to check for null and invalid values.
|
||||
* Real null still acts like null, though, so use JSONNode.NullNode to create a "null" JSONNode.
|
||||
* You can also use JSONNode.InvalidNode to get an invalid JSONNode outright.
|
||||
*
|
||||
* Note that, while reading blind is safe, assignment is not. Attempting to assign object keys to an integer, for example,
|
||||
* will throw an exception. To append to an array, call .Add() or assign to -1. To remove an object key or array element,
|
||||
* assign JSONNode.InvalidNode to it.
|
||||
*
|
||||
*/
|
||||
public class JSONNode {
|
||||
/** Parses the given JSON document to a JSONNode. Throws a SerializationException on parse error. */
|
||||
public static JSONNode Parse(string json) {
|
||||
return JSONParser.Parse(json);
|
||||
}
|
||||
|
||||
public static readonly JSONNode InvalidNode = new JSONNode(NodeType.Invalid);
|
||||
public static readonly JSONNode NullNode = new JSONNode(NodeType.Null);
|
||||
|
||||
public enum NodeType {
|
||||
/** Getting this value would result in undefined or ordinarily throw some type of exception trying to fetch it. */
|
||||
Invalid,
|
||||
String,
|
||||
Number,
|
||||
Object,
|
||||
Array,
|
||||
Bool,
|
||||
Null,
|
||||
}
|
||||
|
||||
public NodeType _type = NodeType.Invalid;
|
||||
private string _stringValue;
|
||||
private double _numberValue;
|
||||
private Dictionary<string, JSONNode> _objectValue;
|
||||
private List<JSONNode> _arrayValue;
|
||||
private bool _boolValue;
|
||||
|
||||
public JSONNode(NodeType type = NodeType.Null) {
|
||||
this._type = type;
|
||||
if (type == NodeType.Object) _objectValue = new Dictionary<string, JSONNode>();
|
||||
else if (type == NodeType.Array) _arrayValue = new List<JSONNode>();
|
||||
}
|
||||
|
||||
public JSONNode(string value) {
|
||||
this._type = NodeType.String;
|
||||
_stringValue = value;
|
||||
}
|
||||
|
||||
public JSONNode(double value) {
|
||||
this._type = NodeType.Number;
|
||||
_numberValue = value;
|
||||
}
|
||||
|
||||
public JSONNode(Dictionary<string, JSONNode> value) {
|
||||
this._type = NodeType.Object;
|
||||
_objectValue = value;
|
||||
}
|
||||
|
||||
public JSONNode(List<JSONNode> value) {
|
||||
this._type = NodeType.Array;
|
||||
_arrayValue = value;
|
||||
}
|
||||
|
||||
public JSONNode(bool value) {
|
||||
this._type = NodeType.Bool;
|
||||
_boolValue = value;
|
||||
}
|
||||
|
||||
public NodeType Type { get { return _type; } }
|
||||
|
||||
public bool IsValid {
|
||||
get { return _type != NodeType.Invalid; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is valid. If not, throws an exception.
|
||||
* Returns {this}, which allows you to add this statement inline in you expressions.
|
||||
*
|
||||
* Example:
|
||||
* var node = data["key1"][1].Check();
|
||||
* int val = data["maxSize"].Check()["elements"][3];
|
||||
*/
|
||||
public JSONNode Check() {
|
||||
if (_type == NodeType.Invalid) throw new InvalidJSONNodeException();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static implicit operator string(JSONNode n) {
|
||||
return n._type == NodeType.String ? n._stringValue : null;
|
||||
}
|
||||
public static implicit operator JSONNode(string v) {
|
||||
return new JSONNode(v);
|
||||
}
|
||||
|
||||
public static implicit operator int(JSONNode n) {
|
||||
return n._type == NodeType.Number ? (int)n._numberValue : 0;
|
||||
}
|
||||
public static implicit operator JSONNode(int v) {
|
||||
return new JSONNode(v);
|
||||
}
|
||||
|
||||
public static implicit operator float(JSONNode n) {
|
||||
return n._type == NodeType.Number ? (float)n._numberValue : 0;
|
||||
}
|
||||
public static implicit operator JSONNode(float v) {
|
||||
return new JSONNode(v);
|
||||
}
|
||||
|
||||
public static implicit operator double(JSONNode n) {
|
||||
return n._type == NodeType.Number ? n._numberValue : 0;
|
||||
}
|
||||
public static implicit operator JSONNode(double v) {
|
||||
return new JSONNode(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter/getter for keys on an object. All keys are strings.
|
||||
* Assign JSONNode.InvalidValue to delete a key.
|
||||
*/
|
||||
public JSONNode this[string k] {
|
||||
get {
|
||||
if (_type == NodeType.Object) {
|
||||
JSONNode ret;
|
||||
if (_objectValue.TryGetValue(k, out ret)) return ret;
|
||||
}
|
||||
return InvalidNode;
|
||||
}
|
||||
set {
|
||||
if (_type != NodeType.Object) throw new InvalidJSONNodeException();
|
||||
if (value._type == NodeType.Invalid) _objectValue.Remove(k);
|
||||
else _objectValue[k] = value;
|
||||
}
|
||||
}
|
||||
public static implicit operator Dictionary<string, JSONNode>(JSONNode n) {
|
||||
return n._type == NodeType.Object ? n._objectValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter/getter for indicies on an array.
|
||||
* Assign JSONNode.InvalidValue to delete a key.
|
||||
* Assign to "-1" to append to the end.
|
||||
*/
|
||||
public JSONNode this[int idx] {
|
||||
get {
|
||||
if (_type == NodeType.Array && idx >= 0 && idx < _arrayValue.Count) {
|
||||
return _arrayValue[idx];
|
||||
}
|
||||
return InvalidNode;
|
||||
}
|
||||
set {
|
||||
if (_type != NodeType.Array) throw new InvalidJSONNodeException();
|
||||
|
||||
if (idx == -1) {
|
||||
if (value._type == NodeType.Invalid) {
|
||||
_arrayValue.RemoveAt(_arrayValue.Count - 1);
|
||||
} else {
|
||||
_arrayValue.Add(value);
|
||||
}
|
||||
} else {
|
||||
if (value._type == NodeType.Invalid) {
|
||||
_arrayValue.RemoveAt(idx);
|
||||
} else {
|
||||
_arrayValue[idx] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static implicit operator List<JSONNode>(JSONNode n) {
|
||||
return n._type == NodeType.Array ? n._arrayValue : null;
|
||||
}
|
||||
|
||||
/** Adds an items if the node is an array. */
|
||||
public void Add(JSONNode item) {
|
||||
if (_type != NodeType.Array) throw new InvalidJSONNodeException();
|
||||
_arrayValue.Add(item);
|
||||
}
|
||||
|
||||
/** If we are an array or object, returns the size, otherwise returns 0. */
|
||||
public int Count {
|
||||
get {
|
||||
switch (_type) {
|
||||
case NodeType.Array: return _arrayValue.Count;
|
||||
case NodeType.Object: return _objectValue.Count;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** True if the value of this node is exactly null. */
|
||||
public bool IsNull {
|
||||
get { return _type == NodeType.Null; }
|
||||
}
|
||||
|
||||
public static implicit operator bool(JSONNode n) {
|
||||
return n._type == NodeType.Bool ? n._boolValue : false;
|
||||
}
|
||||
public static implicit operator JSONNode(bool v) {
|
||||
return new JSONNode(v);
|
||||
}
|
||||
|
||||
/** Returns a native value representation of our value. */
|
||||
public object Value {
|
||||
get {
|
||||
switch (_type) {
|
||||
case NodeType.Invalid:
|
||||
Check();
|
||||
return null;//we don't get here.
|
||||
case NodeType.String:
|
||||
return _stringValue;
|
||||
case NodeType.Number:
|
||||
return _numberValue;
|
||||
case NodeType.Object:
|
||||
return _objectValue;
|
||||
case NodeType.Array:
|
||||
return _arrayValue;
|
||||
case NodeType.Bool:
|
||||
return _boolValue;
|
||||
case NodeType.Null:
|
||||
return null;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Serializes the JSON node and returns a JSON string. */
|
||||
public string AsJSON {
|
||||
get {
|
||||
return JSONParser.Serialize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidJSONNodeException : Exception {}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0ae4f653ec4655d47b160575a231ddc9
|
||||
timeCreated: 1447780609
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,541 @@
|
|||
/*
|
||||
* This is a customized parser, based on "SimpleJson.cs", for JSONNode
|
||||
*
|
||||
* SimpleJson.cs is open source software and this entire file may be dealt with as described below:
|
||||
*/
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SimpleJson.cs" company="The Outercurve Foundation">
|
||||
// Copyright (c) 2011, The Outercurve Foundation, 2015 Zen Fulcrum LLC
|
||||
//
|
||||
// Licensed under the MIT License (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
// 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.
|
||||
// </copyright>
|
||||
// <author>Nathan Totten (ntotten.com), Jim Zimmerman (jimzimmerman.com) and Prabir Shrestha (prabir.me)</author>
|
||||
// <website>https://github.com/facebook-csharp-sdk/simple-json</website>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
/// <summary>
|
||||
/// This class encodes and decodes JSON strings.
|
||||
/// Spec. details, see http://www.json.org/
|
||||
///
|
||||
/// JSON uses Arrays and Objects. These correspond here to the datatypes JsonArray(IList<object>) and JsonObject(IDictionary<string,object>).
|
||||
/// All numbers are parsed to doubles.
|
||||
/// </summary>
|
||||
internal static class JSONParser {
|
||||
private const int TOKEN_NONE = 0;
|
||||
private const int TOKEN_CURLY_OPEN = 1;
|
||||
private const int TOKEN_CURLY_CLOSE = 2;
|
||||
private const int TOKEN_SQUARED_OPEN = 3;
|
||||
private const int TOKEN_SQUARED_CLOSE = 4;
|
||||
private const int TOKEN_COLON = 5;
|
||||
private const int TOKEN_COMMA = 6;
|
||||
private const int TOKEN_STRING = 7;
|
||||
private const int TOKEN_NUMBER = 8;
|
||||
private const int TOKEN_TRUE = 9;
|
||||
private const int TOKEN_FALSE = 10;
|
||||
private const int TOKEN_NULL = 11;
|
||||
private const int BUILDER_CAPACITY = 2000;
|
||||
private static readonly char[] EscapeTable;
|
||||
private static readonly char[] EscapeCharacters = new char[] { '"', '\\', '\b', '\f', '\n', '\r', '\t' };
|
||||
// private static readonly string EscapeCharactersString = new string(EscapeCharacters);
|
||||
static JSONParser() {
|
||||
EscapeTable = new char[93];
|
||||
EscapeTable['"'] = '"';
|
||||
EscapeTable['\\'] = '\\';
|
||||
EscapeTable['\b'] = 'b';
|
||||
EscapeTable['\f'] = 'f';
|
||||
EscapeTable['\n'] = 'n';
|
||||
EscapeTable['\r'] = 'r';
|
||||
EscapeTable['\t'] = 't';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the string json into a value
|
||||
/// </summary>
|
||||
/// <param name="json">A JSON string.</param>
|
||||
/// <returns>An IList<object>, a IDictionary<string,object>, a double, a string, null, true, or false</returns>
|
||||
public static JSONNode Parse(string json) {
|
||||
JSONNode obj;
|
||||
if (TryDeserializeObject(json, out obj))
|
||||
return obj;
|
||||
throw new SerializationException("Invalid JSON string");
|
||||
}
|
||||
/// <summary>
|
||||
/// Try parsing the json string into a value.
|
||||
/// </summary>
|
||||
/// <param name="json">
|
||||
/// A JSON string.
|
||||
/// </param>
|
||||
/// <param name="obj">
|
||||
/// The object.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns true if successfull otherwise false.
|
||||
/// </returns>
|
||||
public static bool TryDeserializeObject(string json, out JSONNode obj) {
|
||||
bool success = true;
|
||||
if (json != null) {
|
||||
char[] charArray = json.ToCharArray();
|
||||
int index = 0;
|
||||
obj = ParseValue(charArray, ref index, ref success);
|
||||
} else
|
||||
obj = null;
|
||||
return success;
|
||||
}
|
||||
|
||||
public static string EscapeToJavascriptString(string jsonString) {
|
||||
if (string.IsNullOrEmpty(jsonString))
|
||||
return jsonString;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char c;
|
||||
for (int i = 0; i < jsonString.Length; ) {
|
||||
c = jsonString[i++];
|
||||
if (c == '\\') {
|
||||
int remainingLength = jsonString.Length - i;
|
||||
if (remainingLength >= 2) {
|
||||
char lookahead = jsonString[i];
|
||||
if (lookahead == '\\') {
|
||||
sb.Append('\\');
|
||||
++i;
|
||||
} else if (lookahead == '"') {
|
||||
sb.Append("\"");
|
||||
++i;
|
||||
} else if (lookahead == 't') {
|
||||
sb.Append('\t');
|
||||
++i;
|
||||
} else if (lookahead == 'b') {
|
||||
sb.Append('\b');
|
||||
++i;
|
||||
} else if (lookahead == 'n') {
|
||||
sb.Append('\n');
|
||||
++i;
|
||||
} else if (lookahead == 'r') {
|
||||
sb.Append('\r');
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static JSONNode ParseObject(char[] json, ref int index, ref bool success) {
|
||||
JSONNode table = new JSONNode(JSONNode.NodeType.Object);
|
||||
int token;
|
||||
// {
|
||||
NextToken(json, ref index);
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
token = LookAhead(json, index);
|
||||
if (token == TOKEN_NONE) {
|
||||
success = false;
|
||||
return null;
|
||||
} else if (token == TOKEN_COMMA)
|
||||
NextToken(json, ref index);
|
||||
else if (token == TOKEN_CURLY_CLOSE) {
|
||||
NextToken(json, ref index);
|
||||
return table;
|
||||
} else {
|
||||
// name
|
||||
string name = ParseString(json, ref index, ref success);
|
||||
if (!success) {
|
||||
success = false;
|
||||
return null;
|
||||
}
|
||||
// :
|
||||
token = NextToken(json, ref index);
|
||||
if (token != TOKEN_COLON) {
|
||||
success = false;
|
||||
return null;
|
||||
}
|
||||
// value
|
||||
JSONNode value = ParseValue(json, ref index, ref success);
|
||||
if (!success) {
|
||||
success = false;
|
||||
return null;
|
||||
}
|
||||
table[name] = value;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
static JSONNode ParseArray(char[] json, ref int index, ref bool success) {
|
||||
JSONNode array = new JSONNode(JSONNode.NodeType.Array);
|
||||
// [
|
||||
NextToken(json, ref index);
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
int token = LookAhead(json, index);
|
||||
if (token == TOKEN_NONE) {
|
||||
success = false;
|
||||
return null;
|
||||
} else if (token == TOKEN_COMMA)
|
||||
NextToken(json, ref index);
|
||||
else if (token == TOKEN_SQUARED_CLOSE) {
|
||||
NextToken(json, ref index);
|
||||
break;
|
||||
} else {
|
||||
JSONNode value = ParseValue(json, ref index, ref success);
|
||||
if (!success)
|
||||
return null;
|
||||
array.Add(value);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static JSONNode ParseValue(char[] json, ref int index, ref bool success) {
|
||||
switch (LookAhead(json, index)) {
|
||||
case TOKEN_STRING:
|
||||
return ParseString(json, ref index, ref success);
|
||||
case TOKEN_NUMBER:
|
||||
return ParseNumber(json, ref index, ref success);
|
||||
case TOKEN_CURLY_OPEN:
|
||||
return ParseObject(json, ref index, ref success);
|
||||
case TOKEN_SQUARED_OPEN:
|
||||
return ParseArray(json, ref index, ref success);
|
||||
case TOKEN_TRUE:
|
||||
NextToken(json, ref index);
|
||||
return true;
|
||||
case TOKEN_FALSE:
|
||||
NextToken(json, ref index);
|
||||
return false;
|
||||
case TOKEN_NULL:
|
||||
NextToken(json, ref index);
|
||||
return JSONNode.NullNode;
|
||||
case TOKEN_NONE:
|
||||
break;
|
||||
}
|
||||
success = false;
|
||||
return JSONNode.InvalidNode;
|
||||
}
|
||||
|
||||
static JSONNode ParseString(char[] json, ref int index, ref bool success) {
|
||||
StringBuilder s = new StringBuilder(BUILDER_CAPACITY);
|
||||
char c;
|
||||
EatWhitespace(json, ref index);
|
||||
// "
|
||||
c = json[index++];
|
||||
bool complete = false;
|
||||
while (!complete) {
|
||||
if (index == json.Length)
|
||||
break;
|
||||
c = json[index++];
|
||||
if (c == '"') {
|
||||
complete = true;
|
||||
break;
|
||||
} else if (c == '\\') {
|
||||
if (index == json.Length)
|
||||
break;
|
||||
c = json[index++];
|
||||
if (c == '"')
|
||||
s.Append('"');
|
||||
else if (c == '\\')
|
||||
s.Append('\\');
|
||||
else if (c == '/')
|
||||
s.Append('/');
|
||||
else if (c == 'b')
|
||||
s.Append('\b');
|
||||
else if (c == 'f')
|
||||
s.Append('\f');
|
||||
else if (c == 'n')
|
||||
s.Append('\n');
|
||||
else if (c == 'r')
|
||||
s.Append('\r');
|
||||
else if (c == 't')
|
||||
s.Append('\t');
|
||||
else if (c == 'u') {
|
||||
int remainingLength = json.Length - index;
|
||||
if (remainingLength >= 4) {
|
||||
// parse the 32 bit hex into an integer codepoint
|
||||
uint codePoint;
|
||||
if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint)))
|
||||
return "";
|
||||
// convert the integer codepoint to a unicode char and add to string
|
||||
if (0xD800 <= codePoint && codePoint <= 0xDBFF) // if high surrogate
|
||||
{
|
||||
index += 4; // skip 4 chars
|
||||
remainingLength = json.Length - index;
|
||||
if (remainingLength >= 6) {
|
||||
uint lowCodePoint;
|
||||
if (new string(json, index, 2) == "\\u" && UInt32.TryParse(new string(json, index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out lowCodePoint)) {
|
||||
if (0xDC00 <= lowCodePoint && lowCodePoint <= 0xDFFF) // if low surrogate
|
||||
{
|
||||
s.Append((char)codePoint);
|
||||
s.Append((char)lowCodePoint);
|
||||
index += 6; // skip 6 chars
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
success = false; // invalid surrogate pair
|
||||
return "";
|
||||
}
|
||||
s.Append(ConvertFromUtf32((int)codePoint));
|
||||
// skip 4 chars
|
||||
index += 4;
|
||||
} else
|
||||
break;
|
||||
}
|
||||
} else
|
||||
s.Append(c);
|
||||
}
|
||||
if (!complete) {
|
||||
success = false;
|
||||
return null;
|
||||
}
|
||||
return s.ToString();
|
||||
}
|
||||
|
||||
private static string ConvertFromUtf32(int utf32) {
|
||||
// http://www.java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/System/System/Char.cs.htm
|
||||
if (utf32 < 0 || utf32 > 0x10FFFF)
|
||||
throw new ArgumentOutOfRangeException("utf32", "The argument must be from 0 to 0x10FFFF.");
|
||||
if (0xD800 <= utf32 && utf32 <= 0xDFFF)
|
||||
throw new ArgumentOutOfRangeException("utf32", "The argument must not be in surrogate pair range.");
|
||||
if (utf32 < 0x10000)
|
||||
return new string((char)utf32, 1);
|
||||
utf32 -= 0x10000;
|
||||
return new string(new char[] { (char)((utf32 >> 10) + 0xD800), (char)(utf32 % 0x0400 + 0xDC00) });
|
||||
}
|
||||
|
||||
static JSONNode ParseNumber(char[] json, ref int index, ref bool success) {
|
||||
EatWhitespace(json, ref index);
|
||||
int lastIndex = GetLastIndexOfNumber(json, index);
|
||||
int charLength = (lastIndex - index) + 1;
|
||||
JSONNode returnNumber;
|
||||
string str = new string(json, index, charLength);
|
||||
if (str.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || str.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) {
|
||||
double number;
|
||||
success = double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number);
|
||||
returnNumber = number;
|
||||
} else {
|
||||
long number;
|
||||
success = long.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number);
|
||||
returnNumber = number;
|
||||
}
|
||||
index = lastIndex + 1;
|
||||
return returnNumber;
|
||||
}
|
||||
|
||||
static int GetLastIndexOfNumber(char[] json, int index) {
|
||||
int lastIndex;
|
||||
for (lastIndex = index; lastIndex < json.Length; lastIndex++)
|
||||
if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) break;
|
||||
return lastIndex - 1;
|
||||
}
|
||||
|
||||
static void EatWhitespace(char[] json, ref int index) {
|
||||
for (; index < json.Length; index++)
|
||||
if (" \t\n\r\b\f".IndexOf(json[index]) == -1) break;
|
||||
}
|
||||
static int LookAhead(char[] json, int index) {
|
||||
int saveIndex = index;
|
||||
return NextToken(json, ref saveIndex);
|
||||
}
|
||||
|
||||
static int NextToken(char[] json, ref int index) {
|
||||
EatWhitespace(json, ref index);
|
||||
if (index == json.Length)
|
||||
return TOKEN_NONE;
|
||||
char c = json[index];
|
||||
index++;
|
||||
switch (c) {
|
||||
case '{':
|
||||
return TOKEN_CURLY_OPEN;
|
||||
case '}':
|
||||
return TOKEN_CURLY_CLOSE;
|
||||
case '[':
|
||||
return TOKEN_SQUARED_OPEN;
|
||||
case ']':
|
||||
return TOKEN_SQUARED_CLOSE;
|
||||
case ',':
|
||||
return TOKEN_COMMA;
|
||||
case '"':
|
||||
return TOKEN_STRING;
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
case '-':
|
||||
return TOKEN_NUMBER;
|
||||
case ':':
|
||||
return TOKEN_COLON;
|
||||
}
|
||||
index--;
|
||||
int remainingLength = json.Length - index;
|
||||
// false
|
||||
if (remainingLength >= 5) {
|
||||
if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') {
|
||||
index += 5;
|
||||
return TOKEN_FALSE;
|
||||
}
|
||||
}
|
||||
// true
|
||||
if (remainingLength >= 4) {
|
||||
if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') {
|
||||
index += 4;
|
||||
return TOKEN_TRUE;
|
||||
}
|
||||
}
|
||||
// null
|
||||
if (remainingLength >= 4) {
|
||||
if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') {
|
||||
index += 4;
|
||||
return TOKEN_NULL;
|
||||
}
|
||||
}
|
||||
return TOKEN_NONE;
|
||||
}
|
||||
|
||||
public static string Serialize(JSONNode node) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var success = SerializeValue(node, sb);
|
||||
if (!success) throw new SerializationException("Failed to serialize JSON");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static bool SerializeValue(JSONNode value, StringBuilder builder) {
|
||||
bool success = true;
|
||||
|
||||
// if (value == null) {
|
||||
// builder.Append("null");
|
||||
// return success;
|
||||
// }
|
||||
|
||||
switch (value.Type) {
|
||||
case JSONNode.NodeType.String:
|
||||
success = SerializeString(value, builder);
|
||||
break;
|
||||
case JSONNode.NodeType.Object: {
|
||||
Dictionary<String, JSONNode> dict = value;
|
||||
success = SerializeObject(dict.Keys, dict.Values, builder);
|
||||
break;
|
||||
}
|
||||
case JSONNode.NodeType.Array:
|
||||
success = SerializeArray((List<JSONNode>)value, builder);
|
||||
break;
|
||||
case JSONNode.NodeType.Number:
|
||||
success = SerializeNumber(value, builder);
|
||||
break;
|
||||
case JSONNode.NodeType.Bool:
|
||||
builder.Append(value ? "true" : "false");
|
||||
break;
|
||||
case JSONNode.NodeType.Null:
|
||||
builder.Append("null");
|
||||
break;
|
||||
case JSONNode.NodeType.Invalid:
|
||||
throw new SerializationException("Cannot serialize invalid JSONNode");
|
||||
default:
|
||||
throw new SerializationException("Unknown JSONNode type");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool SerializeObject(IEnumerable<string> keys, IEnumerable<JSONNode> values, StringBuilder builder) {
|
||||
builder.Append("{");
|
||||
var ke = keys.GetEnumerator();
|
||||
var ve = values.GetEnumerator();
|
||||
bool first = true;
|
||||
while (ke.MoveNext() && ve.MoveNext()) {
|
||||
var key = ke.Current;
|
||||
var value = ve.Current;
|
||||
if (!first)
|
||||
builder.Append(",");
|
||||
string stringKey = key;
|
||||
if (stringKey != null)
|
||||
SerializeString(stringKey, builder);
|
||||
else
|
||||
if (!SerializeValue(value, builder)) return false;
|
||||
builder.Append(":");
|
||||
if (!SerializeValue(value, builder))
|
||||
return false;
|
||||
first = false;
|
||||
}
|
||||
builder.Append("}");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SerializeArray(IEnumerable<JSONNode> anArray, StringBuilder builder) {
|
||||
builder.Append("[");
|
||||
bool first = true;
|
||||
foreach (var value in anArray) {
|
||||
if (!first)
|
||||
builder.Append(",");
|
||||
if (!SerializeValue(value, builder))
|
||||
return false;
|
||||
first = false;
|
||||
}
|
||||
builder.Append("]");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SerializeString(string aString, StringBuilder builder) {
|
||||
// Happy path if there's nothing to be escaped. IndexOfAny is highly optimized (and unmanaged)
|
||||
if (aString.IndexOfAny(EscapeCharacters) == -1) {
|
||||
builder.Append('"');
|
||||
builder.Append(aString);
|
||||
builder.Append('"');
|
||||
return true;
|
||||
}
|
||||
builder.Append('"');
|
||||
int safeCharacterCount = 0;
|
||||
char[] charArray = aString.ToCharArray();
|
||||
for (int i = 0; i < charArray.Length; i++) {
|
||||
char c = charArray[i];
|
||||
// Non ascii characters are fine, buffer them up and send them to the builder
|
||||
// in larger chunks if possible. The escape table is a 1:1 translation table
|
||||
// with \0 [default(char)] denoting a safe character.
|
||||
if (c >= EscapeTable.Length || EscapeTable[c] == default(char)) {
|
||||
safeCharacterCount++;
|
||||
} else {
|
||||
if (safeCharacterCount > 0) {
|
||||
builder.Append(charArray, i - safeCharacterCount, safeCharacterCount);
|
||||
safeCharacterCount = 0;
|
||||
}
|
||||
builder.Append('\\');
|
||||
builder.Append(EscapeTable[c]);
|
||||
}
|
||||
}
|
||||
if (safeCharacterCount > 0) {
|
||||
builder.Append(charArray, charArray.Length - safeCharacterCount, safeCharacterCount);
|
||||
}
|
||||
builder.Append('"');
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SerializeNumber(double number, StringBuilder builder) {
|
||||
builder.Append(Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture));
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8cb978759cbdf48468bc42a00fe0e849
|
||||
timeCreated: 1447780610
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,156 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
static class KeyMappings {
|
||||
private static Dictionary<KeyCode, int> mappings = new Dictionary<KeyCode, int>() {
|
||||
//I'm not gonna lie. I just opened http://www.w3.org/2002/09/tests/keys.html
|
||||
//and copied down the values my keyboard produced. >_<
|
||||
{KeyCode.Escape, 27},
|
||||
{KeyCode.F1, 112},
|
||||
{KeyCode.F2, 113},
|
||||
{KeyCode.F3, 114},
|
||||
{KeyCode.F4, 115},
|
||||
{KeyCode.F5, 116},
|
||||
{KeyCode.F6, 117},
|
||||
{KeyCode.F7, 118},
|
||||
{KeyCode.F8, 119},
|
||||
{KeyCode.F9, 120},
|
||||
{KeyCode.F10, 121},
|
||||
{KeyCode.F11, 122},
|
||||
{KeyCode.F12, 123},
|
||||
{KeyCode.SysReq, 44}, {KeyCode.Print, 44},
|
||||
{KeyCode.ScrollLock, 145},
|
||||
{KeyCode.Pause, 19},
|
||||
{KeyCode.BackQuote, 192},
|
||||
|
||||
|
||||
{KeyCode.Alpha0, 48},
|
||||
{KeyCode.Alpha1, 49},
|
||||
{KeyCode.Alpha2, 50},
|
||||
{KeyCode.Alpha3, 51},
|
||||
{KeyCode.Alpha4, 52},
|
||||
{KeyCode.Alpha5, 53},
|
||||
{KeyCode.Alpha6, 54},
|
||||
{KeyCode.Alpha7, 55},
|
||||
{KeyCode.Alpha8, 56},
|
||||
{KeyCode.Alpha9, 57},
|
||||
{KeyCode.Minus, 189},
|
||||
{KeyCode.Equals, 187},
|
||||
{KeyCode.Backspace, 8},
|
||||
|
||||
{KeyCode.Tab, 9},
|
||||
//char keys
|
||||
{KeyCode.LeftBracket, 219},
|
||||
{KeyCode.RightBracket, 221},
|
||||
{KeyCode.Backslash, 220},
|
||||
|
||||
{KeyCode.CapsLock, 20},
|
||||
//char keys
|
||||
{KeyCode.Semicolon, 186},
|
||||
{KeyCode.Quote, 222},
|
||||
{KeyCode.Return, 13},
|
||||
|
||||
{KeyCode.LeftShift, 16},
|
||||
//char keys
|
||||
{KeyCode.Comma, 188},
|
||||
{KeyCode.Period, 190},
|
||||
{KeyCode.Slash, 191},
|
||||
{KeyCode.RightShift, 16},
|
||||
|
||||
{KeyCode.LeftControl, 17},
|
||||
{KeyCode.LeftCommand, 91}, {KeyCode.LeftWindows, 91},
|
||||
{KeyCode.LeftAlt, 18},
|
||||
{KeyCode.Space, 32},
|
||||
{KeyCode.RightAlt, 18},
|
||||
{KeyCode.RightCommand, 92}, {KeyCode.RightWindows, 92},
|
||||
{KeyCode.Menu, 93},
|
||||
{KeyCode.RightControl, 17},
|
||||
|
||||
|
||||
{KeyCode.Insert, 45},
|
||||
{KeyCode.Home, 36},
|
||||
{KeyCode.PageUp, 33},
|
||||
|
||||
{KeyCode.Delete, 46},
|
||||
{KeyCode.End, 35},
|
||||
{KeyCode.PageDown, 34},
|
||||
|
||||
{KeyCode.UpArrow, 38},
|
||||
{KeyCode.LeftArrow, 37},
|
||||
{KeyCode.DownArrow, 40},
|
||||
{KeyCode.RightArrow, 39},
|
||||
|
||||
|
||||
{KeyCode.Numlock, 144},
|
||||
{KeyCode.KeypadDivide, 111},
|
||||
{KeyCode.KeypadMultiply, 106},
|
||||
{KeyCode.KeypadMinus, 109},
|
||||
|
||||
{KeyCode.Keypad7, 103},
|
||||
{KeyCode.Keypad8, 104},
|
||||
{KeyCode.Keypad9, 105},
|
||||
{KeyCode.KeypadPlus, 107},
|
||||
|
||||
{KeyCode.Keypad4, 100},
|
||||
{KeyCode.Keypad5, 101},
|
||||
{KeyCode.Keypad6, 102},
|
||||
|
||||
{KeyCode.Keypad1, 97},
|
||||
{KeyCode.Keypad2, 98},
|
||||
{KeyCode.Keypad3, 99},
|
||||
{KeyCode.KeypadEnter, 13},
|
||||
|
||||
{KeyCode.Keypad0, 96},
|
||||
{KeyCode.KeypadPeriod, 110},
|
||||
};
|
||||
|
||||
private static Dictionary<int, KeyCode> reverseMappings = new Dictionary<int, KeyCode>();
|
||||
|
||||
static KeyMappings() {
|
||||
foreach (var kvp in mappings) {
|
||||
reverseMappings[kvp.Value] = kvp.Key;
|
||||
}
|
||||
|
||||
for (int i = (int)KeyCode.A; i <= (int)KeyCode.Z; i++) {
|
||||
var key = (KeyCode)i;
|
||||
var keyCode = i - (int)KeyCode.A + 65;
|
||||
mappings[key] = keyCode;
|
||||
reverseMappings[keyCode] = key;
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetWindowsKeyCode(Event ev) {
|
||||
int ukc = (int)ev.keyCode;//unity key code
|
||||
|
||||
//When dealing with characters return the Unicode char as the keycode.
|
||||
if (ukc == 0) {
|
||||
//enter is special.
|
||||
if (ev.character == 10) return 13;
|
||||
|
||||
return ev.character;
|
||||
}
|
||||
|
||||
// if (ukc >= (int)KeyCode.A && ukc <= (int)KeyCode.Z) {
|
||||
// return ukc - (int)KeyCode.A + 65;
|
||||
// }
|
||||
|
||||
int ret;
|
||||
if (mappings.TryGetValue(ev.keyCode, out ret)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
//Don't recognize it, we'll just have to return something, but it's almost sure to be wrong.
|
||||
Debug.LogWarning("Unknown key mapping: " + ev);
|
||||
return ukc;
|
||||
}
|
||||
|
||||
public static KeyCode GetUnityKeyCode(int windowsKeyCode) {
|
||||
KeyCode ret;
|
||||
if (reverseMappings.TryGetValue(windowsKeyCode, out ret)) return ret;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: de63d991f0c0f0b41a32bebdc64b329a
|
||||
timeCreated: 1447440809
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,203 @@
|
|||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/// <summary>
|
||||
/// Manages a browser that's been opened in an OS-native window outside the game's main window.
|
||||
/// (Presently only used for OS X.)
|
||||
/// </summary>
|
||||
public class PopUpBrowser : MonoBehaviour, IBrowserUI {
|
||||
private int windowId;
|
||||
private int browserId;
|
||||
|
||||
private List<string> messages = new List<string>();
|
||||
private Browser browser;
|
||||
private BrowserNative.WindowCallbackFunc callbackRef;//don't let this get GC'd
|
||||
private Vector2 delayedResize;
|
||||
|
||||
public static void Create(int possibleBrowserId) {
|
||||
var go = new GameObject("Pop up browser");
|
||||
var browser = go.AddComponent<Browser>();
|
||||
browser.RequestNativeBrowser(possibleBrowserId);
|
||||
|
||||
var pop = go.AddComponent<PopUpBrowser>();
|
||||
pop.callbackRef = pop.HandleWindowMessage;
|
||||
pop.windowId = BrowserNative.zfb_windowCreate("", pop.callbackRef);
|
||||
pop.browserId = possibleBrowserId;
|
||||
pop.BrowserCursor = new BrowserCursor();
|
||||
pop.KeyEvents = new List<Event>();
|
||||
pop.browser = browser;
|
||||
|
||||
pop.StartCoroutine(pop.FixFocus());
|
||||
|
||||
pop.InputSettings = new BrowserInputSettings();
|
||||
|
||||
browser.UIHandler = pop;
|
||||
browser.EnableRendering = false;//rendering is done differently, so don't run the usual code
|
||||
}
|
||||
|
||||
private void HandleWindowMessage(int windowId, IntPtr data) {
|
||||
var msgJSON = Util.PtrToStringUTF8(data);
|
||||
//if (!msgJSON.Contains("mouseMove")) Debug.Log("Window message: " + msgJSON);
|
||||
|
||||
lock (messages) messages.Add(msgJSON);
|
||||
}
|
||||
|
||||
public void OnDestroy() {
|
||||
if (!BrowserNative.SymbolsLoaded) return;
|
||||
BrowserNative.zfb_windowClose(windowId);
|
||||
}
|
||||
|
||||
|
||||
private IEnumerator FixFocus() {
|
||||
//OS X: Magically, new browser windows that are focused don't think they are focused, even though we told them so.
|
||||
//Hack workaround.
|
||||
yield return null;
|
||||
BrowserNative.zfb_setFocused(browserId, false);
|
||||
yield return null;
|
||||
BrowserNative.zfb_setFocused(browserId, true);
|
||||
yield return null;
|
||||
BrowserNative.zfb_setFocused(browserId, KeyboardHasFocus);
|
||||
}
|
||||
|
||||
public void InputUpdate() {
|
||||
MouseScroll = Vector2.zero;
|
||||
KeyEvents.Clear();
|
||||
delayedResize = new Vector2(float.NaN, float.NaN);
|
||||
|
||||
lock (messages) {
|
||||
for (int i = 0; i < messages.Count; i++) {
|
||||
HandleMessage(messages[i]);
|
||||
}
|
||||
messages.Clear();
|
||||
}
|
||||
|
||||
if (!float.IsNaN(delayedResize.x)) {
|
||||
browser.Resize((int)delayedResize.x, (int)delayedResize.y);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMessage(string message) {
|
||||
var msg = JSONNode.Parse(message);
|
||||
|
||||
|
||||
switch ((string)msg["type"]) {
|
||||
case "mouseDown":
|
||||
case "mouseUp": {
|
||||
int button = msg["button"];
|
||||
MouseButton flag = 0;
|
||||
if (button == 0) flag = MouseButton.Left;
|
||||
else if (button == 1) flag = MouseButton.Right;
|
||||
else if (button == 2) flag = MouseButton.Middle;
|
||||
|
||||
if (msg["type"] == "mouseDown") MouseButtons |= flag;
|
||||
else MouseButtons &= ~flag;
|
||||
|
||||
break;
|
||||
}
|
||||
case "mouseMove": {
|
||||
var screenPos = new Vector2(msg["x"], msg["y"]);
|
||||
screenPos.x = screenPos.x / browser.Size.x;
|
||||
screenPos.y = screenPos.y / browser.Size.y;
|
||||
MousePosition = screenPos;
|
||||
//Debug.Log("mouse now at " + screenPos);
|
||||
break;
|
||||
}
|
||||
case "mouseFocus":
|
||||
MouseHasFocus = msg["focus"];
|
||||
break;
|
||||
case "keyboardFocus":
|
||||
KeyboardHasFocus = msg["focus"];
|
||||
break;
|
||||
case "mouseScroll": {
|
||||
const float mult = 1 / 3f;
|
||||
MouseScroll += new Vector2(msg["x"], msg["y"]) * mult;
|
||||
break;
|
||||
}
|
||||
case "keyDown":
|
||||
case "keyUp": {
|
||||
var ev = new Event();
|
||||
ev.type = msg["type"] == "keyDown" ? EventType.KeyDown : EventType.KeyUp;
|
||||
ev.character = (char)0;
|
||||
ev.keyCode = KeyMappings.GetUnityKeyCode(msg["code"]);
|
||||
SetMods(ev);
|
||||
|
||||
//Debug.Log("Convert wkc " + (int)msg["code"] + " to ukc " + ev.keyCode);
|
||||
|
||||
KeyEvents.Add(ev);
|
||||
break;
|
||||
}
|
||||
case "keyPress": {
|
||||
string characters = msg["characters"];
|
||||
foreach (char c in characters) {
|
||||
var ev = new Event();
|
||||
ev.type = EventType.KeyDown;
|
||||
SetMods(ev);
|
||||
ev.character = c;
|
||||
ev.keyCode = 0;
|
||||
|
||||
KeyEvents.Add(ev);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "resize":
|
||||
//on OS X (editor at least), resizing hangs the update loop, so we suddenly end up with a bajillion resize
|
||||
//messages we were unable to process. Just record it here and when we've processed everything we'll resize
|
||||
delayedResize.x = msg["w"];
|
||||
delayedResize.y = msg["h"];
|
||||
break;
|
||||
case "close":
|
||||
Destroy(gameObject);
|
||||
break;
|
||||
default:
|
||||
Debug.LogWarning("Unknown window event: " + msg.AsJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool shiftDown, controlDown, altDown, commandDown;
|
||||
private void SetMods(Event ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.LeftShift: case KeyCode.RightShift:
|
||||
shiftDown = ev.type == EventType.KeyDown;
|
||||
break;
|
||||
case KeyCode.LeftControl: case KeyCode.RightControl:
|
||||
controlDown = ev.type == EventType.KeyDown;
|
||||
break;
|
||||
case KeyCode.LeftAlt: case KeyCode.RightAlt:
|
||||
altDown = ev.type == EventType.KeyDown;
|
||||
break;
|
||||
case KeyCode.LeftCommand: case KeyCode.RightCommand:
|
||||
case KeyCode.LeftWindows: case KeyCode.RightWindows:
|
||||
commandDown = ev.type == EventType.KeyDown;
|
||||
break;
|
||||
}
|
||||
|
||||
ev.shift = shiftDown;
|
||||
ev.control = controlDown;
|
||||
ev.alt = altDown;
|
||||
ev.command = commandDown;
|
||||
}
|
||||
|
||||
public void Update() {
|
||||
if (!BrowserNative.SymbolsLoaded) return;
|
||||
BrowserNative.zfb_windowRender(windowId, browserId);
|
||||
}
|
||||
|
||||
public bool MouseHasFocus { get; private set; }
|
||||
public Vector2 MousePosition { get; private set; }
|
||||
public MouseButton MouseButtons { get; private set; }
|
||||
public Vector2 MouseScroll { get; private set; }
|
||||
public bool KeyboardHasFocus { get; private set; }
|
||||
public List<Event> KeyEvents { get; private set; }
|
||||
public BrowserCursor BrowserCursor { get; private set; }
|
||||
public BrowserInputSettings InputSettings { get; private set; }
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6172fca72ca9adf4da2ac4b4e3136708
|
||||
timeCreated: 1517329102
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 48b908734323b95409dfd20950349e02
|
||||
folderAsset: yes
|
||||
timeCreated: 1479581013
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser.Promises
|
||||
{
|
||||
/// <summary>
|
||||
/// General extensions to LINQ.
|
||||
/// </summary>
|
||||
public static class EnumerableExt
|
||||
{
|
||||
public static IEnumerable<T> Empty<T>()
|
||||
{
|
||||
return new T[0];
|
||||
}
|
||||
|
||||
public static IEnumerable<T> LazyEach<T>(this IEnumerable<T> source, Action<T> fn)
|
||||
{
|
||||
foreach (var item in source)
|
||||
{
|
||||
fn.Invoke(item);
|
||||
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Each<T>(this IEnumerable<T> source, Action<T> fn)
|
||||
{
|
||||
foreach (var item in source)
|
||||
{
|
||||
fn.Invoke(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Each<T>(this IEnumerable<T> source, Action<T, int> fn)
|
||||
{
|
||||
int index = 0;
|
||||
|
||||
foreach (T item in source)
|
||||
{
|
||||
fn.Invoke(item, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b77f44a328c5e2c40ba15d0fc85741fc
|
||||
timeCreated: 1479581013
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,842 @@
|
|||
using ZenFulcrum.EmbeddedBrowser.Promises;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a C# promise.
|
||||
/// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
///
|
||||
/// This can also be waited on in a Unity coroutine and queried for its value.
|
||||
/// </summary>
|
||||
public interface IPromise<PromisedT>
|
||||
{
|
||||
/// <summary>
|
||||
/// Set the name of the promise, useful for debugging.
|
||||
/// </summary>
|
||||
IPromise<PromisedT> WithName(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// onRejected is called on error.
|
||||
/// </summary>
|
||||
void Done(Action<PromisedT> onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// Adds a default error handler.
|
||||
/// </summary>
|
||||
void Done(Action<PromisedT> onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Complete the promise. Adds a default error handler.
|
||||
/// </summary>
|
||||
void Done();
|
||||
|
||||
/// <summary>
|
||||
/// Handle errors for the promise.
|
||||
/// </summary>
|
||||
IPromise<PromisedT> Catch(Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a non-value promise.
|
||||
/// </summary>
|
||||
IPromise Then(Func<PromisedT, IPromise> onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback.
|
||||
/// </summary>
|
||||
IPromise<PromisedT> Then(Action<PromisedT> onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a non-value promise.
|
||||
/// </summary>
|
||||
IPromise Then(Func<PromisedT, IPromise> onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// </summary>
|
||||
IPromise<PromisedT> Then(Action<PromisedT> onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Return a new promise with a different value.
|
||||
/// May also change the type of the value.
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, ConvertedT> transform);
|
||||
|
||||
/// <summary>
|
||||
/// Return a new promise with a different value.
|
||||
/// May also change the type of the value.
|
||||
/// </summary>
|
||||
[Obsolete("Use Then instead")]
|
||||
IPromise<ConvertedT> Transform<ConvertedT>(Func<PromisedT, ConvertedT> transform);
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// Returns a promise for a collection of the resolved results.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
IPromise<IEnumerable<ConvertedT>> ThenAll<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// Converts to a non-value promise.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
IPromise ThenAll(Func<PromisedT, IEnumerable<IPromise>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// Yields the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> ThenRace<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Converts to a non-value promise.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// Yields the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
IPromise ThenRace(Func<PromisedT, IEnumerable<IPromise>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the resulting value if resolved.
|
||||
/// Throws the rejection if rejected.
|
||||
/// Throws an exception if not settled.
|
||||
/// </summary>
|
||||
PromisedT Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerable that yields null until the promise is settled.
|
||||
/// ("To WaitFor" like the WaitForXXYY functions Unity provides.)
|
||||
/// Suitable for use with a Unity coroutine's "yield return promise.ToWaitFor()"
|
||||
/// Once it finishes, use promise.Value to retrieve the value/error.
|
||||
///
|
||||
/// If throwOnFail is true, the coroutine will abort on promise rejection.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerator ToWaitFor(bool abortOnFail = false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a promise that can be rejected.
|
||||
/// </summary>
|
||||
public interface IRejectable
|
||||
{
|
||||
/// <summary>
|
||||
/// Reject the promise with an exception.
|
||||
/// </summary>
|
||||
void Reject(Exception ex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a promise that can be rejected or resolved.
|
||||
/// </summary>
|
||||
public interface IPendingPromise<PromisedT> : IRejectable
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve the promise with a particular value.
|
||||
/// </summary>
|
||||
void Resolve(PromisedT value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the state of a promise.
|
||||
/// </summary>
|
||||
public enum PromiseState
|
||||
{
|
||||
Pending, // The promise is in-flight.
|
||||
Rejected, // The promise has been rejected.
|
||||
Resolved // The promise has been resolved.
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Implements a C# promise.
|
||||
/// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
/// </summary>
|
||||
public class Promise<PromisedT> : IPromise<PromisedT>, IPendingPromise<PromisedT>, IPromiseInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception when the promise is rejected.
|
||||
/// </summary>
|
||||
private Exception rejectionException;
|
||||
|
||||
/// <summary>
|
||||
/// The value when the promises is resolved.
|
||||
/// </summary>
|
||||
private PromisedT resolveValue;
|
||||
|
||||
/// <summary>
|
||||
/// Error handler.
|
||||
/// </summary>
|
||||
private List<RejectHandler> rejectHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Completed handlers that accept a value.
|
||||
/// </summary>
|
||||
private List<Action<PromisedT>> resolveCallbacks;
|
||||
private List<IRejectable> resolveRejectables;
|
||||
|
||||
/// <summary>
|
||||
/// ID of the promise, useful for debugging.
|
||||
/// </summary>
|
||||
public int Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the promise, when set, useful for debugging.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the current state of the promise.
|
||||
/// </summary>
|
||||
public PromiseState CurState { get; private set; }
|
||||
|
||||
public Promise()
|
||||
{
|
||||
this.CurState = PromiseState.Pending;
|
||||
this.Id = ++Promise.nextPromiseId;
|
||||
|
||||
if (Promise.EnablePromiseTracking)
|
||||
{
|
||||
Promise.pendingPromises.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public Promise(Action<Action<PromisedT>, Action<Exception>> resolver)
|
||||
{
|
||||
this.CurState = PromiseState.Pending;
|
||||
this.Id = ++Promise.nextPromiseId;
|
||||
|
||||
if (Promise.EnablePromiseTracking)
|
||||
{
|
||||
Promise.pendingPromises.Add(this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
resolver(
|
||||
// Resolve
|
||||
value => Resolve(value),
|
||||
|
||||
// Reject
|
||||
ex => Reject(ex)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rejection handler for this promise.
|
||||
/// </summary>
|
||||
private void AddRejectHandler(Action<Exception> onRejected, IRejectable rejectable)
|
||||
{
|
||||
if (rejectHandlers == null)
|
||||
{
|
||||
rejectHandlers = new List<RejectHandler>();
|
||||
}
|
||||
|
||||
rejectHandlers.Add(new RejectHandler() { callback = onRejected, rejectable = rejectable }); ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolve handler for this promise.
|
||||
/// </summary>
|
||||
private void AddResolveHandler(Action<PromisedT> onResolved, IRejectable rejectable)
|
||||
{
|
||||
if (resolveCallbacks == null)
|
||||
{
|
||||
resolveCallbacks = new List<Action<PromisedT>>();
|
||||
}
|
||||
|
||||
if (resolveRejectables == null)
|
||||
{
|
||||
resolveRejectables = new List<IRejectable>();
|
||||
}
|
||||
|
||||
resolveCallbacks.Add(onResolved);
|
||||
resolveRejectables.Add(rejectable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a single handler.
|
||||
/// </summary>
|
||||
private void InvokeHandler<T>(Action<T> callback, IRejectable rejectable, T value)
|
||||
{
|
||||
// Argument.NotNull(() => callback);
|
||||
// Argument.NotNull(() => rejectable);
|
||||
|
||||
try
|
||||
{
|
||||
callback(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rejectable.Reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function clear out all handlers after resolution or rejection.
|
||||
/// </summary>
|
||||
private void ClearHandlers()
|
||||
{
|
||||
rejectHandlers = null;
|
||||
resolveCallbacks = null;
|
||||
resolveRejectables = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke all reject handlers.
|
||||
/// </summary>
|
||||
private void InvokeRejectHandlers(Exception ex)
|
||||
{
|
||||
// Argument.NotNull(() => ex);
|
||||
|
||||
if (rejectHandlers != null)
|
||||
{
|
||||
rejectHandlers.Each(handler => InvokeHandler(handler.callback, handler.rejectable, ex));
|
||||
}
|
||||
|
||||
ClearHandlers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke all resolve handlers.
|
||||
/// </summary>
|
||||
private void InvokeResolveHandlers(PromisedT value)
|
||||
{
|
||||
if (resolveCallbacks != null)
|
||||
{
|
||||
for (int i = 0, maxI = resolveCallbacks.Count; i < maxI; i++) {
|
||||
InvokeHandler(resolveCallbacks[i], resolveRejectables[i], value);
|
||||
}
|
||||
}
|
||||
|
||||
ClearHandlers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reject the promise with an exception.
|
||||
/// </summary>
|
||||
public void Reject(Exception ex)
|
||||
{
|
||||
// Argument.NotNull(() => ex);
|
||||
|
||||
if (CurState != PromiseState.Pending)
|
||||
{
|
||||
throw new ApplicationException("Attempt to reject a promise that is already in state: " + CurState + ", a promise can only be rejected when it is still in state: " + PromiseState.Pending);
|
||||
}
|
||||
|
||||
rejectionException = ex;
|
||||
CurState = PromiseState.Rejected;
|
||||
|
||||
if (Promise.EnablePromiseTracking)
|
||||
{
|
||||
Promise.pendingPromises.Remove(this);
|
||||
}
|
||||
|
||||
InvokeRejectHandlers(ex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the promise with a particular value.
|
||||
/// </summary>
|
||||
public void Resolve(PromisedT value)
|
||||
{
|
||||
if (CurState != PromiseState.Pending)
|
||||
{
|
||||
throw new ApplicationException("Attempt to resolve a promise that is already in state: " + CurState + ", a promise can only be resolved when it is still in state: " + PromiseState.Pending);
|
||||
}
|
||||
|
||||
resolveValue = value;
|
||||
CurState = PromiseState.Resolved;
|
||||
|
||||
if (Promise.EnablePromiseTracking)
|
||||
{
|
||||
Promise.pendingPromises.Remove(this);
|
||||
}
|
||||
|
||||
InvokeResolveHandlers(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// onRejected is called on error.
|
||||
/// </summary>
|
||||
public void Done(Action<PromisedT> onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
Then(onResolved, onRejected)
|
||||
.Catch(ex =>
|
||||
Promise.PropagateUnhandledException(this, ex)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// Adds a default error handler.
|
||||
/// </summary>
|
||||
public void Done(Action<PromisedT> onResolved)
|
||||
{
|
||||
Then(onResolved)
|
||||
.Catch(ex =>
|
||||
Promise.PropagateUnhandledException(this, ex)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete the promise. Adds a default error handler.
|
||||
/// </summary>
|
||||
public void Done()
|
||||
{
|
||||
Catch(ex =>
|
||||
Promise.PropagateUnhandledException(this, ex)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the name of the promise, useful for debugging.
|
||||
/// </summary>
|
||||
public IPromise<PromisedT> WithName(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle errors for the promise.
|
||||
/// </summary>
|
||||
public IPromise<PromisedT> Catch(Action<Exception> onRejected)
|
||||
{
|
||||
// Argument.NotNull(() => onRejected);
|
||||
|
||||
var resultPromise = new Promise<PromisedT>();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action<PromisedT> resolveHandler = v =>
|
||||
{
|
||||
resultPromise.Resolve(v);
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
onRejected(ex);
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved)
|
||||
{
|
||||
return Then(onResolved, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a non-value promise.
|
||||
/// </summary>
|
||||
public IPromise Then(Func<PromisedT, IPromise> onResolved)
|
||||
{
|
||||
return Then(onResolved, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback.
|
||||
/// </summary>
|
||||
public IPromise<PromisedT> Then(Action<PromisedT> onResolved)
|
||||
{
|
||||
return Then(onResolved, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
// This version of the function must supply an onResolved.
|
||||
// Otherwise there is now way to get the converted value to pass to the resulting promise.
|
||||
// Argument.NotNull(() => onResolved);
|
||||
|
||||
var resultPromise = new Promise<ConvertedT>();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action<PromisedT> resolveHandler = v =>
|
||||
{
|
||||
onResolved(v)
|
||||
.Then(
|
||||
// Should not be necessary to specify the arg type on the next line, but Unity (mono) has an internal compiler error otherwise.
|
||||
(ConvertedT chainedValue) => resultPromise.Resolve(chainedValue),
|
||||
ex => resultPromise.Reject(ex)
|
||||
);
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
if (onRejected != null)
|
||||
{
|
||||
onRejected(ex);
|
||||
}
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a non-value promise.
|
||||
/// </summary>
|
||||
public IPromise Then(Func<PromisedT, IPromise> onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
var resultPromise = new Promise();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action<PromisedT> resolveHandler = v =>
|
||||
{
|
||||
if (onResolved != null)
|
||||
{
|
||||
onResolved(v)
|
||||
.Then(
|
||||
() => resultPromise.Resolve(),
|
||||
ex => resultPromise.Reject(ex)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultPromise.Resolve();
|
||||
}
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
if (onRejected != null)
|
||||
{
|
||||
onRejected(ex);
|
||||
}
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// </summary>
|
||||
public IPromise<PromisedT> Then(Action<PromisedT> onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
var resultPromise = new Promise<PromisedT>();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action<PromisedT> resolveHandler = v =>
|
||||
{
|
||||
if (onResolved != null)
|
||||
{
|
||||
onResolved(v);
|
||||
}
|
||||
|
||||
resultPromise.Resolve(v);
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
if (onRejected != null)
|
||||
{
|
||||
onRejected(ex);
|
||||
}
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a new promise with a different value.
|
||||
/// May also change the type of the value.
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, ConvertedT> transform)
|
||||
{
|
||||
// Argument.NotNull(() => transform);
|
||||
return Then(value => Promise<ConvertedT>.Resolved(transform(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a new promise with a different value.
|
||||
/// May also change the type of the value.
|
||||
/// </summary>
|
||||
[Obsolete("Use Then instead")]
|
||||
public IPromise<ConvertedT> Transform<ConvertedT>(Func<PromisedT, ConvertedT> transform)
|
||||
{
|
||||
// Argument.NotNull(() => transform);
|
||||
return Then(value => Promise<ConvertedT>.Resolved(transform(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to invoke or register resolve/reject handlers.
|
||||
/// </summary>
|
||||
private void ActionHandlers(IRejectable resultPromise, Action<PromisedT> resolveHandler, Action<Exception> rejectHandler)
|
||||
{
|
||||
if (CurState == PromiseState.Resolved)
|
||||
{
|
||||
InvokeHandler(resolveHandler, resultPromise, resolveValue);
|
||||
}
|
||||
else if (CurState == PromiseState.Rejected)
|
||||
{
|
||||
InvokeHandler(rejectHandler, resultPromise, rejectionException);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddResolveHandler(resolveHandler, resultPromise);
|
||||
AddRejectHandler(rejectHandler, resultPromise);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// Returns a promise for a collection of the resolved results.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
public IPromise<IEnumerable<ConvertedT>> ThenAll<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain)
|
||||
{
|
||||
return Then(value => Promise<ConvertedT>.All(chain(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// Converts to a non-value promise.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
public IPromise ThenAll(Func<PromisedT, IEnumerable<IPromise>> chain)
|
||||
{
|
||||
return Then(value => Promise.All(chain(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when all of the promises in the enumerable argument have resolved.
|
||||
/// Returns a promise of a collection of the resolved results.
|
||||
/// </summary>
|
||||
public static IPromise<IEnumerable<PromisedT>> All(params IPromise<PromisedT>[] promises)
|
||||
{
|
||||
return All((IEnumerable<IPromise<PromisedT>>)promises); // Cast is required to force use of the other All function.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when all of the promises in the enumerable argument have resolved.
|
||||
/// Returns a promise of a collection of the resolved results.
|
||||
/// </summary>
|
||||
public static IPromise<IEnumerable<PromisedT>> All(IEnumerable<IPromise<PromisedT>> promises)
|
||||
{
|
||||
var promisesArray = promises.ToArray();
|
||||
if (promisesArray.Length == 0)
|
||||
{
|
||||
return Promise<IEnumerable<PromisedT>>.Resolved(EnumerableExt.Empty<PromisedT>());
|
||||
}
|
||||
|
||||
var remainingCount = promisesArray.Length;
|
||||
var results = new PromisedT[remainingCount];
|
||||
var resultPromise = new Promise<IEnumerable<PromisedT>>();
|
||||
resultPromise.WithName("All");
|
||||
|
||||
promisesArray.Each((promise, index) =>
|
||||
{
|
||||
promise
|
||||
.Catch(ex =>
|
||||
{
|
||||
if (resultPromise.CurState == PromiseState.Pending)
|
||||
{
|
||||
// If a promise errorred and the result promise is still pending, reject it.
|
||||
resultPromise.Reject(ex);
|
||||
}
|
||||
})
|
||||
.Then(result =>
|
||||
{
|
||||
results[index] = result;
|
||||
|
||||
--remainingCount;
|
||||
if (remainingCount <= 0)
|
||||
{
|
||||
// This will never happen if any of the promises errorred.
|
||||
resultPromise.Resolve(results);
|
||||
}
|
||||
})
|
||||
.Done();
|
||||
});
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// Yields the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> ThenRace<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain)
|
||||
{
|
||||
return Then(value => Promise<ConvertedT>.Race(chain(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Converts to a non-value promise.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// Yields the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
public IPromise ThenRace(Func<PromisedT, IEnumerable<IPromise>> chain)
|
||||
{
|
||||
return Then(value => Promise.Race(chain(value)));
|
||||
}
|
||||
|
||||
public PromisedT Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurState == PromiseState.Pending) throw new InvalidOperationException("Promise not settled");
|
||||
else if (CurState == PromiseState.Rejected) throw rejectionException;
|
||||
return resolveValue;
|
||||
}
|
||||
}
|
||||
|
||||
class Enumerated<T> : IEnumerator {
|
||||
private Promise<T> promise;
|
||||
private bool abortOnFail;
|
||||
|
||||
public Enumerated(Promise<T> promise, bool abortOnFail) {
|
||||
this.promise = promise;
|
||||
this.abortOnFail = abortOnFail;
|
||||
}
|
||||
|
||||
public bool MoveNext() {
|
||||
if (abortOnFail && promise.CurState == PromiseState.Rejected) {
|
||||
throw promise.rejectionException;
|
||||
}
|
||||
return promise.CurState == PromiseState.Pending;
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public object Current { get { return null; } }
|
||||
}
|
||||
|
||||
public IEnumerator ToWaitFor(bool abortOnFail) {
|
||||
var ret = new Enumerated<PromisedT>(this, abortOnFail);
|
||||
//someone will poll for completion, so act like we've been terminated
|
||||
Done(x => {}, ex => {});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when the first of the promises in the enumerable argument have resolved.
|
||||
/// Returns the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
public static IPromise<PromisedT> Race(params IPromise<PromisedT>[] promises)
|
||||
{
|
||||
return Race((IEnumerable<IPromise<PromisedT>>)promises); // Cast is required to force use of the other function.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when the first of the promises in the enumerable argument have resolved.
|
||||
/// Returns the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
public static IPromise<PromisedT> Race(IEnumerable<IPromise<PromisedT>> promises)
|
||||
{
|
||||
var promisesArray = promises.ToArray();
|
||||
if (promisesArray.Length == 0)
|
||||
{
|
||||
throw new ApplicationException("At least 1 input promise must be provided for Race");
|
||||
}
|
||||
|
||||
var resultPromise = new Promise<PromisedT>();
|
||||
resultPromise.WithName("Race");
|
||||
|
||||
promisesArray.Each((promise, index) =>
|
||||
{
|
||||
promise
|
||||
.Catch(ex =>
|
||||
{
|
||||
if (resultPromise.CurState == PromiseState.Pending)
|
||||
{
|
||||
// If a promise errorred and the result promise is still pending, reject it.
|
||||
resultPromise.Reject(ex);
|
||||
}
|
||||
})
|
||||
.Then(result =>
|
||||
{
|
||||
if (resultPromise.CurState == PromiseState.Pending)
|
||||
{
|
||||
resultPromise.Resolve(result);
|
||||
}
|
||||
})
|
||||
.Done();
|
||||
});
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a simple value directly into a resolved promise.
|
||||
/// </summary>
|
||||
public static IPromise<PromisedT> Resolved(PromisedT promisedValue)
|
||||
{
|
||||
var promise = new Promise<PromisedT>();
|
||||
promise.Resolve(promisedValue);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an exception directly into a rejected promise.
|
||||
/// </summary>
|
||||
public static IPromise<PromisedT> Rejected(Exception ex)
|
||||
{
|
||||
// Argument.NotNull(() => ex);
|
||||
|
||||
var promise = new Promise<PromisedT>();
|
||||
promise.Reject(ex);
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 56222919992acdd489a2deff63c33981
|
||||
timeCreated: 1479581013
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that wraps a pending promise with it's predicate and time data
|
||||
/// </summary>
|
||||
internal class PredicateWait
|
||||
{
|
||||
/// <summary>
|
||||
/// Predicate for resolving the promise
|
||||
/// </summary>
|
||||
public Func<TimeData, bool> predicate;
|
||||
|
||||
/// <summary>
|
||||
/// The time the promise was started
|
||||
/// </summary>
|
||||
public float timeStarted;
|
||||
|
||||
/// <summary>
|
||||
/// The pending promise which is an interface for a promise that can be rejected or resolved.
|
||||
/// </summary>
|
||||
public IPendingPromise pendingPromise;
|
||||
|
||||
/// <summary>
|
||||
/// The time data specific to this pending promise. Includes elapsed time and delta time.
|
||||
/// </summary>
|
||||
public TimeData timeData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time data specific to a particular pending promise.
|
||||
/// </summary>
|
||||
public struct TimeData
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of time that has elapsed since the pending promise started running
|
||||
/// </summary>
|
||||
public float elapsedTime;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time since the last time the pending promise was updated.
|
||||
/// </summary>
|
||||
public float deltaTime;
|
||||
}
|
||||
|
||||
public interface IPromiseTimer
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve the returned promise once the time has elapsed
|
||||
/// </summary>
|
||||
IPromise WaitFor(float seconds);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the returned promise once the predicate evaluates to true
|
||||
/// </summary>
|
||||
IPromise WaitUntil(Func<TimeData, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the returned promise once the predicate evaluates to false
|
||||
/// </summary>
|
||||
IPromise WaitWhile(Func<TimeData, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Update all pending promises. Must be called for the promises to progress and resolve at all.
|
||||
/// </summary>
|
||||
void Update(float deltaTime);
|
||||
}
|
||||
|
||||
public class PromiseTimer : IPromiseTimer
|
||||
{
|
||||
/// <summary>
|
||||
/// The current running total for time that this PromiseTimer has run for
|
||||
/// </summary>
|
||||
private float curTime;
|
||||
|
||||
/// <summary>
|
||||
/// Currently pending promises
|
||||
/// </summary>
|
||||
private List<PredicateWait> waiting = new List<PredicateWait>();
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the returned promise once the time has elapsed
|
||||
/// </summary>
|
||||
public IPromise WaitFor(float seconds)
|
||||
{
|
||||
return WaitUntil(t => t.elapsedTime >= seconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the returned promise once the predicate evaluates to false
|
||||
/// </summary>
|
||||
public IPromise WaitWhile(Func<TimeData, bool> predicate)
|
||||
{
|
||||
return WaitUntil(t => !predicate(t));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the returned promise once the predicate evalutes to true
|
||||
/// </summary>
|
||||
public IPromise WaitUntil(Func<TimeData, bool> predicate)
|
||||
{
|
||||
var promise = new Promise();
|
||||
|
||||
var wait = new PredicateWait()
|
||||
{
|
||||
timeStarted = curTime,
|
||||
pendingPromise = promise,
|
||||
timeData = new TimeData(),
|
||||
predicate = predicate
|
||||
};
|
||||
|
||||
waiting.Add(wait);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all pending promises. Must be called for the promises to progress and resolve at all.
|
||||
/// </summary>
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
curTime += deltaTime;
|
||||
|
||||
int i = 0;
|
||||
while (i < waiting.Count)
|
||||
{
|
||||
var wait = waiting[i];
|
||||
|
||||
var newElapsedTime = curTime - wait.timeStarted;
|
||||
wait.timeData.deltaTime = newElapsedTime - wait.timeData.elapsedTime;
|
||||
wait.timeData.elapsedTime = newElapsedTime;
|
||||
|
||||
bool result;
|
||||
try
|
||||
{
|
||||
result = wait.predicate(wait.timeData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
wait.pendingPromise.Reject(ex);
|
||||
waiting.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result)
|
||||
{
|
||||
wait.pendingPromise.Resolve();
|
||||
waiting.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bab6d12600d36db4b81c0aa04f0fd685
|
||||
timeCreated: 1479581013
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,930 @@
|
|||
using ZenFulcrum.EmbeddedBrowser.Promises;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a non-generic C# promise, this is a promise that simply resolves without delivering a value.
|
||||
/// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
///
|
||||
/// This can also be waited on in a Unity coroutine.
|
||||
/// </summary>
|
||||
public interface IPromise
|
||||
{
|
||||
/// <summary>
|
||||
/// Set the name of the promise, useful for debugging.
|
||||
/// </summary>
|
||||
IPromise WithName(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// onRejected is called on error.
|
||||
/// </summary>
|
||||
void Done(Action onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// Adds a default error handler.
|
||||
/// </summary>
|
||||
void Done(Action onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Complete the promise. Adds a default error handler.
|
||||
/// </summary>
|
||||
void Done();
|
||||
|
||||
/// <summary>
|
||||
/// Handle errors for the promise.
|
||||
/// </summary>
|
||||
IPromise Catch(Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> Then<ConvertedT>(Func<IPromise<ConvertedT>> onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a non-value promise.
|
||||
/// </summary>
|
||||
IPromise Then(Func<IPromise> onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback.
|
||||
/// </summary>
|
||||
IPromise Then(Action onResolved);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> Then<ConvertedT>(Func<IPromise<ConvertedT>> onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a non-value promise.
|
||||
/// </summary>
|
||||
IPromise Then(Func<IPromise> onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// </summary>
|
||||
IPromise Then(Action onResolved, Action<Exception> onRejected);
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
IPromise ThenAll(Func<IEnumerable<IPromise>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// Converts to a non-value promise.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
IPromise<IEnumerable<ConvertedT>> ThenAll<ConvertedT>(Func<IEnumerable<IPromise<ConvertedT>>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Chain a sequence of operations using promises.
|
||||
/// Reutrn a collection of functions each of which starts an async operation and yields a promise.
|
||||
/// Each function will be called and each promise resolved in turn.
|
||||
/// The resulting promise is resolved after each promise is resolved in sequence.
|
||||
/// </summary>
|
||||
IPromise ThenSequence(Func<IEnumerable<Func<IPromise>>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// </summary>
|
||||
IPromise ThenRace(Func<IEnumerable<IPromise>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Converts to a value promise.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// </summary>
|
||||
IPromise<ConvertedT> ThenRace<ConvertedT>(Func<IEnumerable<IPromise<ConvertedT>>> chain);
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerable that yields null until the promise is settled.
|
||||
/// ("To WaitFor" like the WaitForXXYY functions Unity provides.)
|
||||
/// Suitable for use with a Unity coroutine's "yield return promise.ToWaitFor()"
|
||||
///
|
||||
/// If throwOnFail is true, the coroutine will abort on promise rejection.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerator ToWaitFor(bool abortOnFail = false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a promise that can be rejected or resolved.
|
||||
/// </summary>
|
||||
public interface IPendingPromise : IRejectable
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve the promise with a particular value.
|
||||
/// </summary>
|
||||
void Resolve();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to list information of pending promises.
|
||||
/// </summary>
|
||||
public interface IPromiseInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the promise.
|
||||
/// </summary>
|
||||
int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable name for the promise.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arguments to the UnhandledError event.
|
||||
/// </summary>
|
||||
public class ExceptionEventArgs : EventArgs
|
||||
{
|
||||
internal ExceptionEventArgs(Exception exception)
|
||||
{
|
||||
// Argument.NotNull(() => exception);
|
||||
|
||||
this.Exception = exception;
|
||||
}
|
||||
|
||||
public Exception Exception
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a handler invoked when the promise is rejected.
|
||||
/// </summary>
|
||||
public struct RejectHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback fn.
|
||||
/// </summary>
|
||||
public Action<Exception> callback;
|
||||
|
||||
/// <summary>
|
||||
/// The promise that is rejected when there is an error while invoking the handler.
|
||||
/// </summary>
|
||||
public IRejectable rejectable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements a non-generic C# promise, this is a promise that simply resolves without delivering a value.
|
||||
/// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
/// </summary>
|
||||
public class Promise : IPromise, IPendingPromise, IPromiseInfo
|
||||
{
|
||||
static Promise()
|
||||
{
|
||||
UnhandledException += (sender, args) => {
|
||||
UnityEngine.Debug.LogWarning("Rejection: " + args.Exception.Message + "\n" + args.Exception.StackTrace);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to enable tracking of promises.
|
||||
/// </summary>
|
||||
public static bool EnablePromiseTracking = false;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised for unhandled errors.
|
||||
/// For this to work you have to complete your promises with a call to Done().
|
||||
/// </summary>
|
||||
public static event EventHandler<ExceptionEventArgs> UnhandledException
|
||||
{
|
||||
add { unhandlerException += value; }
|
||||
remove { unhandlerException -= value; }
|
||||
}
|
||||
private static EventHandler<ExceptionEventArgs> unhandlerException;
|
||||
|
||||
/// <summary>
|
||||
/// Id for the next promise that is created.
|
||||
/// </summary>
|
||||
internal static int nextPromiseId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Information about pending promises.
|
||||
/// </summary>
|
||||
internal static HashSet<IPromiseInfo> pendingPromises = new HashSet<IPromiseInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Information about pending promises, useful for debugging.
|
||||
/// This is only populated when 'EnablePromiseTracking' is set to true.
|
||||
/// </summary>
|
||||
public static IEnumerable<IPromiseInfo> GetPendingPromises()
|
||||
{
|
||||
return pendingPromises;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The exception when the promise is rejected.
|
||||
/// </summary>
|
||||
private Exception rejectionException;
|
||||
|
||||
/// <summary>
|
||||
/// Error handlers.
|
||||
/// </summary>
|
||||
private List<RejectHandler> rejectHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a handler invoked when the promise is resolved.
|
||||
/// </summary>
|
||||
public struct ResolveHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback fn.
|
||||
/// </summary>
|
||||
public Action callback;
|
||||
|
||||
/// <summary>
|
||||
/// The promise that is rejected when there is an error while invoking the handler.
|
||||
/// </summary>
|
||||
public IRejectable rejectable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completed handlers that accept no value.
|
||||
/// </summary>
|
||||
private List<ResolveHandler> resolveHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// ID of the promise, useful for debugging.
|
||||
/// </summary>
|
||||
public int Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the promise, when set, useful for debugging.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the current state of the promise.
|
||||
/// </summary>
|
||||
public PromiseState CurState { get; private set; }
|
||||
|
||||
public Promise()
|
||||
{
|
||||
this.CurState = PromiseState.Pending;
|
||||
if (EnablePromiseTracking)
|
||||
{
|
||||
pendingPromises.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public Promise(Action<Action, Action<Exception>> resolver)
|
||||
{
|
||||
this.CurState = PromiseState.Pending;
|
||||
if (EnablePromiseTracking)
|
||||
{
|
||||
pendingPromises.Add(this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
resolver(
|
||||
// Resolve
|
||||
() => Resolve(),
|
||||
|
||||
// Reject
|
||||
ex => Reject(ex)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rejection handler for this promise.
|
||||
/// </summary>
|
||||
private void AddRejectHandler(Action<Exception> onRejected, IRejectable rejectable)
|
||||
{
|
||||
if (rejectHandlers == null)
|
||||
{
|
||||
rejectHandlers = new List<RejectHandler>();
|
||||
}
|
||||
|
||||
rejectHandlers.Add(new RejectHandler()
|
||||
{
|
||||
callback = onRejected,
|
||||
rejectable = rejectable
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolve handler for this promise.
|
||||
/// </summary>
|
||||
private void AddResolveHandler(Action onResolved, IRejectable rejectable)
|
||||
{
|
||||
if (resolveHandlers == null)
|
||||
{
|
||||
resolveHandlers = new List<ResolveHandler>();
|
||||
}
|
||||
|
||||
resolveHandlers.Add(new ResolveHandler()
|
||||
{
|
||||
callback = onResolved,
|
||||
rejectable = rejectable
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a single error handler.
|
||||
/// </summary>
|
||||
private void InvokeRejectHandler(Action<Exception> callback, IRejectable rejectable, Exception value)
|
||||
{
|
||||
// Argument.NotNull(() => callback);
|
||||
// Argument.NotNull(() => rejectable);
|
||||
|
||||
try
|
||||
{
|
||||
callback(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rejectable.Reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a single resolve handler.
|
||||
/// </summary>
|
||||
private void InvokeResolveHandler(Action callback, IRejectable rejectable)
|
||||
{
|
||||
// Argument.NotNull(() => callback);
|
||||
// Argument.NotNull(() => rejectable);
|
||||
|
||||
try
|
||||
{
|
||||
callback();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rejectable.Reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function clear out all handlers after resolution or rejection.
|
||||
/// </summary>
|
||||
private void ClearHandlers()
|
||||
{
|
||||
rejectHandlers = null;
|
||||
resolveHandlers = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke all reject handlers.
|
||||
/// </summary>
|
||||
private void InvokeRejectHandlers(Exception ex)
|
||||
{
|
||||
// Argument.NotNull(() => ex);
|
||||
|
||||
if (rejectHandlers != null)
|
||||
{
|
||||
rejectHandlers.Each(handler => InvokeRejectHandler(handler.callback, handler.rejectable, ex));
|
||||
}
|
||||
|
||||
ClearHandlers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke all resolve handlers.
|
||||
/// </summary>
|
||||
private void InvokeResolveHandlers()
|
||||
{
|
||||
if (resolveHandlers != null)
|
||||
{
|
||||
resolveHandlers.Each(handler => InvokeResolveHandler(handler.callback, handler.rejectable));
|
||||
}
|
||||
|
||||
ClearHandlers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reject the promise with an exception.
|
||||
/// </summary>
|
||||
public void Reject(Exception ex)
|
||||
{
|
||||
// Argument.NotNull(() => ex);
|
||||
|
||||
if (CurState != PromiseState.Pending)
|
||||
{
|
||||
throw new ApplicationException("Attempt to reject a promise that is already in state: " + CurState + ", a promise can only be rejected when it is still in state: " + PromiseState.Pending);
|
||||
}
|
||||
|
||||
rejectionException = ex;
|
||||
CurState = PromiseState.Rejected;
|
||||
|
||||
if (EnablePromiseTracking)
|
||||
{
|
||||
pendingPromises.Remove(this);
|
||||
}
|
||||
|
||||
InvokeRejectHandlers(ex);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the promise with a particular value.
|
||||
/// </summary>
|
||||
public void Resolve()
|
||||
{
|
||||
if (CurState != PromiseState.Pending)
|
||||
{
|
||||
throw new ApplicationException("Attempt to resolve a promise that is already in state: " + CurState + ", a promise can only be resolved when it is still in state: " + PromiseState.Pending);
|
||||
}
|
||||
|
||||
CurState = PromiseState.Resolved;
|
||||
|
||||
if (EnablePromiseTracking)
|
||||
{
|
||||
pendingPromises.Remove(this);
|
||||
}
|
||||
|
||||
InvokeResolveHandlers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// onRejected is called on error.
|
||||
/// </summary>
|
||||
public void Done(Action onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
Then(onResolved, onRejected)
|
||||
.Catch(ex =>
|
||||
Promise.PropagateUnhandledException(this, ex)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the promise.
|
||||
/// onResolved is called on successful completion.
|
||||
/// Adds a default error handler.
|
||||
/// </summary>
|
||||
public void Done(Action onResolved)
|
||||
{
|
||||
Then(onResolved)
|
||||
.Catch(ex =>
|
||||
Promise.PropagateUnhandledException(this, ex)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete the promise. Adds a defualt error handler.
|
||||
/// </summary>
|
||||
public void Done()
|
||||
{
|
||||
Catch(ex =>
|
||||
Promise.PropagateUnhandledException(this, ex)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the name of the promise, useful for debugging.
|
||||
/// </summary>
|
||||
public IPromise WithName(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle errors for the promise.
|
||||
/// </summary>
|
||||
public IPromise Catch(Action<Exception> onRejected)
|
||||
{
|
||||
// Argument.NotNull(() => onRejected);
|
||||
|
||||
var resultPromise = new Promise();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action resolveHandler = () =>
|
||||
{
|
||||
resultPromise.Resolve();
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
onRejected(ex);
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> Then<ConvertedT>(Func<IPromise<ConvertedT>> onResolved)
|
||||
{
|
||||
return Then(onResolved, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback that chains a non-value promise.
|
||||
/// </summary>
|
||||
public IPromise Then(Func<IPromise> onResolved)
|
||||
{
|
||||
return Then(onResolved, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback.
|
||||
/// </summary>
|
||||
public IPromise Then(Action onResolved)
|
||||
{
|
||||
return Then(onResolved, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a value promise (optionally converting to a different value type).
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> Then<ConvertedT>(Func<IPromise<ConvertedT>> onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
// This version of the function must supply an onResolved.
|
||||
// Otherwise there is now way to get the converted value to pass to the resulting promise.
|
||||
// Argument.NotNull(() => onResolved);
|
||||
|
||||
var resultPromise = new Promise<ConvertedT>();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action resolveHandler = () =>
|
||||
{
|
||||
onResolved()
|
||||
.Then(
|
||||
// Should not be necessary to specify the arg type on the next line, but Unity (mono) has an internal compiler error otherwise.
|
||||
(ConvertedT chainedValue) => resultPromise.Resolve(chainedValue),
|
||||
ex => resultPromise.Reject(ex)
|
||||
);
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
if (onRejected != null)
|
||||
{
|
||||
onRejected(ex);
|
||||
}
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// The resolved callback chains a non-value promise.
|
||||
/// </summary>
|
||||
public IPromise Then(Func<IPromise> onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
var resultPromise = new Promise();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action resolveHandler = () =>
|
||||
{
|
||||
if (onResolved != null)
|
||||
{
|
||||
onResolved()
|
||||
.Then(
|
||||
() => resultPromise.Resolve(),
|
||||
ex => resultPromise.Reject(ex)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultPromise.Resolve();
|
||||
}
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
if (onRejected != null)
|
||||
{
|
||||
onRejected(ex);
|
||||
}
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a resolved callback and a rejected callback.
|
||||
/// </summary>
|
||||
public IPromise Then(Action onResolved, Action<Exception> onRejected)
|
||||
{
|
||||
var resultPromise = new Promise();
|
||||
resultPromise.WithName(Name);
|
||||
|
||||
Action resolveHandler = () =>
|
||||
{
|
||||
if (onResolved != null)
|
||||
{
|
||||
onResolved();
|
||||
}
|
||||
|
||||
resultPromise.Resolve();
|
||||
};
|
||||
|
||||
Action<Exception> rejectHandler = ex =>
|
||||
{
|
||||
if (onRejected != null)
|
||||
{
|
||||
onRejected(ex);
|
||||
}
|
||||
|
||||
resultPromise.Reject(ex);
|
||||
};
|
||||
|
||||
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to invoke or register resolve/reject handlers.
|
||||
/// </summary>
|
||||
private void ActionHandlers(IRejectable resultPromise, Action resolveHandler, Action<Exception> rejectHandler)
|
||||
{
|
||||
if (CurState == PromiseState.Resolved)
|
||||
{
|
||||
InvokeResolveHandler(resolveHandler, resultPromise);
|
||||
}
|
||||
else if (CurState == PromiseState.Rejected)
|
||||
{
|
||||
InvokeRejectHandler(rejectHandler, resultPromise, rejectionException);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddResolveHandler(resolveHandler, resultPromise);
|
||||
AddRejectHandler(rejectHandler, resultPromise);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
public IPromise ThenAll(Func<IEnumerable<IPromise>> chain)
|
||||
{
|
||||
return Then(() => Promise.All(chain()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain an enumerable of promises, all of which must resolve.
|
||||
/// Converts to a non-value promise.
|
||||
/// The resulting promise is resolved when all of the promises have resolved.
|
||||
/// It is rejected as soon as any of the promises have been rejected.
|
||||
/// </summary>
|
||||
public IPromise<IEnumerable<ConvertedT>> ThenAll<ConvertedT>(Func<IEnumerable<IPromise<ConvertedT>>> chain)
|
||||
{
|
||||
return Then(() => Promise<ConvertedT>.All(chain()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when all of the promises in the enumerable argument have resolved.
|
||||
/// Returns a promise of a collection of the resolved results.
|
||||
/// </summary>
|
||||
public static IPromise All(params IPromise[] promises)
|
||||
{
|
||||
return All((IEnumerable<IPromise>)promises); // Cast is required to force use of the other All function.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when all of the promises in the enumerable argument have resolved.
|
||||
/// Returns a promise of a collection of the resolved results.
|
||||
/// </summary>
|
||||
public static IPromise All(IEnumerable<IPromise> promises)
|
||||
{
|
||||
var promisesArray = promises.ToArray();
|
||||
if (promisesArray.Length == 0)
|
||||
{
|
||||
return Promise.Resolved();
|
||||
}
|
||||
|
||||
var remainingCount = promisesArray.Length;
|
||||
var resultPromise = new Promise();
|
||||
resultPromise.WithName("All");
|
||||
|
||||
promisesArray.Each((promise, index) =>
|
||||
{
|
||||
promise
|
||||
.Catch(ex =>
|
||||
{
|
||||
if (resultPromise.CurState == PromiseState.Pending)
|
||||
{
|
||||
// If a promise errorred and the result promise is still pending, reject it.
|
||||
resultPromise.Reject(ex);
|
||||
}
|
||||
})
|
||||
.Then(() =>
|
||||
{
|
||||
--remainingCount;
|
||||
if (remainingCount <= 0)
|
||||
{
|
||||
// This will never happen if any of the promises errorred.
|
||||
resultPromise.Resolve();
|
||||
}
|
||||
})
|
||||
.Done();
|
||||
});
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain a sequence of operations using promises.
|
||||
/// Reutrn a collection of functions each of which starts an async operation and yields a promise.
|
||||
/// Each function will be called and each promise resolved in turn.
|
||||
/// The resulting promise is resolved after each promise is resolved in sequence.
|
||||
/// </summary>
|
||||
public IPromise ThenSequence(Func<IEnumerable<Func<IPromise>>> chain)
|
||||
{
|
||||
return Then(() => Sequence(chain()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain a number of operations using promises.
|
||||
/// Takes a number of functions each of which starts an async operation and yields a promise.
|
||||
/// </summary>
|
||||
public static IPromise Sequence(params Func<IPromise>[] fns)
|
||||
{
|
||||
return Sequence((IEnumerable<Func<IPromise>>)fns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chain a sequence of operations using promises.
|
||||
/// Takes a collection of functions each of which starts an async operation and yields a promise.
|
||||
/// </summary>
|
||||
public static IPromise Sequence(IEnumerable<Func<IPromise>> fns)
|
||||
{
|
||||
return fns.Aggregate(
|
||||
Promise.Resolved(),
|
||||
(prevPromise, fn) =>
|
||||
{
|
||||
return prevPromise.Then(() => fn());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// </summary>
|
||||
public IPromise ThenRace(Func<IEnumerable<IPromise>> chain)
|
||||
{
|
||||
return Then(() => Promise.Race(chain()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a function that yields an enumerable of promises.
|
||||
/// Converts to a value promise.
|
||||
/// Returns a promise that resolves when the first of the promises has resolved.
|
||||
/// </summary>
|
||||
public IPromise<ConvertedT> ThenRace<ConvertedT>(Func<IEnumerable<IPromise<ConvertedT>>> chain)
|
||||
{
|
||||
return Then(() => Promise<ConvertedT>.Race(chain()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when the first of the promises in the enumerable argument have resolved.
|
||||
/// Returns the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
public static IPromise Race(params IPromise[] promises)
|
||||
{
|
||||
return Race((IEnumerable<IPromise>)promises); // Cast is required to force use of the other function.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a promise that resolves when the first of the promises in the enumerable argument have resolved.
|
||||
/// Returns the value from the first promise that has resolved.
|
||||
/// </summary>
|
||||
public static IPromise Race(IEnumerable<IPromise> promises)
|
||||
{
|
||||
var promisesArray = promises.ToArray();
|
||||
if (promisesArray.Length == 0)
|
||||
{
|
||||
throw new ApplicationException("At least 1 input promise must be provided for Race");
|
||||
}
|
||||
|
||||
var resultPromise = new Promise();
|
||||
resultPromise.WithName("Race");
|
||||
|
||||
promisesArray.Each((promise, index) =>
|
||||
{
|
||||
promise
|
||||
.Catch(ex =>
|
||||
{
|
||||
if (resultPromise.CurState == PromiseState.Pending)
|
||||
{
|
||||
// If a promise errorred and the result promise is still pending, reject it.
|
||||
resultPromise.Reject(ex);
|
||||
}
|
||||
})
|
||||
.Then(() =>
|
||||
{
|
||||
if (resultPromise.CurState == PromiseState.Pending)
|
||||
{
|
||||
resultPromise.Resolve();
|
||||
}
|
||||
})
|
||||
.Done();
|
||||
});
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a simple value directly into a resolved promise.
|
||||
/// </summary>
|
||||
public static IPromise Resolved()
|
||||
{
|
||||
var promise = new Promise();
|
||||
promise.Resolve();
|
||||
return promise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an exception directly into a rejected promise.
|
||||
/// </summary>
|
||||
public static IPromise Rejected(Exception ex)
|
||||
{
|
||||
// Argument.NotNull(() => ex);
|
||||
|
||||
var promise = new Promise();
|
||||
promise.Reject(ex);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the UnhandledException event.
|
||||
/// </summary>
|
||||
internal static void PropagateUnhandledException(object sender, Exception ex)
|
||||
{
|
||||
if (unhandlerException != null)
|
||||
{
|
||||
unhandlerException(sender, new ExceptionEventArgs(ex));
|
||||
}
|
||||
}
|
||||
|
||||
class Enumerated : IEnumerator {
|
||||
private Promise promise;
|
||||
private bool abortOnFail;
|
||||
|
||||
public Enumerated(Promise promise, bool abortOnFail) {
|
||||
this.promise = promise;
|
||||
this.abortOnFail = abortOnFail;
|
||||
}
|
||||
|
||||
public bool MoveNext() {
|
||||
if (abortOnFail && promise.CurState == PromiseState.Rejected) {
|
||||
throw promise.rejectionException;
|
||||
}
|
||||
return promise.CurState == PromiseState.Pending;
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public object Current { get { return null; } }
|
||||
}
|
||||
|
||||
|
||||
public IEnumerator ToWaitFor(bool abortOnFail = false) {
|
||||
var ret = new Enumerated(this, abortOnFail);
|
||||
//someone will poll for completion, so act like we've been terminated
|
||||
Done(() => {}, ex => {});
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8c0ade0b4d4a8704baa1b214a700ed50
|
||||
timeCreated: 1479581013
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,22 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Hooks to ensure that zfb gets shut down on app (or playmode) exit.
|
||||
* (This used to be used only for builds, but now that we can cleanly shut down the browser
|
||||
* system after every playmode run it's used in the Editor too.)
|
||||
*/
|
||||
class StandaloneShutdown : MonoBehaviour {
|
||||
public static void Create() {
|
||||
var go = new GameObject("ZFB Shutdown");
|
||||
go.AddComponent<StandaloneShutdown>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
|
||||
public void OnApplicationQuit() {
|
||||
BrowserNative.UnloadNative();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 88efef500fce2a24b96f9f6a661a15fb
|
||||
timeCreated: 1449682818
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,151 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
using UnityEngine.Networking;
|
||||
#endif
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Implements fetching BrowserAssets for standalone builds.
|
||||
*
|
||||
* During build, everything in BrowserAssets is packaged into a single file with the following format:
|
||||
* brString {FileHeader}
|
||||
* i32 numEntries
|
||||
* {numEntries} IndexEntry objects
|
||||
* data //The data section is a series of filedata chunks, as laid out in the index.
|
||||
*/
|
||||
public class StandaloneWebResources : WebResources {
|
||||
public struct IndexEntry {
|
||||
public string name;
|
||||
public long offset;
|
||||
public int length;
|
||||
}
|
||||
|
||||
private const string FileHeader = "zfbRes_v1";
|
||||
|
||||
protected Dictionary<string, IndexEntry> toc = new Dictionary<string, IndexEntry>();
|
||||
protected string dataFile;
|
||||
|
||||
public StandaloneWebResources(string dataFile) {
|
||||
this.dataFile = dataFile;
|
||||
}
|
||||
|
||||
public const string DefaultPath = "Resources/browser_assets";
|
||||
|
||||
public void LoadIndex() {
|
||||
using (var data = new BinaryReader(File.OpenRead(dataFile))) {
|
||||
var header = data.ReadString();
|
||||
if (header != FileHeader) throw new Exception("Invalid web resource file");
|
||||
|
||||
var num = data.ReadInt32();
|
||||
|
||||
for (int i = 0; i < num; ++i) {
|
||||
var entry = new IndexEntry() {
|
||||
name = data.ReadString(),
|
||||
offset = data.ReadInt64(),
|
||||
length = data.ReadInt32(),
|
||||
};
|
||||
toc[entry.name] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleRequest(int id, string url) {
|
||||
var parsedURL = new Uri(url);
|
||||
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
var path = UnityWebRequest.UnEscapeURL(parsedURL.AbsolutePath);
|
||||
#else
|
||||
var path = WWW.UnEscapeURL(parsedURL.AbsolutePath);
|
||||
#endif
|
||||
|
||||
IndexEntry entry;
|
||||
if (!toc.TryGetValue(path, out entry)) {
|
||||
SendError(id, "Not found", 404);
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread(() => {
|
||||
try {
|
||||
var ext = Path.GetExtension(entry.name);
|
||||
if (ext.Length > 0) ext = ext.Substring(1);
|
||||
|
||||
string mimeType;
|
||||
if (!extensionMimeTypes.TryGetValue(ext, out mimeType)) {
|
||||
mimeType = extensionMimeTypes["*"];
|
||||
}
|
||||
|
||||
using (var file = File.OpenRead(dataFile)) {
|
||||
var pre = new ResponsePreamble {
|
||||
headers = null,
|
||||
length = entry.length,
|
||||
mimeType = mimeType,
|
||||
statusCode = 200,
|
||||
};
|
||||
SendPreamble(id, pre);
|
||||
|
||||
file.Seek(entry.offset, SeekOrigin.Begin);
|
||||
var data = new byte[entry.length];
|
||||
var readLen = file.Read(data, 0, entry.length);
|
||||
if (readLen != data.Length) throw new Exception("Insufficient data for file");
|
||||
|
||||
SendData(id, data);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
|
||||
}).Start();
|
||||
|
||||
}
|
||||
|
||||
public void WriteData(Dictionary<string, byte[]> files) {
|
||||
var entries = new Dictionary<string, IndexEntry>();
|
||||
|
||||
using (var file = File.OpenWrite(dataFile)) {
|
||||
var writer = new BinaryWriter(file, Encoding.UTF8 /*, true (Mono too old)*/);
|
||||
writer.Write(FileHeader);
|
||||
writer.Write(files.Count);
|
||||
|
||||
var tocStart = file.Position;
|
||||
|
||||
foreach (var kvp in files) {
|
||||
writer.Write(kvp.Key);
|
||||
writer.Write(0L);
|
||||
writer.Write(0);
|
||||
}
|
||||
//we'll come back and fill it in right later
|
||||
|
||||
foreach (var kvp in files) {
|
||||
var data = kvp.Value;
|
||||
var entry = new IndexEntry {
|
||||
name = kvp.Key,
|
||||
length = kvp.Value.Length,
|
||||
offset = file.Position,
|
||||
};
|
||||
|
||||
writer.Write(data);
|
||||
entries[kvp.Key] = entry;
|
||||
}
|
||||
|
||||
//now go back and write the correct data.
|
||||
writer.Seek((int)tocStart, SeekOrigin.Begin);
|
||||
|
||||
foreach (var kvp in files) {
|
||||
var entry = entries[kvp.Key];
|
||||
writer.Write(kvp.Key);
|
||||
writer.Write(entry.offset);
|
||||
writer.Write(entry.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 116a67bb616e372428beb4db63fa0591
|
||||
timeCreated: 1449617902
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
/**
|
||||
* Cooks up the user agent to use for the browser.
|
||||
*
|
||||
* Ideally, we'd just say a little bit and let websites feature-detect, but many websites (sadly)
|
||||
* still use UA sniffing to decide what version of the page to give us.
|
||||
*
|
||||
* Notably, **Google** does this (for example, maps.google.com), which I find rather strange considering
|
||||
* that I'd expect them to be among those telling us to feature-detect.
|
||||
*
|
||||
* So, use this class to generate a pile of turd that looks like every other browser out there acting like
|
||||
* every browser that came before it so we get the "real" version of pages when we browse.
|
||||
*/
|
||||
public class UserAgent {
|
||||
|
||||
private static string agentOverride;
|
||||
|
||||
/**
|
||||
* Returns a user agent that, hopefully, tricks legacy, stupid, non-feature-detection websites
|
||||
* into giving us their actual content.
|
||||
*
|
||||
* If you change this, the Editor will usually need to be restarted for changes to take effect.
|
||||
*/
|
||||
public static string GetUserAgent() {
|
||||
if (agentOverride != null) return agentOverride;
|
||||
|
||||
var chromeVersion = Marshal.PtrToStringAnsi(BrowserNative.zfb_getVersion());
|
||||
|
||||
//(Note: I don't care what version of the OS you have, we're telling the website you have this
|
||||
//version so you get a good site.)
|
||||
string osStr = "Windows NT 6.1";
|
||||
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||
osStr = "Macintosh; Intel Mac OS X 10_7_0";
|
||||
#elif UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX
|
||||
osStr = "X11; Linux x86_64";
|
||||
#endif
|
||||
|
||||
var userAgent =
|
||||
"Mozilla/5.0 " +
|
||||
"(" + osStr + "; Unity 3D; ZFBrowser 2.1.0; " + Application.productName + " " + Application.version + ") " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chromeVersion + " Safari/537.36"
|
||||
;
|
||||
|
||||
//Chromium has issues with non-ASCII user agents.
|
||||
userAgent = Regex.Replace(userAgent, @"[^\u0020-\u007E]", "?");
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public static void SetUserAgent(string userAgent) {
|
||||
if (BrowserNative.NativeLoaded) {
|
||||
throw new InvalidOperationException("User Agent can only be changed before native backend is initialized.");
|
||||
}
|
||||
|
||||
agentOverride = userAgent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fd7ecd3989cd6dd4e8cf983f4829d97f
|
||||
timeCreated: 1456246764
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser {
|
||||
|
||||
public static class Util {
|
||||
|
||||
/**
|
||||
* Sometimes creating a culture in a different thread causes Mono to crash
|
||||
* with mono_class_vtable_full.
|
||||
*
|
||||
* This variant of StartsWith won't try to use a culture.
|
||||
*/
|
||||
public static bool SafeStartsWith(this string check, string starter) {
|
||||
if (check == null || starter == null) return false;
|
||||
|
||||
if (check.Length < starter.Length) return false;
|
||||
|
||||
for (int i = 0; i < starter.Length; ++i) {
|
||||
if (check[i] != starter[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a UTF8-encoded null-terminated string to a CLR string.
|
||||
/// </summary>
|
||||
/// <param name="strIn"></param>
|
||||
/// <returns></returns>
|
||||
public static string PtrToStringUTF8(IntPtr strIn) {
|
||||
if (strIn == IntPtr.Zero) return null;
|
||||
int strLen = 0;
|
||||
while (Marshal.ReadByte(strIn, strLen) != 0) ++strLen;
|
||||
var buffer = new byte[strLen];
|
||||
Marshal.Copy(strIn, buffer, 0, strLen);
|
||||
return Encoding.UTF8.GetString(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public class JSException : Exception {
|
||||
public JSException(string what) : base(what) {}
|
||||
}
|
||||
|
||||
public enum KeyAction {
|
||||
Press, Release, PressAndRelease
|
||||
}
|
||||
|
||||
public class BrowserFocusState {
|
||||
public bool hasKeyboardFocus;
|
||||
public bool hasMouseFocus;
|
||||
|
||||
public string focusedTagName;
|
||||
public bool focusedNodeEditable;
|
||||
}
|
||||
|
||||
public class BrowserNavState {
|
||||
public bool canGoForward, canGoBack, loading;
|
||||
public string url = "";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6b8101d35cb5dd149bd00dc81dff0262
|
||||
timeCreated: 1454553765
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,10 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 700b927cab691a449bd94222d6724dab
|
||||
folderAsset: yes
|
||||
timeCreated: 1510959402
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,279 @@
|
|||
//#define OCULUS_SDK //<-- define this in your project settings if you have the OVR API
|
||||
#if UNITY_2017_2_OR_NEWER
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR;
|
||||
using ZenFulcrum.VR.OpenVRBinding;
|
||||
|
||||
|
||||
namespace ZenFulcrum.EmbeddedBrowser.VR {
|
||||
|
||||
public enum InputAxis {
|
||||
MainTrigger,//main index finger trigger
|
||||
Grip,//Vive squeeze/Oculus hand trigger
|
||||
JoypadX, JoypadY,//touchpad/joystick x/y position
|
||||
Joypad,//touchpad/joystick click/press
|
||||
Application,//application meanu/start button
|
||||
}
|
||||
|
||||
public enum JoyPadType {
|
||||
None,
|
||||
Joystick,
|
||||
TouchPad,
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unity's VR input system is sorely lacking in usefulness.
|
||||
/// We can finally get pose data in a more-or-less straightforward manner (InputTracking.GetNodeStates,
|
||||
/// state.TryGetPosition, etc.), but getting the axis and buttons is an awful mess.
|
||||
///
|
||||
/// This page https://docs.unity3d.com/Manual/OpenVRControllers.html says we can access the inputs via
|
||||
/// the input system! And, you can look at (of all things) the Joystick names to see which joystick
|
||||
/// is a VR controller at runtime. But guess what? There's no Unity API to fetch the value of a given
|
||||
/// axis on a given controller! Extra-stupid, right? You can define a specific input in the input menu,
|
||||
/// but that doesn't help when you switch to a different computer and the joystick numbers change!
|
||||
/// Short of defining an "input" for every possible controller * every axis you use, there's not a
|
||||
/// way to get the controller axis inputs in a way that will work reliably across different machines.
|
||||
///
|
||||
/// (We can fetch buttons manually via Input.GetKey(KeyCode.Joystick1Button0 + buttonId + joystickIdx * 20),
|
||||
/// but this don't address the axis issue at all.)
|
||||
///
|
||||
/// Anyway, this is a workaround. Around another Unity problem. Implemented by hooking into backend APIs directly.
|
||||
/// </summary>
|
||||
public class VRInput {
|
||||
private static VRInput impl;
|
||||
|
||||
public static void Init() {
|
||||
if (impl == null) impl = GetImpl();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state of the given button/axis.
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <param name="axis"></param>
|
||||
/// <returns></returns>
|
||||
public static float GetAxis(XRNodeState node, InputAxis axis) {
|
||||
if (impl == null) impl = GetImpl();
|
||||
return impl.GetAxisValue(node, axis);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the controller is capable, returns if (and sometimes how closely) the player is touching
|
||||
/// the given control.
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <param name="axis"></param>
|
||||
/// <returns></returns>
|
||||
public static float GetTouch(XRNodeState node, InputAxis axis) {
|
||||
if (impl == null) impl = GetImpl();
|
||||
return impl.GetTouchValue(node, axis);
|
||||
}
|
||||
|
||||
protected virtual float GetAxisValue(XRNodeState node, InputAxis axis) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected virtual float GetTouchValue(XRNodeState node, InputAxis axis) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Dictionary<ulong, JoyPadType> nodeTypes = new Dictionary<ulong, JoyPadType>();
|
||||
public static JoyPadType GetJoypadType(XRNodeState node) {
|
||||
JoyPadType ret;
|
||||
if (!nodeTypes.TryGetValue(node.uniqueID, out ret)) {
|
||||
ret = JoyPadType.None;
|
||||
|
||||
if (impl == null) impl = GetImpl();
|
||||
var name = impl.GetNodeName(node);
|
||||
|
||||
if (name.Contains("Oculus Touch Controller") || name.StartsWith("Oculus Rift CV1")) {
|
||||
//OpenVR gives us "Oculus Rift CV1 (Left Controller)" etc. where I wish it would mention the type of controller (Touch)
|
||||
ret = JoyPadType.Joystick;
|
||||
} else if (name.StartsWith("Vive Controller")) {
|
||||
ret = JoyPadType.TouchPad;
|
||||
} else {
|
||||
Debug.LogWarning("Unknown controller type: " + name);
|
||||
}
|
||||
|
||||
nodeTypes[node.uniqueID] = ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public virtual string GetNodeName(XRNodeState node) {
|
||||
return InputTracking.GetNodeName(node.uniqueID);
|
||||
}
|
||||
|
||||
|
||||
// public virtual JoyPadType JoypadTypeValue(XRNodeState node) { return JoyPadType.None; }
|
||||
|
||||
private static VRInput GetImpl() {
|
||||
if (XRSettings.loadedDeviceName == "OpenVR") {
|
||||
return new OpenVRInput();
|
||||
} else if (XRSettings.loadedDeviceName == "Oculus") {
|
||||
#if OCULUS_SDK
|
||||
return new OculusVRInput();
|
||||
#else
|
||||
Debug.LogError("To use the Oculus API for input, import the Oculus SDK and define OCULUS_SDK");
|
||||
return new VRInput();
|
||||
#endif
|
||||
|
||||
} else {
|
||||
Debug.LogError("Unknown VR input system: " + XRSettings.loadedDeviceName);
|
||||
return new VRInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenVRInput : VRInput {
|
||||
|
||||
protected VRControllerState_t lastState;
|
||||
|
||||
public static string GetStringProperty(uint deviceId, ETrackedDeviceProperty prop) {
|
||||
var buffer = new StringBuilder((int)OpenVR.k_unMaxPropertyStringSize);
|
||||
ETrackedPropertyError err = ETrackedPropertyError.TrackedProp_Success;
|
||||
|
||||
OpenVR.System.GetStringTrackedDeviceProperty(
|
||||
deviceId, prop,
|
||||
buffer, OpenVR.k_unMaxPropertyStringSize, ref err
|
||||
);
|
||||
|
||||
if (err != ETrackedPropertyError.TrackedProp_Success) {
|
||||
throw new Exception("Failed to get property " + prop + " on device " + deviceId + ": " + err);
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public override string GetNodeName(XRNodeState node) {
|
||||
var deviceId = (uint)GetDeviceId(node);
|
||||
|
||||
try {
|
||||
return GetStringProperty(deviceId, ETrackedDeviceProperty.Prop_ModelNumber_String);
|
||||
} catch (Exception ex) {
|
||||
Debug.LogError("Failed to get device name for device " + deviceId + ": " + ex.Message);
|
||||
return base.GetNodeName(node);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void ReadState(XRNodeState node) {
|
||||
if (OpenVR.System == null) {
|
||||
Debug.LogWarning("OpenVR not active");
|
||||
lastState = default(VRControllerState_t);
|
||||
return;
|
||||
}
|
||||
|
||||
var controllerId = GetDeviceId(node);
|
||||
if (controllerId < 0) {
|
||||
lastState = default(VRControllerState_t);
|
||||
return;
|
||||
}
|
||||
|
||||
//Debug.Log("Id is " + controllerId);
|
||||
|
||||
var res = OpenVR.System.GetControllerState(
|
||||
(uint)controllerId, ref lastState,
|
||||
(uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(VRControllerState_t))
|
||||
);
|
||||
|
||||
if (!res) {
|
||||
Debug.LogWarning("Failed to get controller state");
|
||||
}
|
||||
}
|
||||
|
||||
protected override float GetAxisValue(XRNodeState node, InputAxis axis) {
|
||||
ReadState(node);
|
||||
|
||||
switch (axis) {
|
||||
case InputAxis.MainTrigger:
|
||||
return lastState.rAxis1.x;
|
||||
case InputAxis.Grip:
|
||||
return (lastState.ulButtonPressed & (1ul << (int)EVRButtonId.k_EButton_Grip)) != 0 ? 1 : 0;
|
||||
case InputAxis.JoypadX:
|
||||
return lastState.rAxis0.x;
|
||||
case InputAxis.JoypadY:
|
||||
return lastState.rAxis0.y;
|
||||
case InputAxis.Joypad:
|
||||
return (lastState.ulButtonPressed & (1ul << (int)EVRButtonId.k_EButton_SteamVR_Touchpad)) != 0 ? 1 : 0;
|
||||
case InputAxis.Application:
|
||||
return (lastState.ulButtonPressed & (1ul << (int)EVRButtonId.k_EButton_ApplicationMenu)) != 0 ? 1 : 0;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException("axis", axis, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override float GetTouchValue(XRNodeState node, InputAxis axis) {
|
||||
ReadState(node);
|
||||
|
||||
switch (axis) {
|
||||
case InputAxis.Joypad:
|
||||
return (lastState.ulButtonTouched & (1ul << (int)EVRButtonId.k_EButton_SteamVR_Touchpad)) != 0 ? 1 : 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDeviceId(XRNodeState node) {
|
||||
var targetRole = node.nodeType == XRNode.LeftHand ? ETrackedControllerRole.LeftHand : ETrackedControllerRole.RightHand;
|
||||
for (uint i = 0; i < OpenVR.k_unMaxTrackedDeviceCount; i++) {
|
||||
var role = OpenVR.System.GetControllerRoleForTrackedDeviceIndex(i);
|
||||
if (role == targetRole) return (int)i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
#if OCULUS_SDK
|
||||
class OculusVRInput : VRInput {
|
||||
|
||||
protected override float GetAxisValue(XRNodeState node, InputAxis axis) {
|
||||
var controller = GetController(node);
|
||||
OVRPlugin.ControllerState4 state = OVRPlugin.GetControllerState4((uint)controller);
|
||||
|
||||
switch (axis) {
|
||||
case InputAxis.MainTrigger:
|
||||
return controller == OVRInput.Controller.LTouch ? state.LIndexTrigger : state.RIndexTrigger;
|
||||
case InputAxis.Grip:
|
||||
return controller == OVRInput.Controller.LTouch ? state.LHandTrigger : state.RHandTrigger;
|
||||
case InputAxis.JoypadX:
|
||||
case InputAxis.JoypadY: {
|
||||
var joy = controller == OVRInput.Controller.LTouch ? state.LThumbstick : state.RThumbstick;
|
||||
return axis == InputAxis.JoypadX ? joy.x : joy.y;
|
||||
}
|
||||
case InputAxis.Joypad: {
|
||||
var buttonId = controller == OVRInput.Controller.LTouch ? 0x00000400 : 0x00000004; //see enum ovrButton_ in OVER_CAPI.h
|
||||
return (state.Buttons & buttonId) != 0 ? 1 : 0;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private OVRInput.Controller GetController(XRNodeState node) {
|
||||
switch (node.nodeType) {
|
||||
case XRNode.LeftHand:
|
||||
return OVRInput.Controller.LTouch;
|
||||
case XRNode.RightHand:
|
||||
return OVRInput.Controller.RTouch;
|
||||
default:
|
||||
return OVRInput.Controller.None;
|
||||
}
|
||||
}
|
||||
|
||||
protected override float GetTouchValue(XRNodeState node, InputAxis axis) {
|
||||
//nothing touch-related is presently used for Oculus Touch controllers, so nothing is all we need here
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9a740e9fb5a3b4f49b1074b056b4f38d
|
||||
timeCreated: 1510961690
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bcf96c9375432ce40a269198b2c4d89c
|
||||
timeCreated: 1510959402
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue