```
feat(gameplay): 更新单位属性和材质配置 - 调整多个单位的基础生命值和攻击力数值平衡 - 修改CoreBattleGround材质的纹理缩放比例 - 更新地砖材质颜色为白色 - 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持 - 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法 - 为水晶矿添加中立阵营设置和光球视觉效果同步 - 调整预制体中的全局缩放参数 - 为狼人和石巨人添加NavMeshAgent组件支持 - 优化水晶矿血条显示位置计算方式 ```
This commit is contained in:
1 parent
0e7de5f885
commit
53d95802e5
448 files changed
+133333
-472
No files matched your search
+511
@@ -0,0 +1,511 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
internal static class NativeDllLoader
|
||||
{
|
||||
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
|
||||
// ---- Platform P/Invoke ----------------------------------------- //
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
private const uint GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002;
|
||||
|
||||
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr LoadLibraryW(string lpLibFileName);
|
||||
|
||||
[DllImport("kernel32", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FreeLibrary(IntPtr hModule);
|
||||
|
||||
[DllImport("kernel32", SetLastError = true)]
|
||||
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
|
||||
|
||||
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetModuleHandleExW(uint dwFlags, string lpModuleName, out IntPtr phModule);
|
||||
|
||||
private static string Win32Error() =>
|
||||
new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()).Message;
|
||||
|
||||
private static IntPtr PlatformLoad(string path)
|
||||
{
|
||||
IntPtr h = LoadLibraryW(path);
|
||||
if (h == IntPtr.Zero)
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: LoadLibraryW failed: {Win32Error()}");
|
||||
return h;
|
||||
}
|
||||
private static void PlatformFree(IntPtr handle) { if (handle != IntPtr.Zero) FreeLibrary(handle); }
|
||||
private static IntPtr PlatformGetProc(IntPtr handle, string symbol)
|
||||
{
|
||||
IntPtr ptr = GetProcAddress(handle, symbol);
|
||||
if (ptr == IntPtr.Zero)
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: GetProcAddress('{symbol}') failed: {Win32Error()}");
|
||||
return ptr;
|
||||
}
|
||||
// Path-based probe: returns true iff the module at `path` is still loaded with the
|
||||
// given `expected` handle. UNCHANGED_REFCOUNT avoids bumping the refcount, so this
|
||||
// is purely a query — never call FreeLibrary on the result.
|
||||
private static bool PlatformIsHandleStillLoaded(string path, IntPtr expected)
|
||||
{
|
||||
if (expected == IntPtr.Zero) return false;
|
||||
return GetModuleHandleExW(
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
path,
|
||||
out IntPtr current)
|
||||
&& current == expected;
|
||||
}
|
||||
|
||||
#elif UNITY_EDITOR_OSX
|
||||
private const int RTLD_NOW = 0x2;
|
||||
private const int RTLD_NOLOAD = 0x10; // dyld value (differs from glibc)
|
||||
|
||||
[DllImport("libdl")]
|
||||
private static extern IntPtr dlopen(string filename, int flags);
|
||||
|
||||
[DllImport("libdl")]
|
||||
private static extern int dlclose(IntPtr handle);
|
||||
|
||||
[DllImport("libdl")]
|
||||
private static extern IntPtr dlsym(IntPtr handle, string symbol);
|
||||
|
||||
[DllImport("libdl")]
|
||||
private static extern IntPtr dlerror();
|
||||
|
||||
private static string DlError() { IntPtr p = dlerror(); return p != IntPtr.Zero ? Marshal.PtrToStringAnsi(p) : "unknown error"; }
|
||||
|
||||
private static IntPtr PlatformLoad(string path)
|
||||
{
|
||||
dlerror();
|
||||
IntPtr h = dlopen(path, RTLD_NOW);
|
||||
if (h == IntPtr.Zero)
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: dlopen failed: {DlError()}");
|
||||
return h;
|
||||
}
|
||||
private static void PlatformFree(IntPtr handle) { if (handle != IntPtr.Zero) dlclose(handle); }
|
||||
private static IntPtr PlatformGetProc(IntPtr handle, string symbol)
|
||||
{
|
||||
dlerror();
|
||||
IntPtr ptr = dlsym(handle, symbol);
|
||||
if (ptr == IntPtr.Zero)
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: dlsym('{symbol}') failed: {DlError()}");
|
||||
return ptr;
|
||||
}
|
||||
// Path-based probe via dlopen(RTLD_NOLOAD): returns the handle iff the image is
|
||||
// already mapped at `path`, NULL otherwise. NOLOAD still bumps the refcount on
|
||||
// success, so we balance with dlclose immediately. Never call PlatformFree on
|
||||
// anything other than handles we explicitly loaded.
|
||||
private static bool PlatformIsHandleStillLoaded(string path, IntPtr expected)
|
||||
{
|
||||
if (expected == IntPtr.Zero) return false;
|
||||
dlerror();
|
||||
IntPtr probe = dlopen(path, RTLD_NOW | RTLD_NOLOAD);
|
||||
if (probe == IntPtr.Zero) return false;
|
||||
bool matches = probe == expected;
|
||||
dlclose(probe);
|
||||
return matches;
|
||||
}
|
||||
|
||||
#else // UNITY_EDITOR_LINUX
|
||||
private const int RTLD_NOW = 0x2;
|
||||
private const int RTLD_NOLOAD = 0x4; // glibc value (differs from dyld)
|
||||
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern IntPtr dlopen(string filename, int flags);
|
||||
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern int dlclose(IntPtr handle);
|
||||
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern IntPtr dlsym(IntPtr handle, string symbol);
|
||||
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern IntPtr dlerror();
|
||||
|
||||
private static string DlError() { IntPtr p = dlerror(); return p != IntPtr.Zero ? Marshal.PtrToStringAnsi(p) : "unknown error"; }
|
||||
|
||||
private static IntPtr PlatformLoad(string path)
|
||||
{
|
||||
dlerror();
|
||||
IntPtr h = dlopen(path, RTLD_NOW);
|
||||
if (h == IntPtr.Zero)
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: dlopen failed: {DlError()}");
|
||||
return h;
|
||||
}
|
||||
private static void PlatformFree(IntPtr handle) { if (handle != IntPtr.Zero) dlclose(handle); }
|
||||
private static IntPtr PlatformGetProc(IntPtr handle, string symbol)
|
||||
{
|
||||
dlerror();
|
||||
IntPtr ptr = dlsym(handle, symbol);
|
||||
if (ptr == IntPtr.Zero)
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: dlsym('{symbol}') failed: {DlError()}");
|
||||
return ptr;
|
||||
}
|
||||
// Path-based probe via dlopen(RTLD_NOLOAD): returns the handle iff the image is
|
||||
// already mapped at `path`, NULL otherwise. NOLOAD still bumps the refcount on
|
||||
// success, so we balance with dlclose immediately. Never call PlatformFree on
|
||||
// anything other than handles we explicitly loaded.
|
||||
private static bool PlatformIsHandleStillLoaded(string path, IntPtr expected)
|
||||
{
|
||||
if (expected == IntPtr.Zero) return false;
|
||||
dlerror();
|
||||
IntPtr probe = dlopen(path, RTLD_NOW | RTLD_NOLOAD);
|
||||
if (probe == IntPtr.Zero) return false;
|
||||
bool matches = probe == expected;
|
||||
dlclose(probe);
|
||||
return matches;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- Event handlers ---------------------------------------------- //
|
||||
|
||||
static NativeDllLoader()
|
||||
{
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
UnityEditor.PackageManager.Events.registeringPackages -= OnPackagesRegistering;
|
||||
UnityEditor.PackageManager.Events.registeringPackages += OnPackagesRegistering;
|
||||
#endif
|
||||
EditorApplication.quitting -= OnEditorQuitting;
|
||||
EditorApplication.quitting += OnEditorQuitting;
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
|
||||
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
|
||||
}
|
||||
private static void OnEditorQuitting() => Unload("unity_quit");
|
||||
|
||||
private static void OnBeforeAssemblyReload()
|
||||
{
|
||||
UnregisterLogCallback();
|
||||
}
|
||||
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
private static void OnPackagesRegistering(UnityEditor.PackageManager.PackageRegistrationEventArgs args)
|
||||
{
|
||||
// changedFrom / changedTo are paired. Restart only when this
|
||||
// package's native plugin bytes actually change. Removal is
|
||||
// args.removed only.
|
||||
UnityEditor.PackageManager.PackageInfo current;
|
||||
try { current = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(NativeDllLoader).Assembly); }
|
||||
catch { return; }
|
||||
string ourName = current?.name;
|
||||
if (string.IsNullOrEmpty(ourName)) return;
|
||||
|
||||
int changeCount = Math.Min(args.changedFrom.Count, args.changedTo.Count);
|
||||
for (int i = 0; i < changeCount; i++)
|
||||
{
|
||||
var from = args.changedFrom[i];
|
||||
var to = args.changedTo[i];
|
||||
if (from.name != ourName || to.name != ourName) continue;
|
||||
|
||||
if (BridgeUpdateRestartPrompt.IsNativeDllContentChanged(
|
||||
PluginPathUnder(from.resolvedPath),
|
||||
PluginPathUnder(to.resolvedPath)))
|
||||
{
|
||||
BridgeUpdateRestartPrompt.MarkUpdated();
|
||||
Unload("package_updating");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool added = false;
|
||||
foreach (var package in args.added)
|
||||
{
|
||||
if (package.name != ourName) continue;
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
if (added) return;
|
||||
foreach (var package in args.removed)
|
||||
{
|
||||
if (package.name != ourName) continue;
|
||||
Unload("package_removed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- State ----------------------------------------------------- //
|
||||
|
||||
private const string HandleKey = "NativeDllLoader._handle";
|
||||
|
||||
private static IntPtr _handle = IntPtr.Zero;
|
||||
|
||||
internal static bool IsLoaded => _handle != IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Fired just before the native library is unloaded.
|
||||
/// Subscribers (e.g. UnityTcpBridge) should call their own Stop() here
|
||||
/// so higher-level teardown happens before the handle is freed.
|
||||
/// The argument is why the unload is happening (e.g. "unity_quit",
|
||||
/// "package_updating"), destined for the heartbeat file's "reason"
|
||||
/// field; null means no specific reason (generic "stopped").
|
||||
/// </summary>
|
||||
internal static event Action<string> OnUnload;
|
||||
|
||||
// ---- Log callback ---------------------------------------------- //
|
||||
|
||||
// Native setter — resolved from "Log_SetCallback" and held here so callers can reference NativeDllLoader.SetLogCallback directly.
|
||||
internal delegate void SetLogCallbackDelegate(IntPtr callback);
|
||||
internal static SetLogCallbackDelegate SetLogCallback;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void NativeLogCallbackDelegate(int level, [MarshalAs(UnmanagedType.LPStr)] string message);
|
||||
|
||||
// Kept as a field to prevent GC collection while native holds the pointer.
|
||||
private static NativeLogCallbackDelegate _logCallback;
|
||||
|
||||
private static void RegisterLogCallback()
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
try
|
||||
{
|
||||
_logCallback = (level, message) =>
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case 0: CodelyLogger.Log($"[NativeTcpBridge] {message}"); break;
|
||||
case 1: CodelyLogger.LogWarning($"[NativeTcpBridge] {message}"); break;
|
||||
default: CodelyLogger.LogError($"[NativeTcpBridge] {message}"); break;
|
||||
}
|
||||
};
|
||||
IntPtr ptr = Marshal.GetFunctionPointerForDelegate(_logCallback);
|
||||
SetLogCallback?.Invoke(ptr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: RegisterLogCallback failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnregisterLogCallback()
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
try { SetLogCallback?.Invoke(IntPtr.Zero); } catch { }
|
||||
_logCallback = null;
|
||||
}
|
||||
|
||||
// ---- Public API ------------------------------------------------ //
|
||||
|
||||
internal static bool Load()
|
||||
{
|
||||
if (_handle != IntPtr.Zero) return true;
|
||||
|
||||
string dllPath = FindDllPath();
|
||||
if (string.IsNullOrEmpty(dllPath)) return false;
|
||||
|
||||
// On domain reload the DLL stays loaded in the process. Recover the handle
|
||||
// from SessionState so we can re-resolve delegates without reloading the binary.
|
||||
// Validate via a path-based probe first — a package update or other unload may
|
||||
// have invalidated the handle, and calling FreeLibrary / dlclose on a freed
|
||||
// handle is undefined behavior.
|
||||
string stored = SessionState.GetString(HandleKey, "0");
|
||||
if (long.TryParse(stored, out long handleValue) && handleValue != 0)
|
||||
{
|
||||
var storedHandle = new IntPtr(handleValue);
|
||||
if (PlatformIsHandleStillLoaded(dllPath, storedHandle))
|
||||
{
|
||||
_handle = storedHandle;
|
||||
if (ResolveAll())
|
||||
{
|
||||
CodelyLogger.Log("NativeDllLoader: resolved all symbols with stored handle");
|
||||
RegisterLogCallback();
|
||||
BridgeUpdateRestartPrompt.RememberNativeDllFromFile(dllPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle is valid but symbols are missing (rare ABI mismatch). The handle
|
||||
// really refers to our module, so freeing it here is safe.
|
||||
PlatformFree(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
CodelyLogger.Log("NativeDllLoader: stored native handle is no longer loaded; reloading from disk");
|
||||
}
|
||||
SessionState.EraseString(HandleKey);
|
||||
}
|
||||
|
||||
bool isLoaded = LoadFrom(dllPath);
|
||||
if(isLoaded) {
|
||||
RegisterLogCallback();
|
||||
BridgeUpdateRestartPrompt.RememberNativeDllFromFile(dllPath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool Reload()
|
||||
{
|
||||
Unload();
|
||||
return Load();
|
||||
}
|
||||
|
||||
internal static void Unload(string reason = null)
|
||||
{
|
||||
if (_handle == IntPtr.Zero) return;
|
||||
OnUnload?.Invoke(reason);
|
||||
UnregisterLogCallback();
|
||||
PlatformFree(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
NativeUnityTcpBridgeAPI.NTB_GetAbiVersion = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_StartBridge = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_Stop = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_StopWithReason = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_IsRunning = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_GetBoundPort = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_GetConnectionCount = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_TryDequeueCommand = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_TryBeginCommand = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_EnqueueResponse = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_GetClientsJson = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_PauseManagedCommandProcessing = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_ResumeManagedCommandProcessing = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_NotifyAll = null;
|
||||
NativeUnityTcpBridgeAPI.NTB_SetStreamPort = null;
|
||||
SetLogCallback = null;
|
||||
SessionState.EraseString(HandleKey);
|
||||
}
|
||||
|
||||
// ---- Private helpers ------------------------------------------- //
|
||||
|
||||
private static bool LoadFrom(string dllPath)
|
||||
{
|
||||
_handle = PlatformLoad(dllPath);
|
||||
if (_handle == IntPtr.Zero)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: failed to load '{dllPath}'.");
|
||||
return false;
|
||||
}
|
||||
if (!ResolveAll())
|
||||
{
|
||||
PlatformFree(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
return false;
|
||||
}
|
||||
SessionState.SetString(HandleKey, _handle.ToInt64().ToString());
|
||||
CodelyLogger.Log("NativeDllLoader: loaded DLL and resolved all symbols");
|
||||
return true;
|
||||
}
|
||||
|
||||
// The ABI handshake must come first: it is the only symbol whose
|
||||
// signature is guaranteed to match across versions, so it has to be
|
||||
// checked before any other symbol is *called*. (Resolving the rest is
|
||||
// harmless on its own — GetDelegateForFunctionPointer does not call
|
||||
// into native — but a mismatched call would read arguments that were
|
||||
// never passed.) Failing here is the designed outcome on a version
|
||||
// skew, not an error path worth recovering from.
|
||||
private static bool ResolveAll()
|
||||
{
|
||||
if (!CheckAbiVersion()) return false;
|
||||
|
||||
return
|
||||
Resolve("NTB_StartBridge", ref NativeUnityTcpBridgeAPI.NTB_StartBridge) &&
|
||||
Resolve("NTB_StopWithReason", ref NativeUnityTcpBridgeAPI.NTB_StopWithReason) &&
|
||||
Resolve("NTB_Stop", ref NativeUnityTcpBridgeAPI.NTB_Stop) &&
|
||||
Resolve("NTB_IsRunning", ref NativeUnityTcpBridgeAPI.NTB_IsRunning) &&
|
||||
Resolve("NTB_GetBoundPort", ref NativeUnityTcpBridgeAPI.NTB_GetBoundPort) &&
|
||||
Resolve("NTB_GetConnectionCount", ref NativeUnityTcpBridgeAPI.NTB_GetConnectionCount) &&
|
||||
Resolve("NTB_TryDequeueCommand", ref NativeUnityTcpBridgeAPI.NTB_TryDequeueCommand) &&
|
||||
Resolve("NTB_TryBeginCommand", ref NativeUnityTcpBridgeAPI.NTB_TryBeginCommand) &&
|
||||
Resolve("NTB_EnqueueResponse", ref NativeUnityTcpBridgeAPI.NTB_EnqueueResponse) &&
|
||||
Resolve("NTB_GetClientsJson", ref NativeUnityTcpBridgeAPI.NTB_GetClientsJson) &&
|
||||
Resolve("NTB_PauseManagedCommandProcessing", ref NativeUnityTcpBridgeAPI.NTB_PauseManagedCommandProcessing) &&
|
||||
Resolve("NTB_ResumeManagedCommandProcessing", ref NativeUnityTcpBridgeAPI.NTB_ResumeManagedCommandProcessing) &&
|
||||
Resolve("NTB_NotifyAll", ref NativeUnityTcpBridgeAPI.NTB_NotifyAll) &&
|
||||
Resolve("NTB_SetStreamPort", ref NativeUnityTcpBridgeAPI.NTB_SetStreamPort) &&
|
||||
Resolve("Log_SetCallback", ref SetLogCallback);
|
||||
}
|
||||
|
||||
private static bool CheckAbiVersion()
|
||||
{
|
||||
if (!Resolve("NTB_GetAbiVersion", ref NativeUnityTcpBridgeAPI.NTB_GetAbiVersion))
|
||||
{
|
||||
CodelyLogger.LogWarning(
|
||||
"NativeDllLoader: native plugin predates ABI versioning (no NTB_GetAbiVersion); refusing to bind.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int nativeAbi;
|
||||
try
|
||||
{
|
||||
nativeAbi = NativeUnityTcpBridgeAPI.NTB_GetAbiVersion();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: ABI version probe failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nativeAbi != NativeUnityTcpBridgeAPI.ExpectedAbiVersion)
|
||||
{
|
||||
CodelyLogger.LogWarning(
|
||||
$"NativeDllLoader: native ABI {nativeAbi} does not match the expected {NativeUnityTcpBridgeAPI.ExpectedAbiVersion}; " +
|
||||
"refusing to bind. If this appears during startup right after a package update, it is expected and harmless: " +
|
||||
"Unity is running a previously compiled assembly against the updated plugin, and the bridge will start normally " +
|
||||
"once script compilation finishes and the domain reloads.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Resolve<T>(string symbol, ref T del) where T : Delegate
|
||||
{
|
||||
IntPtr ptr = PlatformGetProc(_handle, symbol);
|
||||
if (ptr == IntPtr.Zero) return false;
|
||||
del = Marshal.GetDelegateForFunctionPointer<T>(ptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string PluginPathUnder(string packageRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(packageRoot)) return null;
|
||||
#if UNITY_EDITOR_WIN
|
||||
string relative = Path.Combine("Plugins", "Win", "NativeTcpBridge.dll");
|
||||
#elif UNITY_EDITOR_OSX
|
||||
string relative = Path.Combine("Plugins", "macOS", "libNativeTcpBridge.dylib");
|
||||
#else
|
||||
string relative = Path.Combine("Plugins", "Linux", "x86_64", "libNativeTcpBridge.so");
|
||||
#endif
|
||||
return Path.GetFullPath(Path.Combine(packageRoot, relative));
|
||||
}
|
||||
|
||||
private static string FindDllPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(NativeDllLoader).Assembly);
|
||||
if (packageInfo == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("NativeDllLoader: could not locate package via PackageInfo.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string fullPath = PluginPathUnder(packageInfo.resolvedPath);
|
||||
if (!string.IsNullOrEmpty(fullPath) && File.Exists(fullPath)) return fullPath;
|
||||
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: DLL not found at '{fullPath}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeDllLoader: path lookup failed. {ex.Message}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#else
|
||||
// ---- Unsupported platform stubs -------------------------------- //
|
||||
internal static bool IsLoaded => false;
|
||||
internal static bool Load() => false;
|
||||
internal static bool Reload() => false;
|
||||
internal static void Unload(string reason = null) { }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4dbfef085167ee4a847caa783ef5e58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
// P/Invoke surface for the unified NativeTcpBridge (NTB_*) C ABI.
|
||||
// One TCP listener serves both inbound commands and outbound notifications.
|
||||
internal static class NativeUnityTcpBridgeAPI
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
// Must equal NTB_ABI_VERSION in NativeTcpBridge.h. NativeDllLoader
|
||||
// reads the native value before calling anything else and refuses to
|
||||
// bind on a mismatch — see the ABI stability note in that header.
|
||||
//
|
||||
// The mismatch is not hypothetical bookkeeping: Unity 2019 runs the
|
||||
// previously compiled assembly once against a freshly updated native
|
||||
// plugin before it recompiles, so this exact check is what stands
|
||||
// between a version skew and a call with the wrong argument count.
|
||||
internal const int ExpectedAbiVersion = 2;
|
||||
|
||||
// FROZEN — never change this signature; it is the handshake that lets
|
||||
// every other signature here change safely. See NativeTcpBridge.h.
|
||||
internal delegate int GetAbiVersionDelegate();
|
||||
|
||||
// heartbeatFilePathUtf8Z/projectPathUtf8Z are UTF-8, null-terminated
|
||||
// byte arrays (see NativeUnityTcpBridgeHost.Utf8Z) — not the raw .NET
|
||||
// string marshaler, to match this file's existing byte[]-buffer
|
||||
// convention. Idempotent: if the native singleton is already running
|
||||
// (e.g. reattaching after a domain reload), returns the existing
|
||||
// bound port without re-binding and without touching the heartbeat
|
||||
// writer, whose lifetime matches the TCP listener's exactly.
|
||||
//
|
||||
// Named "StartBridge", not "Start": the native entry point was renamed
|
||||
// when it gained the heartbeat parameters, so that assemblies compiled
|
||||
// against the old 2-arg NTB_Start fail to resolve it and shut down
|
||||
// cleanly instead of calling it with the wrong argument count. See the
|
||||
// ABI stability note in NativeTcpBridge.h before renaming or changing
|
||||
// the signature of anything here.
|
||||
internal delegate int StartBridgeDelegate(int requestedPort, int maxFrameBytes, byte[] heartbeatFilePathUtf8Z, byte[] projectPathUtf8Z, int heartbeatIntervalMs, int streamPort);
|
||||
internal delegate void StopDelegate();
|
||||
// Added after ABI v1 shipped, deliberately without a version bump:
|
||||
// additive entry points can't cause the argument-count hazard the
|
||||
// guard exists for (a stale assembly never resolves a name it wasn't
|
||||
// compiled with). Resolved as optional — see ResolveAll — so callers
|
||||
// must null-check and fall back to NTB_Stop, though with managed and
|
||||
// native always shipping together it is only ever null on a dev-time
|
||||
// skew. reasonUtf8Z is UTF-8, null-terminated (see
|
||||
// NativeUnityTcpBridgeHost.Utf8Z); it lands in the heartbeat file's
|
||||
// "reason" field while stopped.
|
||||
internal delegate void StopWithReasonDelegate(byte[] reasonUtf8Z);
|
||||
internal delegate int IsRunningDelegate();
|
||||
internal delegate int GetBoundPortDelegate();
|
||||
internal delegate int GetConnectionCountDelegate();
|
||||
internal delegate int TryDequeueCommandDelegate(out ulong outRequestId, byte[] buffer, int bufferCapacity, out int outPayloadBytes);
|
||||
internal delegate int TryBeginCommandDelegate(ulong requestId);
|
||||
internal delegate int EnqueueResponseDelegate(ulong requestId, byte[] payload, int payloadBytes);
|
||||
internal delegate int GetClientsJsonDelegate(byte[] buffer, int bufferCapacity, out int outBytes);
|
||||
internal delegate void PauseManagedCommandProcessingDelegate();
|
||||
internal delegate void ResumeManagedCommandProcessingDelegate();
|
||||
internal delegate int NotifyAllDelegate(byte[] payload, int payloadBytes);
|
||||
// The only piece of native heartbeat state meant to change after
|
||||
// NTB_StartBridge — safe to call at any time.
|
||||
internal delegate void SetStreamPortDelegate(int streamPort);
|
||||
|
||||
internal static GetAbiVersionDelegate NTB_GetAbiVersion;
|
||||
internal static StartBridgeDelegate NTB_StartBridge;
|
||||
internal static StopDelegate NTB_Stop;
|
||||
internal static StopWithReasonDelegate NTB_StopWithReason;
|
||||
internal static IsRunningDelegate NTB_IsRunning;
|
||||
internal static GetBoundPortDelegate NTB_GetBoundPort;
|
||||
internal static GetConnectionCountDelegate NTB_GetConnectionCount;
|
||||
internal static TryDequeueCommandDelegate NTB_TryDequeueCommand;
|
||||
internal static TryBeginCommandDelegate NTB_TryBeginCommand;
|
||||
internal static EnqueueResponseDelegate NTB_EnqueueResponse;
|
||||
internal static GetClientsJsonDelegate NTB_GetClientsJson;
|
||||
internal static PauseManagedCommandProcessingDelegate NTB_PauseManagedCommandProcessing;
|
||||
internal static ResumeManagedCommandProcessingDelegate NTB_ResumeManagedCommandProcessing;
|
||||
internal static NotifyAllDelegate NTB_NotifyAll;
|
||||
internal static SetStreamPortDelegate NTB_SetStreamPort;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c267a73ff2c54b93b0242fa0c945f713
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
using UnityTcp.Editor.Helpers;
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
// Managed wrapper around the unified NativeTcpBridge (NTB_*) C ABI.
|
||||
// One TCP listener serves both inbound commands and outbound notifications;
|
||||
// clients that advertise CLIENT_VERSION>=2 are eligible for notifications.
|
||||
internal static class NativeUnityTcpBridgeHost
|
||||
{
|
||||
private const int InboundBufferSize = 8 * 1024 * 1024;
|
||||
private const int ClientSnapshotBufferSize = 512 * 1024;
|
||||
|
||||
private static readonly byte[] InboundBuffer = new byte[InboundBufferSize];
|
||||
private static readonly byte[] ClientSnapshotBuffer = new byte[ClientSnapshotBufferSize];
|
||||
|
||||
private sealed class ClientSnapshot
|
||||
{
|
||||
public List<ClientSnapshotEntry> clients { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ClientSnapshotEntry
|
||||
{
|
||||
public ulong id { get; set; }
|
||||
public string endpoint { get; set; }
|
||||
public string platform { get; set; }
|
||||
public int version { get; set; }
|
||||
}
|
||||
|
||||
// ---- Start / Stop ---------------------------------------------- //
|
||||
|
||||
// heartbeatFilePath/projectPath configure the handshake file written
|
||||
// on a fresh bind (see NTB_StartBridge's doc comment). These only
|
||||
// take effect on a true fresh start, not a reattach.
|
||||
// heartbeatIntervalMs is accepted for ABI compatibility and ignored.
|
||||
public static bool StartOrAttach(
|
||||
int requestedPort, int maxFrameBytes,
|
||||
string heartbeatFilePath, string projectPath, int heartbeatIntervalMs, int streamPort,
|
||||
out int boundPort)
|
||||
{
|
||||
boundPort = 0;
|
||||
if (!NativeDllLoader.IsLoaded && !NativeDllLoader.Load())
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge: failed to load native library. Ensure the plugin is installed correctly.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
// After a domain reload the managed side restarts but the native server
|
||||
// remains bound to its existing port. Probe first so we attach to the
|
||||
// existing listener instead of relying solely on NTB_StartBridge's idempotency.
|
||||
if (IsRunning())
|
||||
{
|
||||
int existingPort = GetBoundPort();
|
||||
if (existingPort > 0)
|
||||
{
|
||||
boundPort = existingPort;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int startResult = NativeUnityTcpBridgeAPI.NTB_StartBridge(
|
||||
requestedPort, maxFrameBytes,
|
||||
Utf8Z(heartbeatFilePath), Utf8Z(projectPath), heartbeatIntervalMs, streamPort);
|
||||
if (startResult <= 0) return false;
|
||||
boundPort = startResult;
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogError($"NativeTcpBridge start failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// reason, when given, ends up in the heartbeat file's "reason" field
|
||||
// for as long as the bridge stays stopped (e.g. "manually_stopped",
|
||||
// "unity_quit", "package_updating"), so clients can tell a deliberate
|
||||
// shutdown from an unexpected one. Null keeps the generic "stopped".
|
||||
public static void StopServer(string reason = null)
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded) return;
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
// NTB_StopWithReason is optional (older DLLs predate it and
|
||||
// ship the same ABI version) — fall back to plain NTB_Stop.
|
||||
if (!string.IsNullOrEmpty(reason) && NativeUnityTcpBridgeAPI.NTB_StopWithReason != null)
|
||||
NativeUnityTcpBridgeAPI.NTB_StopWithReason(Utf8Z(reason));
|
||||
else
|
||||
NativeUnityTcpBridgeAPI.NTB_Stop();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge stop failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Query methods --------------------------------------------- //
|
||||
|
||||
public static bool IsRunning()
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return NativeUnityTcpBridgeAPI.NTB_IsRunning() == 1;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public static int GetBoundPort()
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return NativeUnityTcpBridgeAPI.NTB_GetBoundPort?.Invoke() ?? 0;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
public static int GetConnectionCount()
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Math.Max(0, NativeUnityTcpBridgeAPI.NTB_GetConnectionCount());
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
// ---- Command queue --------------------------------------------- //
|
||||
|
||||
public static bool TryDequeueCommand(out ulong requestId, out string commandText)
|
||||
{
|
||||
requestId = 0;
|
||||
commandText = null;
|
||||
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
if (NativeUnityTcpBridgeAPI.NTB_TryDequeueCommand(
|
||||
out requestId,
|
||||
InboundBuffer,
|
||||
InboundBuffer.Length,
|
||||
out int payloadBytes) != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payloadBytes <= 0 || payloadBytes > InboundBuffer.Length) return false;
|
||||
|
||||
commandText = Encoding.UTF8.GetString(InboundBuffer, 0, payloadBytes);
|
||||
return !string.IsNullOrEmpty(commandText);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge dequeue failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryBeginCommand(ulong requestId)
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded || requestId == 0) return false;
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return NativeUnityTcpBridgeAPI.NTB_TryBeginCommand(requestId) == 1;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge begin command failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool EnqueueResponse(ulong requestId, string responseJson)
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestId == 0 || string.IsNullOrEmpty(responseJson)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] payload = Encoding.UTF8.GetBytes(responseJson);
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return NativeUnityTcpBridgeAPI.NTB_EnqueueResponse(requestId, payload, payload.Length) == 1;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge enqueue response failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Managed command lifecycle -------------------------------- //
|
||||
|
||||
public static void PauseManagedCommandProcessing()
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
NativeUnityTcpBridgeAPI.NTB_PauseManagedCommandProcessing?.Invoke();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge PauseManagedCommandProcessing failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ResumeManagedCommandProcessing()
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
NativeUnityTcpBridgeAPI.NTB_ResumeManagedCommandProcessing?.Invoke();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge ResumeManagedCommandProcessing failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Notify ---------------------------------------------------- //
|
||||
|
||||
public static bool NotifyAll(string payloadJson)
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(payloadJson)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(payloadJson);
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return NativeUnityTcpBridgeAPI.NTB_NotifyAll(bytes, bytes.Length) == 1;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge NotifyAll failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Connected clients ----------------------------------------- //
|
||||
|
||||
public static Dictionary<string, string> GetConnectedClients()
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
if (!NativeDllLoader.IsLoaded)
|
||||
{
|
||||
CodelyLogger.LogError("NativeTcpBridge is not started. Call Start Bridge first.");
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
if (NativeUnityTcpBridgeAPI.NTB_GetClientsJson(
|
||||
ClientSnapshotBuffer,
|
||||
ClientSnapshotBuffer.Length,
|
||||
out int bytes) != 1)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (bytes <= 0 || bytes > ClientSnapshotBuffer.Length) return result;
|
||||
|
||||
string json = Encoding.UTF8.GetString(ClientSnapshotBuffer, 0, bytes);
|
||||
var snapshot = JsonConvert.DeserializeObject<ClientSnapshot>(json);
|
||||
if (snapshot?.clients == null) return result;
|
||||
|
||||
foreach (var client in snapshot.clients)
|
||||
{
|
||||
if (string.IsNullOrEmpty(client?.endpoint)) continue;
|
||||
result[client.endpoint] = string.IsNullOrEmpty(client.platform) ? "unknown" : client.platform;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge get clients failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---- Heartbeat --------------------------------------------------- //
|
||||
|
||||
// Forwards the NWB (WebRTC/stream) signaling port to native. Kept
|
||||
// for ABI compatibility; the handshake file no longer surfaces
|
||||
// stream_port.
|
||||
public static void SetStreamPort(int streamPort)
|
||||
{
|
||||
if (!NativeDllLoader.IsLoaded) return;
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
NativeUnityTcpBridgeAPI.NTB_SetStreamPort?.Invoke(streamPort);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"NativeTcpBridge SetStreamPort failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// UTF-8, null-terminated — matches the byte[] convention this P/Invoke
|
||||
// surface uses elsewhere, rather than relying on string marshaling
|
||||
// attributes that vary across Mono/IL2CPP versions.
|
||||
private static byte[] Utf8Z(string s) => Encoding.UTF8.GetBytes((s ?? string.Empty) + "\0");
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d7d1713daea4bae8ac66a966cea6298
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// Low-level P/Invoke declarations for the NativeWindowBridge streaming plugin.
|
||||
/// Mirrors NativeWindowBridge.h C ABI exactly.
|
||||
/// </summary>
|
||||
internal static class NativeWindowBridgeAPI
|
||||
{
|
||||
// Keep the delegate type available cross-platform so managed references compile.
|
||||
// On unsupported platforms, NWB_SetLogCallback is a no-op and never reaches native code.
|
||||
internal delegate void NWB_LogCallbackDelegate(int level, string message);
|
||||
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
private const string DllName = "NativeWindowBridge";
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_StartServer")]
|
||||
private static extern int NWB_StartServer_Native(int httpPort);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_StopServer")]
|
||||
private static extern void NWB_StopServer_Native();
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_IsRunning")]
|
||||
private static extern int NWB_IsRunning_Native();
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetBoundPort")]
|
||||
private static extern int NWB_GetBoundPort_Native();
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_UpdateWindowList", CharSet = CharSet.Ansi)]
|
||||
private static extern int NWB_UpdateWindowList_Native(string windowsJson);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_SetUnityPid")]
|
||||
private static extern void NWB_SetUnityPid_Native(int pid);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetWindowListJson")]
|
||||
private static extern int NWB_GetWindowListJson_Native(byte[] buffer, int bufferCapacity, out int outBytes);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetStreamStatusJson")]
|
||||
private static extern int NWB_GetStreamStatusJson_Native(byte[] buffer, int bufferCapacity, out int outBytes);
|
||||
|
||||
// Offscreen rendering mode: C# pushes frames and polls input events.
|
||||
[DllImport(DllName, EntryPoint = "NWB_StartOffscreenCapture")]
|
||||
private static extern int NWB_StartOffscreenCapture_Native(int fps, int width, int height);
|
||||
|
||||
/// <summary>Stops offscreen/OS capture on the native side (resets m_OffscreenMode).</summary>
|
||||
[DllImport(DllName, EntryPoint = "NWB_StopCapture")]
|
||||
private static extern void NWB_StopCapture_Native();
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_PushFrame")]
|
||||
private static extern int NWB_PushFrame_Native(IntPtr bgraData, int width, int height, int stride);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetPendingInput")]
|
||||
private static extern int NWB_GetPendingInput_Native(byte[] buffer, int bufferSize);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetPendingOffscreenRequest")]
|
||||
private static extern int NWB_GetPendingOffscreenRequest_Native(
|
||||
byte[] windowTypeBuf, int bufSize,
|
||||
out int outFps, out int outWidth, out int outHeight, out float outDpr);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetPendingCompositeRequest")]
|
||||
private static extern int NWB_GetPendingCompositeRequest_Native(
|
||||
byte[] jsonBuf, int bufSize,
|
||||
out int outFps, out int outWidth, out int outHeight, out float outDpr);
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_IsOffscreenActive")]
|
||||
private static extern int NWB_IsOffscreenActive_Native();
|
||||
|
||||
// Check for pending resize request from frontend (via /stream/resize).
|
||||
// Returns 1 if pending (and clears it), 0 otherwise.
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetPendingResize")]
|
||||
private static extern int NWB_GetPendingResize_Native(out int outWidth, out int outHeight, out float outDpr);
|
||||
|
||||
// Query the active offscreen capture window type (survives domain reload).
|
||||
// Returns bytes written (excluding null), or 0 if no active offscreen.
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetActiveOffscreenWindowType")]
|
||||
private static extern int NWB_GetActiveOffscreenWindowType_Native(byte[] buffer, int bufferSize);
|
||||
|
||||
// Send a UTF-8 JSON message to the browser via the WebRTC DataChannel.
|
||||
[DllImport(DllName, EntryPoint = "NWB_SendDataChannelMessage")]
|
||||
private static extern int NWB_SendDataChannelMessage_Native(byte[] jsonUtf8, int length);
|
||||
|
||||
// Seconds since the last browser heartbeat arrived via DataChannel.
|
||||
// Tracked in C++ (WebRTC thread), accurate even when C# main thread
|
||||
// is blocked. Returns -1 if no heartbeat has been received yet.
|
||||
[DllImport(DllName, EntryPoint = "NWB_GetSecondsSinceLastHeartbeat")]
|
||||
private static extern float NWB_GetSecondsSinceLastHeartbeat_Native();
|
||||
|
||||
[DllImport(DllName, EntryPoint = "NWB_SetLogCallback")]
|
||||
private static extern void NWB_SetLogCallback_Native(NWB_LogCallbackDelegate callback);
|
||||
#endif
|
||||
|
||||
// Public managed surface. On unsupported platforms this is intentionally
|
||||
// implemented as no-op/default so shared code compiles and does not call native.
|
||||
internal static int NWB_StartServer(int httpPort)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_StartServer_Native(httpPort);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void NWB_StopServer()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
NWB_StopServer_Native();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_IsRunning()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_IsRunning_Native();
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetBoundPort()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetBoundPort_Native();
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_UpdateWindowList(string windowsJson)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_UpdateWindowList_Native(windowsJson);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void NWB_SetUnityPid(int pid)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
NWB_SetUnityPid_Native(pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetWindowListJson(byte[] buffer, int bufferCapacity, out int outBytes)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetWindowListJson_Native(buffer, bufferCapacity, out outBytes);
|
||||
#else
|
||||
outBytes = 0;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetStreamStatusJson(byte[] buffer, int bufferCapacity, out int outBytes)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetStreamStatusJson_Native(buffer, bufferCapacity, out outBytes);
|
||||
#else
|
||||
outBytes = 0;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_StartOffscreenCapture(int fps, int width, int height)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_StartOffscreenCapture_Native(fps, width, height);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void NWB_StopCapture()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
NWB_StopCapture_Native();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_PushFrame(IntPtr bgraData, int width, int height, int stride)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_PushFrame_Native(bgraData, width, height, stride);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetPendingInput(byte[] buffer, int bufferSize)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetPendingInput_Native(buffer, bufferSize);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetPendingOffscreenRequest(
|
||||
byte[] windowTypeBuf, int bufSize, out int outFps, out int outWidth, out int outHeight, out float outDpr)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetPendingOffscreenRequest_Native(windowTypeBuf, bufSize, out outFps, out outWidth, out outHeight, out outDpr);
|
||||
#else
|
||||
outFps = 0;
|
||||
outWidth = 0;
|
||||
outHeight = 0;
|
||||
outDpr = 0f;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetPendingCompositeRequest(
|
||||
byte[] jsonBuf, int bufSize, out int outFps, out int outWidth, out int outHeight, out float outDpr)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetPendingCompositeRequest_Native(jsonBuf, bufSize, out outFps, out outWidth, out outHeight, out outDpr);
|
||||
#else
|
||||
outFps = 0;
|
||||
outWidth = 0;
|
||||
outHeight = 0;
|
||||
outDpr = 0f;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_IsOffscreenActive()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_IsOffscreenActive_Native();
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetPendingResize(out int outWidth, out int outHeight, out float outDpr)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetPendingResize_Native(out outWidth, out outHeight, out outDpr);
|
||||
#else
|
||||
outWidth = 0;
|
||||
outHeight = 0;
|
||||
outDpr = 0f;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_GetActiveOffscreenWindowType(byte[] buffer, int bufferSize)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetActiveOffscreenWindowType_Native(buffer, bufferSize);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static int NWB_SendDataChannelMessage(byte[] jsonUtf8, int length)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_SendDataChannelMessage_Native(jsonUtf8, length);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static float NWB_GetSecondsSinceLastHeartbeat()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
return NWB_GetSecondsSinceLastHeartbeat_Native();
|
||||
#else
|
||||
return -1f;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void NWB_SetLogCallback(NWB_LogCallbackDelegate callback)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
NWB_SetLogCallback_Native(callback);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfd60c322495e4341b9d561a3d2f4ddd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private static GameObject[] s_CachedAddComponentTargets;
|
||||
private static string s_AddComponentSearchFilter = "";
|
||||
private static float s_AddComponentPopupWidth;
|
||||
private static float s_AddComponentPopupHeight;
|
||||
|
||||
private static bool s_AddComponentReflectionDone;
|
||||
private static Type s_AddComponentDataSourceType;
|
||||
private static Type s_ComponentDropdownItemType;
|
||||
private static Type s_AdvancedDropdownDataSourceType;
|
||||
private static Type s_AdvancedDropdownItemType;
|
||||
private static FieldInfo s_AddComponentWindowStateField;
|
||||
private static PropertyInfo s_AdvancedDropdownMainTreeProp;
|
||||
private static PropertyInfo s_AdvancedDropdownSearchTreeProp;
|
||||
private static PropertyInfo s_AdvancedDropdownItemChildrenProp;
|
||||
private static PropertyInfo s_AdvancedDropdownItemDisplayNameProp;
|
||||
private static PropertyInfo s_AdvancedDropdownItemHasChildrenProp;
|
||||
private static PropertyInfo s_ComponentDropdownMenuPathProp;
|
||||
private static MethodInfo s_AdvancedDropdownReloadDataMethod;
|
||||
private static MethodInfo s_AdvancedDropdownRebuildSearchMethod;
|
||||
private static MethodInfo s_ExecuteMenuItemOnGameObjectsMethod;
|
||||
|
||||
private static void EnsureAddComponentReflection()
|
||||
{
|
||||
if (s_AddComponentReflectionDone) return;
|
||||
s_AddComponentReflectionDone = true;
|
||||
|
||||
try
|
||||
{
|
||||
var editorAsm = typeof(EditorWindow).Assembly;
|
||||
s_AddComponentDataSourceType = editorAsm.GetType("UnityEditor.AddComponent.AddComponentDataSource");
|
||||
s_ComponentDropdownItemType = editorAsm.GetType("UnityEditor.AddComponent.ComponentDropdownItem");
|
||||
s_AdvancedDropdownDataSourceType = editorAsm.GetType("UnityEditor.IMGUI.Controls.AdvancedDropdownDataSource");
|
||||
s_AdvancedDropdownItemType = editorAsm.GetType("UnityEditor.IMGUI.Controls.AdvancedDropdownItem");
|
||||
|
||||
Type addComponentWindowType = editorAsm.GetType("UnityEditor.AddComponent.AddComponentWindow");
|
||||
if (addComponentWindowType != null)
|
||||
{
|
||||
s_AddComponentWindowStateField = addComponentWindowType.GetField(
|
||||
"s_State",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (s_AdvancedDropdownDataSourceType != null)
|
||||
{
|
||||
s_AdvancedDropdownMainTreeProp = s_AdvancedDropdownDataSourceType.GetProperty(
|
||||
"mainTree",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
s_AdvancedDropdownSearchTreeProp = s_AdvancedDropdownDataSourceType.GetProperty(
|
||||
"searchTree",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
s_AdvancedDropdownReloadDataMethod = s_AdvancedDropdownDataSourceType.GetMethod(
|
||||
"ReloadData",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
s_AdvancedDropdownRebuildSearchMethod = s_AdvancedDropdownDataSourceType.GetMethod(
|
||||
"RebuildSearch",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (s_AdvancedDropdownItemType != null)
|
||||
{
|
||||
s_AdvancedDropdownItemChildrenProp = s_AdvancedDropdownItemType.GetProperty(
|
||||
"children",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
s_AdvancedDropdownItemHasChildrenProp = s_AdvancedDropdownItemType.GetProperty(
|
||||
"hasChildren",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
for (Type t = s_AdvancedDropdownItemType; t != null && s_AdvancedDropdownItemDisplayNameProp == null; t = t.BaseType)
|
||||
{
|
||||
s_AdvancedDropdownItemDisplayNameProp = t.GetProperty(
|
||||
"displayName",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
}
|
||||
|
||||
if (s_ComponentDropdownItemType != null)
|
||||
{
|
||||
s_ComponentDropdownMenuPathProp = s_ComponentDropdownItemType.GetProperty(
|
||||
"menuPath",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
s_ExecuteMenuItemOnGameObjectsMethod = typeof(EditorApplication).GetMethod(
|
||||
"ExecuteMenuItemOnGameObjects",
|
||||
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
|
||||
null,
|
||||
new[] { typeof(string), typeof(GameObject[]) },
|
||||
null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AddComponent] Reflection init error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExtractAddComponentPopup(
|
||||
EditorWindow hostedWindow, float relX, float relY, float popW, float popH)
|
||||
{
|
||||
EnsureAddComponentReflection();
|
||||
if (hostedWindow == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
FieldInfo targetsField = hostedWindow.GetType().GetField(
|
||||
"m_GameObjects",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
s_CachedAddComponentTargets = targetsField?.GetValue(hostedWindow) as GameObject[];
|
||||
if (s_CachedAddComponentTargets == null || s_CachedAddComponentTargets.Length == 0)
|
||||
{
|
||||
LogVerbose("[NWB-AddComponent] No target GameObjects on AddComponentWindow");
|
||||
return false;
|
||||
}
|
||||
|
||||
PropertyInfo searchProp = hostedWindow.GetType().GetProperty(
|
||||
"searchString",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (searchProp != null && string.IsNullOrEmpty(s_AddComponentSearchFilter))
|
||||
{
|
||||
s_AddComponentSearchFilter = searchProp.GetValue(hostedWindow) as string ?? "";
|
||||
}
|
||||
|
||||
s_AddComponentPopupWidth = popW;
|
||||
s_AddComponentPopupHeight = popH;
|
||||
s_LastFrontendPopupX = relX;
|
||||
s_LastFrontendPopupY = relY;
|
||||
|
||||
return TrySendAddComponentPopup(relX, relY, popW, popH);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AddComponent] Extract error: {ex.Message}\n{ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySendAddComponentPopup(float relX, float relY, float popW, float popH)
|
||||
{
|
||||
EnsureAddComponentReflection();
|
||||
if (s_CachedAddComponentTargets == null || s_CachedAddComponentTargets.Length == 0)
|
||||
return false;
|
||||
if (s_AddComponentDataSourceType == null ||
|
||||
s_AddComponentWindowStateField == null ||
|
||||
s_AdvancedDropdownMainTreeProp == null ||
|
||||
s_AdvancedDropdownReloadDataMethod == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-AddComponent] Required reflection types not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object state = s_AddComponentWindowStateField.GetValue(null);
|
||||
if (state == null)
|
||||
state = Activator.CreateInstance(s_AddComponentWindowStateField.FieldType);
|
||||
|
||||
object dataSource = Activator.CreateInstance(
|
||||
s_AddComponentDataSourceType,
|
||||
state,
|
||||
s_CachedAddComponentTargets);
|
||||
s_AdvancedDropdownReloadDataMethod.Invoke(dataSource, null);
|
||||
|
||||
object mainTree = s_AdvancedDropdownMainTreeProp.GetValue(dataSource);
|
||||
object treeToRender = mainTree;
|
||||
|
||||
if (!string.IsNullOrEmpty(s_AddComponentSearchFilter) &&
|
||||
s_AdvancedDropdownRebuildSearchMethod != null &&
|
||||
mainTree != null)
|
||||
{
|
||||
s_AdvancedDropdownRebuildSearchMethod.Invoke(
|
||||
dataSource,
|
||||
new object[] { s_AddComponentSearchFilter, mainTree });
|
||||
object searchTree = s_AdvancedDropdownSearchTreeProp?.GetValue(dataSource);
|
||||
if (searchTree != null)
|
||||
treeToRender = searchTree;
|
||||
}
|
||||
|
||||
if (treeToRender == null)
|
||||
{
|
||||
LogVerbose("[NWB-AddComponent] Component tree is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
var pathList = new List<string>();
|
||||
int itemIndex = 0;
|
||||
int validCount = 0;
|
||||
|
||||
s_FrontendPopupIdCounter++;
|
||||
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
|
||||
s_PopupSourceType = "add_component";
|
||||
|
||||
var sb = new StringBuilder(65536);
|
||||
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
|
||||
sb.Append(s_CurrentFrontendPopupId);
|
||||
sb.Append("\",\"sourceType\":\"add_component\",\"x\":");
|
||||
sb.Append(Mathf.RoundToInt(relX));
|
||||
sb.Append(",\"y\":");
|
||||
sb.Append(Mathf.RoundToInt(relY));
|
||||
sb.Append(",\"width\":");
|
||||
sb.Append(Mathf.RoundToInt(Mathf.Max(popW, 200f)));
|
||||
sb.Append(",\"height\":");
|
||||
sb.Append(Mathf.RoundToInt(Mathf.Max(popH, 280f)));
|
||||
sb.Append(",\"searchText\":\"");
|
||||
sb.Append(EscapeJsonString(s_AddComponentSearchFilter ?? ""));
|
||||
sb.Append("\",\"items\":[");
|
||||
bool needComma = false;
|
||||
AppendAddComponentDropdownItems(
|
||||
sb,
|
||||
GetAdvancedDropdownChildren(treeToRender),
|
||||
ref itemIndex,
|
||||
ref validCount,
|
||||
ref needComma,
|
||||
pathList);
|
||||
sb.Append("]}");
|
||||
|
||||
if (validCount <= 0)
|
||||
{
|
||||
LogVerbose("[NWB-AddComponent] No selectable component items extracted");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_CachedPopupItemPaths = pathList.ToArray();
|
||||
string payload = sb.ToString();
|
||||
int payloadBytes = Encoding.UTF8.GetByteCount(payload);
|
||||
|
||||
bool sent;
|
||||
if (payloadBytes > kMaxDataChannelPayloadBytes)
|
||||
{
|
||||
sent = SendPopupPayloadChunked(s_CurrentFrontendPopupId, payload, payloadBytes);
|
||||
if (sent)
|
||||
LogVerbose($"[NWB-AddComponent] Sent chunked popup: bytes={payloadBytes} items={validCount}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sent = SendDataChannelMessage(payload);
|
||||
}
|
||||
|
||||
s_FrontendPopupSent = sent;
|
||||
if (sent)
|
||||
{
|
||||
CancelNativeDelayedPopup();
|
||||
LogVerbose($"[NWB-AddComponent] Sent popup: id={s_CurrentFrontendPopupId} items={validCount} search='{s_AddComponentSearchFilter}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AddComponent] Failed to send popup: id={s_CurrentFrontendPopupId}");
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AddComponent] Send error: {ex.Message}\n{ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable GetAdvancedDropdownChildren(object item)
|
||||
{
|
||||
if (item == null || s_AdvancedDropdownItemChildrenProp == null)
|
||||
return new object[0];
|
||||
return s_AdvancedDropdownItemChildrenProp.GetValue(item) as IEnumerable ?? new object[0];
|
||||
}
|
||||
|
||||
private static void AppendAddComponentDropdownItems(
|
||||
StringBuilder sb,
|
||||
IEnumerable items,
|
||||
ref int itemIndex,
|
||||
ref int validCount,
|
||||
ref bool needComma,
|
||||
List<string> pathList)
|
||||
{
|
||||
if (items == null) return;
|
||||
|
||||
foreach (object item in items)
|
||||
{
|
||||
if (item == null) continue;
|
||||
if (item.GetType().Name == "SeparatorDropdownItem")
|
||||
continue;
|
||||
|
||||
string label = GetAdvancedDropdownDisplayName(item);
|
||||
if (string.IsNullOrEmpty(label))
|
||||
continue;
|
||||
|
||||
bool hasChildren = s_AdvancedDropdownItemHasChildrenProp != null &&
|
||||
(bool)s_AdvancedDropdownItemHasChildrenProp.GetValue(item);
|
||||
|
||||
if (hasChildren)
|
||||
{
|
||||
if (needComma) sb.Append(',');
|
||||
needComma = true;
|
||||
sb.Append("{\"label\":\"");
|
||||
sb.Append(EscapeJsonString(label));
|
||||
sb.Append("\",\"enabled\":true,\"checked\":false,\"separator\":false,\"submenu\":true,\"children\":[");
|
||||
bool childComma = false;
|
||||
int childValid = 0;
|
||||
AppendAddComponentDropdownItems(
|
||||
sb,
|
||||
GetAdvancedDropdownChildren(item),
|
||||
ref itemIndex,
|
||||
ref childValid,
|
||||
ref childComma,
|
||||
pathList);
|
||||
sb.Append("]}");
|
||||
if (childValid > 0)
|
||||
validCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s_ComponentDropdownItemType == null ||
|
||||
!s_ComponentDropdownItemType.IsInstanceOfType(item) ||
|
||||
s_ComponentDropdownMenuPathProp == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string menuPath = s_ComponentDropdownMenuPathProp.GetValue(item) as string;
|
||||
if (string.IsNullOrEmpty(menuPath))
|
||||
continue;
|
||||
|
||||
if (needComma) sb.Append(',');
|
||||
needComma = true;
|
||||
sb.Append("{\"index\":");
|
||||
sb.Append(itemIndex);
|
||||
sb.Append(",\"path\":\"");
|
||||
sb.Append(EscapeJsonString(menuPath));
|
||||
sb.Append("\",\"label\":\"");
|
||||
sb.Append(EscapeJsonString(label));
|
||||
sb.Append("\",\"enabled\":true,\"checked\":false,\"separator\":false}");
|
||||
pathList.Add(menuPath);
|
||||
itemIndex++;
|
||||
validCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAdvancedDropdownDisplayName(object item)
|
||||
{
|
||||
if (item == null || s_AdvancedDropdownItemDisplayNameProp == null)
|
||||
return "";
|
||||
return s_AdvancedDropdownItemDisplayNameProp.GetValue(item) as string ?? "";
|
||||
}
|
||||
|
||||
private static bool TryHandleAddComponentPopupSelect(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || s_CachedAddComponentTargets == null)
|
||||
return false;
|
||||
|
||||
EnsureAddComponentReflection();
|
||||
if (s_ExecuteMenuItemOnGameObjectsMethod == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-AddComponent] ExecuteMenuItemOnGameObjects not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bool ok = (bool)s_ExecuteMenuItemOnGameObjectsMethod.Invoke(
|
||||
null,
|
||||
new object[] { path, s_CachedAddComponentTargets });
|
||||
LogVerbose($"[NWB-AddComponent] ExecuteMenuItemOnGameObjects path='{path}' ok={ok}");
|
||||
return ok;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AddComponent] Execute error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearAddComponentPopupState()
|
||||
{
|
||||
s_CachedAddComponentTargets = null;
|
||||
s_AddComponentSearchFilter = "";
|
||||
s_AddComponentPopupWidth = 0f;
|
||||
s_AddComponentPopupHeight = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c743eceaf57cd840917d1baa74322d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+659
@@ -0,0 +1,659 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private enum AdvancedDropdownHandlerKind
|
||||
{
|
||||
StatelessIndex,
|
||||
Multiselect,
|
||||
MaterialShader,
|
||||
}
|
||||
|
||||
private static object s_CachedAdvancedDropdownDataSource;
|
||||
private static AdvancedDropdownHandlerKind s_AdvancedDropdownHandlerKind;
|
||||
private static MaterialEditor s_CachedAdvancedDropdownMaterialEditor;
|
||||
private static string s_AdvancedDropdownSearchFilter = "";
|
||||
private static float s_AdvancedDropdownPopupWidth;
|
||||
private static float s_AdvancedDropdownPopupHeight;
|
||||
|
||||
private static bool s_AdvancedDropdownWindowReflectionDone;
|
||||
private static PropertyInfo s_AdvancedDropdownWindowDataSourceProp;
|
||||
private static PropertyInfo s_AdvancedDropdownWindowSearchProp;
|
||||
private static PropertyInfo s_AdvancedDropdownItemElementIndexProp;
|
||||
private static PropertyInfo s_AdvancedDropdownItemIdProp;
|
||||
private static PropertyInfo s_AdvancedDropdownDataSourceSelectedIdsProp;
|
||||
private static PropertyInfo s_ShaderDropdownItemFullNameProp;
|
||||
private static FieldInfo s_MaterialEditorsField;
|
||||
private static FieldInfo s_MaterialEditorShaderField;
|
||||
private static FieldInfo s_ShaderDropdownCurrentShaderField;
|
||||
private static MethodInfo s_MaterialEditorOnSelectedShaderPopupMethod;
|
||||
private static MethodInfo s_MultiselectUpdateSelectedIdMethod;
|
||||
private static Type s_StatelessAdvancedDropdownType;
|
||||
private static FieldInfo s_StatelessAdvancedDropdownResultField;
|
||||
private static FieldInfo s_StatelessAdvancedDropdownWindowClosedField;
|
||||
private static FieldInfo s_StatelessAdvancedDropdownShouldReturnValueField;
|
||||
private static FieldInfo s_StatelessAdvancedDropdownParentWindowField;
|
||||
|
||||
private static void EnsureAdvancedDropdownWindowReflection()
|
||||
{
|
||||
if (s_AdvancedDropdownWindowReflectionDone) return;
|
||||
s_AdvancedDropdownWindowReflectionDone = true;
|
||||
|
||||
try
|
||||
{
|
||||
EnsureAddComponentReflection();
|
||||
|
||||
var editorAsm = typeof(EditorWindow).Assembly;
|
||||
Type advancedDropdownWindowType = editorAsm.GetType(
|
||||
"UnityEditor.IMGUI.Controls.AdvancedDropdownWindow");
|
||||
if (advancedDropdownWindowType != null)
|
||||
{
|
||||
s_AdvancedDropdownWindowDataSourceProp = advancedDropdownWindowType.GetProperty(
|
||||
"dataSource",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
s_AdvancedDropdownWindowSearchProp = advancedDropdownWindowType.GetProperty(
|
||||
"searchString",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (s_AdvancedDropdownItemType != null)
|
||||
{
|
||||
s_AdvancedDropdownItemElementIndexProp = s_AdvancedDropdownItemType.GetProperty(
|
||||
"elementIndex",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
s_AdvancedDropdownItemIdProp = s_AdvancedDropdownItemType.GetProperty(
|
||||
"id",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (s_AdvancedDropdownDataSourceType != null)
|
||||
{
|
||||
s_AdvancedDropdownDataSourceSelectedIdsProp = s_AdvancedDropdownDataSourceType.GetProperty(
|
||||
"selectedIDs",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
s_StatelessAdvancedDropdownType = editorAsm.GetType("UnityEditor.StatelessAdvancedDropdown");
|
||||
if (s_StatelessAdvancedDropdownType != null)
|
||||
{
|
||||
const BindingFlags staticFlags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
|
||||
s_StatelessAdvancedDropdownResultField = s_StatelessAdvancedDropdownType.GetField("m_Result", staticFlags);
|
||||
s_StatelessAdvancedDropdownWindowClosedField = s_StatelessAdvancedDropdownType.GetField("m_WindowClosed", staticFlags);
|
||||
s_StatelessAdvancedDropdownShouldReturnValueField = s_StatelessAdvancedDropdownType.GetField("m_ShouldReturnValue", staticFlags);
|
||||
s_StatelessAdvancedDropdownParentWindowField = s_StatelessAdvancedDropdownType.GetField("s_ParentWindow", staticFlags);
|
||||
}
|
||||
|
||||
Type multiselectDataSourceType = editorAsm.GetType(
|
||||
"UnityEditor.IMGUI.Controls.MultiselectDataSource");
|
||||
if (multiselectDataSourceType != null)
|
||||
{
|
||||
s_MultiselectUpdateSelectedIdMethod = multiselectDataSourceType.GetMethod(
|
||||
"UpdateSelectedId",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
foreach (Type nested in typeof(MaterialEditor).GetNestedTypes(
|
||||
BindingFlags.NonPublic | BindingFlags.Public))
|
||||
{
|
||||
if (nested.Name == "ShaderDropdownItem")
|
||||
{
|
||||
s_ShaderDropdownItemFullNameProp = nested.GetProperty(
|
||||
"fullName",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Type nested in typeof(MaterialEditor).GetNestedTypes(
|
||||
BindingFlags.NonPublic | BindingFlags.Public))
|
||||
{
|
||||
if (nested.Name != "ShaderSelectionDropdown") continue;
|
||||
foreach (Type inner in nested.GetNestedTypes(
|
||||
BindingFlags.NonPublic | BindingFlags.Public))
|
||||
{
|
||||
if (inner.Name == "ShaderDropdownDataSource")
|
||||
{
|
||||
s_ShaderDropdownCurrentShaderField = inner.GetField(
|
||||
"m_CurrentShader",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
s_MaterialEditorsField = typeof(MaterialEditor).GetField(
|
||||
"s_MaterialEditors",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
s_MaterialEditorShaderField = typeof(MaterialEditor).GetField(
|
||||
"m_Shader",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
s_MaterialEditorOnSelectedShaderPopupMethod = typeof(MaterialEditor).GetMethod(
|
||||
"OnSelectedShaderPopup",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AdvancedDropdown] Reflection init error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExtractAdvancedDropdownPopup(
|
||||
EditorWindow hostedWindow, float relX, float relY, float popW, float popH)
|
||||
{
|
||||
EnsureAdvancedDropdownWindowReflection();
|
||||
if (hostedWindow == null || s_AdvancedDropdownWindowDataSourceProp == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
object dataSource = s_AdvancedDropdownWindowDataSourceProp.GetValue(hostedWindow);
|
||||
if (dataSource == null)
|
||||
return false;
|
||||
|
||||
string dataSourceName = dataSource.GetType().Name;
|
||||
AdvancedDropdownHandlerKind kind;
|
||||
switch (dataSourceName)
|
||||
{
|
||||
case "ShaderDropdownDataSource":
|
||||
kind = AdvancedDropdownHandlerKind.MaterialShader;
|
||||
s_CachedAdvancedDropdownMaterialEditor = FindMaterialEditorForShaderDropdown(dataSource);
|
||||
if (s_CachedAdvancedDropdownMaterialEditor == null)
|
||||
{
|
||||
LogVerbose("[NWB-AdvancedDropdown] No MaterialEditor for shader dropdown");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "SimpleDataSource":
|
||||
case "MultiLevelDataSource":
|
||||
kind = AdvancedDropdownHandlerKind.StatelessIndex;
|
||||
s_CachedAdvancedDropdownMaterialEditor = null;
|
||||
break;
|
||||
case "MultiselectDataSource":
|
||||
kind = AdvancedDropdownHandlerKind.Multiselect;
|
||||
s_CachedAdvancedDropdownMaterialEditor = null;
|
||||
break;
|
||||
default:
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Unsupported dataSource: {dataSourceName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_CachedAdvancedDropdownDataSource = dataSource;
|
||||
s_AdvancedDropdownHandlerKind = kind;
|
||||
|
||||
if (string.IsNullOrEmpty(s_AdvancedDropdownSearchFilter) &&
|
||||
s_AdvancedDropdownWindowSearchProp != null)
|
||||
{
|
||||
s_AdvancedDropdownSearchFilter =
|
||||
s_AdvancedDropdownWindowSearchProp.GetValue(hostedWindow) as string ?? "";
|
||||
}
|
||||
|
||||
s_AdvancedDropdownPopupWidth = popW;
|
||||
s_AdvancedDropdownPopupHeight = popH;
|
||||
s_LastFrontendPopupX = relX;
|
||||
s_LastFrontendPopupY = relY;
|
||||
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Extracting {dataSourceName} ({kind})");
|
||||
return TrySendAdvancedDropdownPopup(relX, relY, popW, popH);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AdvancedDropdown] Extract error: {ex.Message}\n{ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static MaterialEditor FindMaterialEditorForShaderDropdown(object dataSource)
|
||||
{
|
||||
if (s_MaterialEditorsField == null)
|
||||
return null;
|
||||
|
||||
Shader currentShader = null;
|
||||
if (s_ShaderDropdownCurrentShaderField != null)
|
||||
currentShader = s_ShaderDropdownCurrentShaderField.GetValue(dataSource) as Shader;
|
||||
|
||||
var editors = s_MaterialEditorsField.GetValue(null) as IList;
|
||||
if (editors == null || editors.Count == 0)
|
||||
return null;
|
||||
|
||||
MaterialEditor inspectorMatch = null;
|
||||
MaterialEditor shaderMatch = null;
|
||||
|
||||
foreach (object editorObj in editors)
|
||||
{
|
||||
var materialEditor = editorObj as MaterialEditor;
|
||||
if (materialEditor == null)
|
||||
continue;
|
||||
|
||||
if (s_OffscreenTarget != null &&
|
||||
IsMaterialEditorOnTargetInspector(materialEditor))
|
||||
{
|
||||
inspectorMatch = materialEditor;
|
||||
}
|
||||
|
||||
if (s_MaterialEditorShaderField != null && currentShader != null)
|
||||
{
|
||||
var editorShader = s_MaterialEditorShaderField.GetValue(materialEditor) as Shader;
|
||||
if (editorShader == currentShader)
|
||||
shaderMatch = materialEditor;
|
||||
}
|
||||
}
|
||||
|
||||
return inspectorMatch ?? shaderMatch ?? (editors[0] as MaterialEditor);
|
||||
}
|
||||
|
||||
private static bool IsMaterialEditorOnTargetInspector(MaterialEditor materialEditor)
|
||||
{
|
||||
if (s_OffscreenTarget == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var trackerField = s_OffscreenTarget.GetType().GetField(
|
||||
"m_Tracker",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
object tracker = trackerField?.GetValue(s_OffscreenTarget);
|
||||
if (tracker == null)
|
||||
return false;
|
||||
|
||||
PropertyInfo activeEditorsProp = tracker.GetType().GetProperty(
|
||||
"activeEditors",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var activeEditors = activeEditorsProp?.GetValue(tracker) as UnityEditor.Editor[];
|
||||
if (activeEditors == null)
|
||||
return false;
|
||||
|
||||
foreach (UnityEditor.Editor editor in activeEditors)
|
||||
{
|
||||
if (editor == materialEditor)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort inspector association.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TrySendAdvancedDropdownPopup(float relX, float relY, float popW, float popH)
|
||||
{
|
||||
EnsureAdvancedDropdownWindowReflection();
|
||||
if (s_CachedAdvancedDropdownDataSource == null || s_AdvancedDropdownMainTreeProp == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-AdvancedDropdown] Missing cached dropdown state");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object mainTree = s_AdvancedDropdownMainTreeProp.GetValue(s_CachedAdvancedDropdownDataSource);
|
||||
object treeToRender = mainTree;
|
||||
|
||||
if (!string.IsNullOrEmpty(s_AdvancedDropdownSearchFilter) &&
|
||||
s_AdvancedDropdownRebuildSearchMethod != null &&
|
||||
mainTree != null)
|
||||
{
|
||||
s_AdvancedDropdownRebuildSearchMethod.Invoke(
|
||||
s_CachedAdvancedDropdownDataSource,
|
||||
new object[] { s_AdvancedDropdownSearchFilter, mainTree });
|
||||
object searchTree = s_AdvancedDropdownSearchTreeProp?.GetValue(s_CachedAdvancedDropdownDataSource);
|
||||
if (searchTree != null)
|
||||
treeToRender = searchTree;
|
||||
}
|
||||
|
||||
if (treeToRender == null)
|
||||
{
|
||||
LogVerbose("[NWB-AdvancedDropdown] Dropdown tree is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
var pathList = new List<string>();
|
||||
int itemIndex = 0;
|
||||
int validCount = 0;
|
||||
var selectedIds = GetAdvancedDropdownSelectedIds(s_CachedAdvancedDropdownDataSource);
|
||||
|
||||
s_FrontendPopupIdCounter++;
|
||||
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
|
||||
s_PopupSourceType = "advanced_dropdown";
|
||||
|
||||
var sb = new StringBuilder(65536);
|
||||
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
|
||||
sb.Append(s_CurrentFrontendPopupId);
|
||||
sb.Append("\",\"sourceType\":\"advanced_dropdown\",\"x\":");
|
||||
sb.Append(Mathf.RoundToInt(relX));
|
||||
sb.Append(",\"y\":");
|
||||
sb.Append(Mathf.RoundToInt(relY));
|
||||
sb.Append(",\"width\":");
|
||||
sb.Append(Mathf.RoundToInt(Mathf.Max(popW, 200f)));
|
||||
sb.Append(",\"height\":");
|
||||
sb.Append(Mathf.RoundToInt(Mathf.Max(popH, 280f)));
|
||||
sb.Append(",\"searchText\":\"");
|
||||
sb.Append(EscapeJsonString(s_AdvancedDropdownSearchFilter ?? ""));
|
||||
sb.Append("\",\"items\":[");
|
||||
bool needComma = false;
|
||||
AppendAdvancedDropdownItems(
|
||||
sb,
|
||||
GetAdvancedDropdownChildren(treeToRender),
|
||||
ref itemIndex,
|
||||
ref validCount,
|
||||
ref needComma,
|
||||
pathList,
|
||||
selectedIds);
|
||||
sb.Append("]}");
|
||||
|
||||
if (validCount <= 0)
|
||||
{
|
||||
LogVerbose("[NWB-AdvancedDropdown] No selectable dropdown items extracted");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_CachedPopupItemPaths = pathList.ToArray();
|
||||
string payload = sb.ToString();
|
||||
int payloadBytes = Encoding.UTF8.GetByteCount(payload);
|
||||
|
||||
bool sent;
|
||||
if (payloadBytes > kMaxDataChannelPayloadBytes)
|
||||
{
|
||||
sent = SendPopupPayloadChunked(s_CurrentFrontendPopupId, payload, payloadBytes);
|
||||
if (sent)
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Sent chunked popup: bytes={payloadBytes} items={validCount}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sent = SendDataChannelMessage(payload);
|
||||
}
|
||||
|
||||
s_FrontendPopupSent = sent;
|
||||
if (sent)
|
||||
{
|
||||
CancelNativeDelayedPopup();
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Sent popup: id={s_CurrentFrontendPopupId} kind={s_AdvancedDropdownHandlerKind} items={validCount}");
|
||||
}
|
||||
else
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AdvancedDropdown] Failed to send popup: id={s_CurrentFrontendPopupId}");
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AdvancedDropdown] Send error: {ex.Message}\n{ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<int> GetAdvancedDropdownSelectedIds(object dataSource)
|
||||
{
|
||||
var selected = new HashSet<int>();
|
||||
if (dataSource == null || s_AdvancedDropdownDataSourceSelectedIdsProp == null)
|
||||
return selected;
|
||||
|
||||
var selectedIds = s_AdvancedDropdownDataSourceSelectedIdsProp.GetValue(dataSource) as IList;
|
||||
if (selectedIds == null)
|
||||
return selected;
|
||||
|
||||
foreach (object idObj in selectedIds)
|
||||
{
|
||||
if (idObj is int id)
|
||||
selected.Add(id);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static int GetAdvancedDropdownItemId(object item)
|
||||
{
|
||||
if (item == null || s_AdvancedDropdownItemIdProp == null)
|
||||
return 0;
|
||||
return (int)s_AdvancedDropdownItemIdProp.GetValue(item);
|
||||
}
|
||||
|
||||
private static int GetAdvancedDropdownElementIndex(object item)
|
||||
{
|
||||
if (item == null || s_AdvancedDropdownItemElementIndexProp == null)
|
||||
return -1;
|
||||
return (int)s_AdvancedDropdownItemElementIndexProp.GetValue(item);
|
||||
}
|
||||
|
||||
private static void AppendAdvancedDropdownItems(
|
||||
StringBuilder sb,
|
||||
IEnumerable items,
|
||||
ref int itemIndex,
|
||||
ref int validCount,
|
||||
ref bool needComma,
|
||||
List<string> pathList,
|
||||
HashSet<int> selectedIds)
|
||||
{
|
||||
if (items == null) return;
|
||||
|
||||
foreach (object item in items)
|
||||
{
|
||||
if (item == null) continue;
|
||||
if (item.GetType().Name == "SeparatorDropdownItem")
|
||||
continue;
|
||||
|
||||
string label = GetAdvancedDropdownDisplayName(item);
|
||||
if (string.IsNullOrEmpty(label))
|
||||
continue;
|
||||
|
||||
bool hasChildren = s_AdvancedDropdownItemHasChildrenProp != null &&
|
||||
(bool)s_AdvancedDropdownItemHasChildrenProp.GetValue(item);
|
||||
|
||||
if (hasChildren)
|
||||
{
|
||||
if (needComma) sb.Append(',');
|
||||
needComma = true;
|
||||
sb.Append("{\"label\":\"");
|
||||
sb.Append(EscapeJsonString(label));
|
||||
sb.Append("\",\"enabled\":true,\"checked\":false,\"separator\":false,\"submenu\":true,\"children\":[");
|
||||
bool childComma = false;
|
||||
int childValid = 0;
|
||||
AppendAdvancedDropdownItems(
|
||||
sb,
|
||||
GetAdvancedDropdownChildren(item),
|
||||
ref itemIndex,
|
||||
ref childValid,
|
||||
ref childComma,
|
||||
pathList,
|
||||
selectedIds);
|
||||
sb.Append("]}");
|
||||
if (childValid > 0)
|
||||
validCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string path = null;
|
||||
if (item.GetType().Name == "ShaderDropdownItem" &&
|
||||
s_ShaderDropdownItemFullNameProp != null)
|
||||
{
|
||||
path = s_ShaderDropdownItemFullNameProp.GetValue(item) as string;
|
||||
}
|
||||
else
|
||||
{
|
||||
int elementIndex = GetAdvancedDropdownElementIndex(item);
|
||||
if (elementIndex >= 0)
|
||||
path = elementIndex.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
|
||||
bool isChecked = selectedIds != null && selectedIds.Contains(GetAdvancedDropdownItemId(item));
|
||||
|
||||
if (needComma) sb.Append(',');
|
||||
needComma = true;
|
||||
sb.Append("{\"index\":");
|
||||
sb.Append(itemIndex);
|
||||
sb.Append(",\"path\":\"");
|
||||
sb.Append(EscapeJsonString(path));
|
||||
sb.Append("\",\"label\":\"");
|
||||
sb.Append(EscapeJsonString(label));
|
||||
sb.Append("\",\"enabled\":true,\"checked\":");
|
||||
sb.Append(isChecked ? "true" : "false");
|
||||
sb.Append(",\"separator\":false}");
|
||||
pathList.Add(path);
|
||||
itemIndex++;
|
||||
validCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the frontend popup should stay open (multiselect).
|
||||
/// </summary>
|
||||
private static bool TryHandleAdvancedDropdownPopupSelect(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || s_CachedAdvancedDropdownDataSource == null)
|
||||
return false;
|
||||
|
||||
EnsureAdvancedDropdownWindowReflection();
|
||||
|
||||
try
|
||||
{
|
||||
switch (s_AdvancedDropdownHandlerKind)
|
||||
{
|
||||
case AdvancedDropdownHandlerKind.MaterialShader:
|
||||
TryHandleMaterialShaderDropdownSelect(path);
|
||||
return false;
|
||||
case AdvancedDropdownHandlerKind.Multiselect:
|
||||
TryHandleMultiselectDropdownSelect(path);
|
||||
return true;
|
||||
default:
|
||||
TryHandleStatelessIndexDropdownSelect(path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AdvancedDropdown] Select error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryHandleMaterialShaderDropdownSelect(string path)
|
||||
{
|
||||
if (s_CachedAdvancedDropdownMaterialEditor == null ||
|
||||
s_MaterialEditorOnSelectedShaderPopupMethod == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-AdvancedDropdown] Material shader select handler unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_MaterialEditorOnSelectedShaderPopupMethod.Invoke(
|
||||
s_CachedAdvancedDropdownMaterialEditor,
|
||||
new object[] { path });
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Applied shader: '{path}'");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryHandleStatelessIndexDropdownSelect(string path)
|
||||
{
|
||||
if (!int.TryParse(path, out int selectedIndex))
|
||||
return false;
|
||||
|
||||
if (s_StatelessAdvancedDropdownResultField == null ||
|
||||
s_StatelessAdvancedDropdownWindowClosedField == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-AdvancedDropdown] StatelessAdvancedDropdown fields not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_StatelessAdvancedDropdownResultField.SetValue(null, selectedIndex);
|
||||
s_StatelessAdvancedDropdownWindowClosedField.SetValue(null, true);
|
||||
RepaintStatelessAdvancedDropdownParent();
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Applied index selection: {selectedIndex}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryHandleMultiselectDropdownSelect(string path)
|
||||
{
|
||||
if (!int.TryParse(path, out int selectedIndex) ||
|
||||
s_MultiselectUpdateSelectedIdMethod == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
object item = FindAdvancedDropdownLeafByElementIndex(
|
||||
s_AdvancedDropdownMainTreeProp.GetValue(s_CachedAdvancedDropdownDataSource),
|
||||
selectedIndex);
|
||||
if (item == null)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-AdvancedDropdown] Multiselect item not found: index={selectedIndex}");
|
||||
return true;
|
||||
}
|
||||
|
||||
s_MultiselectUpdateSelectedIdMethod.Invoke(s_CachedAdvancedDropdownDataSource, new[] { item });
|
||||
|
||||
if (s_StatelessAdvancedDropdownShouldReturnValueField != null)
|
||||
s_StatelessAdvancedDropdownShouldReturnValueField.SetValue(null, true);
|
||||
|
||||
RepaintStatelessAdvancedDropdownParent();
|
||||
|
||||
s_FrontendPopupSent = false;
|
||||
TrySendAdvancedDropdownPopup(
|
||||
s_LastFrontendPopupX,
|
||||
s_LastFrontendPopupY,
|
||||
s_AdvancedDropdownPopupWidth,
|
||||
s_AdvancedDropdownPopupHeight);
|
||||
LogVerbose($"[NWB-AdvancedDropdown] Applied multiselect index: {selectedIndex}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static object FindAdvancedDropdownLeafByElementIndex(object node, int elementIndex)
|
||||
{
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
foreach (object child in GetAdvancedDropdownChildren(node))
|
||||
{
|
||||
if (child == null || child.GetType().Name == "SeparatorDropdownItem")
|
||||
continue;
|
||||
|
||||
bool hasChildren = s_AdvancedDropdownItemHasChildrenProp != null &&
|
||||
(bool)s_AdvancedDropdownItemHasChildrenProp.GetValue(child);
|
||||
if (hasChildren)
|
||||
{
|
||||
object found = FindAdvancedDropdownLeafByElementIndex(child, elementIndex);
|
||||
if (found != null)
|
||||
return found;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetAdvancedDropdownElementIndex(child) == elementIndex)
|
||||
return child;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void RepaintStatelessAdvancedDropdownParent()
|
||||
{
|
||||
EditorWindow parent = null;
|
||||
if (s_StatelessAdvancedDropdownParentWindowField != null)
|
||||
parent = s_StatelessAdvancedDropdownParentWindowField.GetValue(null) as EditorWindow;
|
||||
|
||||
parent?.Repaint();
|
||||
s_OffscreenTarget?.Repaint();
|
||||
}
|
||||
|
||||
private static void ClearAdvancedDropdownPopupState()
|
||||
{
|
||||
s_CachedAdvancedDropdownDataSource = null;
|
||||
s_CachedAdvancedDropdownMaterialEditor = null;
|
||||
s_AdvancedDropdownSearchFilter = "";
|
||||
s_AdvancedDropdownPopupWidth = 0f;
|
||||
s_AdvancedDropdownPopupHeight = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ccca71b3087a944087f66ed1d09e5a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+2460
File diff suppressed because it is too large.
Load diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc8e2741237d471ca15a88be94f12527
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private static FieldInfo s_SceneViewLastCursorField;
|
||||
private static string s_LastSentEditorCursorStyle;
|
||||
private static string s_ActiveDragCursorStyle;
|
||||
private static Vector2 s_LastCursorProbePos;
|
||||
private static bool s_HasLastCursorProbePos;
|
||||
private const float kSplitGrabDist = 5f;
|
||||
|
||||
private static void EnsureEditorCursorReflection()
|
||||
{
|
||||
if (s_SceneViewLastCursorField != null)
|
||||
return;
|
||||
|
||||
s_SceneViewLastCursorField = typeof(SceneView).GetField(
|
||||
"s_LastCursor",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
private static void RememberCursorProbePosition(Vector2 mousePos)
|
||||
{
|
||||
if (mousePos.sqrMagnitude > 0.01f)
|
||||
s_LastCursorProbePos = mousePos;
|
||||
else if (s_HasLastMousePos)
|
||||
s_LastCursorProbePos = s_LastMousePos;
|
||||
else if (s_OffscreenTarget != null)
|
||||
{
|
||||
Rect pos = s_OffscreenTarget.position;
|
||||
s_LastCursorProbePos = new Vector2(pos.width * 0.5f, pos.height * 0.5f);
|
||||
}
|
||||
s_HasLastCursorProbePos = true;
|
||||
}
|
||||
|
||||
private static Vector2 GetCursorProbePosition()
|
||||
{
|
||||
return s_HasLastCursorProbePos ? s_LastCursorProbePos : Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SendGuiEvent(object view, Event evt)
|
||||
{
|
||||
if (view == null || evt == null)
|
||||
return;
|
||||
|
||||
MethodInfo sendEvent = view.GetType().GetMethod(
|
||||
"SendEvent",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
null,
|
||||
new[] { typeof(Event) },
|
||||
null);
|
||||
sendEvent?.Invoke(view, new object[] { evt });
|
||||
}
|
||||
|
||||
private static void RunCursorGuiCycle(Vector2 mousePos)
|
||||
{
|
||||
if (s_OffscreenTarget == null)
|
||||
return;
|
||||
|
||||
object hostView = GetParentView(s_OffscreenTarget);
|
||||
if (hostView != null)
|
||||
{
|
||||
SendGuiEvent(hostView, new Event { type = EventType.Layout, mousePosition = mousePos });
|
||||
SendGuiEvent(hostView, new Event { type = EventType.Repaint, mousePosition = mousePos });
|
||||
SendGuiEvent(hostView, new Event { type = EventType.MouseMove, mousePosition = mousePos, delta = Vector2.zero });
|
||||
}
|
||||
|
||||
if (s_OffscreenTarget is SceneView)
|
||||
{
|
||||
s_OffscreenTarget.SendEvent(new Event { type = EventType.Layout, mousePosition = mousePos });
|
||||
s_OffscreenTarget.SendEvent(new Event { type = EventType.Repaint, mousePosition = mousePos });
|
||||
s_OffscreenTarget.SendEvent(new Event { type = EventType.MouseMove, mousePosition = mousePos, delta = Vector2.zero });
|
||||
}
|
||||
}
|
||||
|
||||
private static MouseCursor FindSplitCursorForHotControl(object view, int hotControl)
|
||||
{
|
||||
if (view == null || hotControl == 0)
|
||||
return MouseCursor.Arrow;
|
||||
|
||||
Type viewType = view.GetType();
|
||||
if (viewType.Name == "SplitView")
|
||||
{
|
||||
FieldInfo controlIdField = viewType.GetField("controlID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo verticalField = viewType.GetField("vertical", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
PropertyInfo childrenProp = viewType.GetProperty("children", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (controlIdField != null && verticalField != null &&
|
||||
(int)controlIdField.GetValue(view) == hotControl)
|
||||
{
|
||||
return (bool)verticalField.GetValue(view)
|
||||
? MouseCursor.SplitResizeUpDown
|
||||
: MouseCursor.SplitResizeLeftRight;
|
||||
}
|
||||
|
||||
if (childrenProp?.GetValue(view) is Array children)
|
||||
{
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
{
|
||||
MouseCursor nested = FindSplitCursorForHotControl(children.GetValue(i), hotControl);
|
||||
if (nested != MouseCursor.Arrow)
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return MouseCursor.Arrow;
|
||||
}
|
||||
|
||||
PropertyInfo childProp = viewType.GetProperty("children", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (childProp?.GetValue(view) is Array childViews)
|
||||
{
|
||||
for (int i = 0; i < childViews.Length; i++)
|
||||
{
|
||||
MouseCursor nested = FindSplitCursorForHotControl(childViews.GetValue(i), hotControl);
|
||||
if (nested != MouseCursor.Arrow)
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return MouseCursor.Arrow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split resize cursors live on DockArea/SplitView, not SceneView.s_LastCursor.
|
||||
/// </summary>
|
||||
private static MouseCursor TryResolveSplitCursor(object hostView, Vector2 mousePosInView)
|
||||
{
|
||||
if (hostView == null)
|
||||
return MouseCursor.Arrow;
|
||||
|
||||
int hotControl = GUIUtility.hotControl;
|
||||
if (hotControl != 0)
|
||||
{
|
||||
object hostWindow = s_CompositeActive
|
||||
? GetCompositeHostWindow()
|
||||
: GetViewWindow(hostView);
|
||||
MouseCursor dragCursor = FindSplitCursorForHotControl(GetRootSplitView(hostWindow), hotControl);
|
||||
if (dragCursor == MouseCursor.Arrow)
|
||||
dragCursor = FindSplitCursorForHotControl(hostView, hotControl);
|
||||
if (dragCursor != MouseCursor.Arrow)
|
||||
return dragCursor;
|
||||
}
|
||||
|
||||
PropertyInfo parentProp = hostView.GetType().GetProperty("parent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
PropertyInfo positionProp = hostView.GetType().GetProperty("position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
object view = hostView;
|
||||
for (int guard = 0; guard < 8 && view != null; guard++)
|
||||
{
|
||||
object splitParent = parentProp?.GetValue(view);
|
||||
if (splitParent == null || splitParent.GetType().Name != "SplitView")
|
||||
break;
|
||||
|
||||
Type splitType = splitParent.GetType();
|
||||
MethodInfo indexOfChild = splitType.GetMethod("IndexOfChild", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo verticalField = splitType.GetField("vertical", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo controlIdField = splitType.GetField("controlID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
PropertyInfo childrenProp = splitType.GetProperty("children", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (indexOfChild == null || verticalField == null || childrenProp == null)
|
||||
break;
|
||||
|
||||
int splitControlId = controlIdField != null ? (int)controlIdField.GetValue(splitParent) : 0;
|
||||
if (hotControl != 0 && hotControl != splitControlId)
|
||||
{
|
||||
view = splitParent;
|
||||
continue;
|
||||
}
|
||||
|
||||
int idx = (int)indexOfChild.Invoke(splitParent, new[] { view });
|
||||
bool vertical = (bool)verticalField.GetValue(splitParent);
|
||||
int childCount = (childrenProp.GetValue(splitParent) as Array)?.Length ?? 0;
|
||||
Rect position = positionProp != null ? (Rect)positionProp.GetValue(view) : Rect.zero;
|
||||
|
||||
if (vertical)
|
||||
{
|
||||
if (idx != 0 && mousePosInView.y <= kSplitGrabDist)
|
||||
return MouseCursor.SplitResizeUpDown;
|
||||
if (idx != childCount - 1 &&
|
||||
mousePosInView.y >= position.height - kSplitGrabDist + 1f &&
|
||||
mousePosInView.y <= position.height)
|
||||
return MouseCursor.SplitResizeUpDown;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (idx != 0 && mousePosInView.x <= kSplitGrabDist)
|
||||
return MouseCursor.SplitResizeLeftRight;
|
||||
if (idx != childCount - 1 &&
|
||||
mousePosInView.x >= position.width - kSplitGrabDist + 1f &&
|
||||
mousePosInView.x <= position.width)
|
||||
return MouseCursor.SplitResizeLeftRight;
|
||||
}
|
||||
|
||||
view = splitParent;
|
||||
}
|
||||
|
||||
return MouseCursor.Arrow;
|
||||
}
|
||||
|
||||
private static MouseCursor ViewToolToMouseCursor(ViewTool tool)
|
||||
{
|
||||
switch (tool)
|
||||
{
|
||||
case ViewTool.Pan: return MouseCursor.Pan;
|
||||
case ViewTool.Orbit: return MouseCursor.Orbit;
|
||||
case ViewTool.FPS: return MouseCursor.FPS;
|
||||
case ViewTool.Zoom: return MouseCursor.Zoom;
|
||||
default: return MouseCursor.Arrow;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsEditorViewToolCursorContext()
|
||||
{
|
||||
if (s_OffscreenScenePanLocked)
|
||||
return true;
|
||||
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
return global::UnityEditor.Tools.viewToolActive;
|
||||
#else
|
||||
return global::UnityEditor.Tools.current == Tool.View;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static MouseCursor TryResolveSceneViewCursor()
|
||||
{
|
||||
// During drag, SceneView clutch tools (right=FPS, alt+left=Orbit, etc.)
|
||||
// update Tools.viewTool but s_LastCursor is stale because we skip the
|
||||
// Layout/Repaint probe cycle to preserve hotControl.
|
||||
if (s_RemoteMouseButtonDown && IsEditorViewToolCursorContext())
|
||||
return ViewToolToMouseCursor(global::UnityEditor.Tools.viewTool);
|
||||
|
||||
EnsureEditorCursorReflection();
|
||||
if (s_SceneViewLastCursorField == null)
|
||||
return MouseCursor.Arrow;
|
||||
|
||||
try
|
||||
{
|
||||
return (MouseCursor)s_SceneViewLastCursorField.GetValue(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogVerbose($"[NWB-Cursor] Read SceneView.s_LastCursor failed: {ex.Message}");
|
||||
return MouseCursor.Arrow;
|
||||
}
|
||||
}
|
||||
|
||||
private static string MouseCursorToStyleId(MouseCursor cursor)
|
||||
{
|
||||
switch (cursor)
|
||||
{
|
||||
case MouseCursor.Text: return "text";
|
||||
case MouseCursor.ResizeVertical:
|
||||
case MouseCursor.SplitResizeUpDown: return "ns-resize";
|
||||
case MouseCursor.ResizeHorizontal:
|
||||
case MouseCursor.SplitResizeLeftRight: return "ew-resize";
|
||||
case MouseCursor.Link: return "pointer";
|
||||
case MouseCursor.SlideArrow: return "col-resize";
|
||||
case MouseCursor.ResizeUpRight: return "nesw-resize";
|
||||
case MouseCursor.ResizeUpLeft: return "nwse-resize";
|
||||
case MouseCursor.MoveArrow: return "move";
|
||||
case MouseCursor.RotateArrow: return "rotate";
|
||||
case MouseCursor.ScaleArrow: return "scale";
|
||||
case MouseCursor.ArrowPlus: return "copy";
|
||||
case MouseCursor.ArrowMinus: return "not-allowed";
|
||||
case MouseCursor.Pan: return "pan";
|
||||
case MouseCursor.Orbit: return "orbit";
|
||||
case MouseCursor.Zoom: return "zoom";
|
||||
case MouseCursor.FPS: return "fps";
|
||||
case MouseCursor.CustomCursor: return "custom";
|
||||
default: return "default";
|
||||
}
|
||||
}
|
||||
|
||||
private static MouseCursor TryResolveEditorMouseCursor()
|
||||
{
|
||||
if (s_OffscreenTarget != null)
|
||||
{
|
||||
object hostView = GetParentView(s_OffscreenTarget);
|
||||
if (hostView != null)
|
||||
{
|
||||
MouseCursor splitCursor = TryResolveSplitCursor(hostView, GetCursorProbePosition());
|
||||
if (splitCursor != MouseCursor.Arrow)
|
||||
return splitCursor;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_OffscreenTarget is SceneView)
|
||||
{
|
||||
MouseCursor sceneCursor = TryResolveSceneViewCursor();
|
||||
if (sceneCursor != MouseCursor.Arrow)
|
||||
return sceneCursor;
|
||||
}
|
||||
|
||||
return MouseCursor.Arrow;
|
||||
}
|
||||
|
||||
private static void TrySendDefaultEditorCursor()
|
||||
{
|
||||
if (!s_OffscreenActive)
|
||||
return;
|
||||
|
||||
string styleId = "default";
|
||||
if (styleId == "default" && s_RemoteMouseButtonDown && !string.IsNullOrEmpty(s_ActiveDragCursorStyle))
|
||||
styleId = s_ActiveDragCursorStyle;
|
||||
|
||||
if (styleId == s_LastSentEditorCursorStyle)
|
||||
return;
|
||||
|
||||
s_LastSentEditorCursorStyle = styleId;
|
||||
SendDataChannelMessage("{\"type\":\"cursor_style\",\"cursor\":\"" + styleId + "\"}");
|
||||
LogVerbose($"[NWB-Cursor] -> {styleId} (outside hit)");
|
||||
}
|
||||
|
||||
private static void TrySendEditorCursorStyle()
|
||||
{
|
||||
if (!s_OffscreenActive || s_OffscreenTarget == null)
|
||||
return;
|
||||
|
||||
if (s_OffscreenTargetType != null &&
|
||||
s_OffscreenTargetType.Name == "GameView" &&
|
||||
global::UnityEngine.Cursor.lockState == CursorLockMode.Locked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string styleId = MouseCursorToStyleId(TryResolveEditorMouseCursor());
|
||||
if (styleId == "default" && s_RemoteMouseButtonDown && !string.IsNullOrEmpty(s_ActiveDragCursorStyle))
|
||||
styleId = s_ActiveDragCursorStyle;
|
||||
else if (styleId != "default")
|
||||
s_ActiveDragCursorStyle = styleId;
|
||||
|
||||
if (styleId == s_LastSentEditorCursorStyle)
|
||||
return;
|
||||
|
||||
s_LastSentEditorCursorStyle = styleId;
|
||||
SendDataChannelMessage("{\"type\":\"cursor_style\",\"cursor\":\"" + styleId + "\"}");
|
||||
LogVerbose($"[NWB-Cursor] -> {styleId}");
|
||||
}
|
||||
|
||||
private static void LockEditorDragCursorFromHover()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(s_LastSentEditorCursorStyle) &&
|
||||
s_LastSentEditorCursorStyle != "default")
|
||||
{
|
||||
s_ActiveDragCursorStyle = s_LastSentEditorCursorStyle;
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateEditorCursorFromInput(string type, Vector2 mousePos)
|
||||
{
|
||||
if (s_OffscreenTarget == null || s_OffscreenTargetType == null ||
|
||||
s_OffscreenTargetType.Name == "GameView")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "mouseup")
|
||||
s_ActiveDragCursorStyle = null;
|
||||
|
||||
RememberCursorProbePosition(mousePos);
|
||||
mousePos = GetCursorProbePosition();
|
||||
if (!s_RemoteMouseButtonDown)
|
||||
RunCursorGuiCycle(mousePos);
|
||||
TrySendEditorCursorStyle();
|
||||
}
|
||||
|
||||
private static void ClearEditorCursorState(bool notifyFrontend = true)
|
||||
{
|
||||
s_LastSentEditorCursorStyle = null;
|
||||
s_ActiveDragCursorStyle = null;
|
||||
s_HasLastCursorProbePos = false;
|
||||
if (!notifyFrontend || !s_OffscreenActive)
|
||||
return;
|
||||
|
||||
SendDataChannelMessage("{\"type\":\"cursor_style\",\"cursor\":\"default\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0962d207323241b68d2a3b53baab997f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+724
@@ -0,0 +1,724 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private static int s_DragAndDropTestingLogCount;
|
||||
private static int s_ProjectSelectionSnapshotLogCount;
|
||||
private static int s_BlockedNativeProjectDragLogCount;
|
||||
private static int s_NativeDragCancelWatchdogVersion;
|
||||
private static int s_SyntheticDragThresholdLogCount;
|
||||
private static bool s_RemoteNativeDragAndDropActive;
|
||||
private static EditorWindow s_RemoteNativeDragSourceWindow;
|
||||
private static UnityEngine.Object[] s_ProjectAssetDragObjects;
|
||||
private static string[] s_ProjectAssetDragPaths;
|
||||
private static bool s_SyntheticDragGestureTracking;
|
||||
private static Vector2 s_SyntheticDragStartPosition;
|
||||
private static string s_SyntheticDragWindowTypeName;
|
||||
|
||||
// Ignore tiny pointer jitter so click interactions (e.g. hierarchy foldout)
|
||||
// are not misclassified as drag gestures.
|
||||
private const float kSyntheticDragStartThreshold = 3f;
|
||||
private const float kSyntheticDragStartThresholdSqr =
|
||||
kSyntheticDragStartThreshold * kSyntheticDragStartThreshold;
|
||||
|
||||
private static bool ShouldStartSyntheticProjectAssetDragForInput(string inputType, string windowTypeName)
|
||||
{
|
||||
return inputType == "mousedrag" &&
|
||||
(windowTypeName == "ProjectBrowser" || windowTypeName == "UnityEditor.ProjectBrowser");
|
||||
}
|
||||
|
||||
private static bool ShouldStartSyntheticHierarchyGameObjectDragForInput(string inputType, string windowTypeName)
|
||||
{
|
||||
return inputType == "mousedrag" &&
|
||||
(windowTypeName == "SceneHierarchyWindow" || windowTypeName == "UnityEditor.SceneHierarchyWindow");
|
||||
}
|
||||
|
||||
private static bool ShouldCaptureProjectAssetSelectionForInput(string inputType, string windowTypeName)
|
||||
{
|
||||
return inputType == "mousedown" &&
|
||||
(windowTypeName == "ProjectBrowser" || windowTypeName == "UnityEditor.ProjectBrowser");
|
||||
}
|
||||
|
||||
private static bool ShouldBlockNativeEditorDragStart(string inputType, string windowTypeName)
|
||||
{
|
||||
return ShouldStartSyntheticProjectAssetDragForInput(inputType, windowTypeName) ||
|
||||
ShouldStartSyntheticHierarchyGameObjectDragForInput(inputType, windowTypeName);
|
||||
}
|
||||
|
||||
private static bool ShouldForwardNativeDragAndDropEvent(string inputType)
|
||||
{
|
||||
return inputType == "mousedrag" || inputType == "mouseup";
|
||||
}
|
||||
|
||||
private static bool ShouldUseCompositeMouseCaptureForInput(string inputType)
|
||||
{
|
||||
return !s_RemoteNativeDragAndDropActive || !ShouldForwardNativeDragAndDropEvent(inputType);
|
||||
}
|
||||
|
||||
private static bool ShouldTrackSyntheticDragWindowType(string windowTypeName)
|
||||
{
|
||||
return windowTypeName == "ProjectBrowser" ||
|
||||
windowTypeName == "UnityEditor.ProjectBrowser" ||
|
||||
windowTypeName == "SceneHierarchyWindow" ||
|
||||
windowTypeName == "UnityEditor.SceneHierarchyWindow";
|
||||
}
|
||||
|
||||
private static void UpdateSyntheticDragGestureState(
|
||||
EditorWindow window,
|
||||
string inputType,
|
||||
Vector2 mousePosition)
|
||||
{
|
||||
string windowTypeName = window?.GetType().FullName ?? window?.GetType().Name;
|
||||
bool trackableWindow = !string.IsNullOrEmpty(windowTypeName) &&
|
||||
ShouldTrackSyntheticDragWindowType(windowTypeName);
|
||||
|
||||
if (inputType == "mousedown")
|
||||
{
|
||||
if (trackableWindow)
|
||||
{
|
||||
s_SyntheticDragGestureTracking = true;
|
||||
s_SyntheticDragStartPosition = mousePosition;
|
||||
s_SyntheticDragWindowTypeName = windowTypeName;
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSyntheticDragGestureState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputType == "mouseup")
|
||||
{
|
||||
ClearSyntheticDragGestureState();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasSyntheticDragThresholdMet(
|
||||
EditorWindow window,
|
||||
string inputType,
|
||||
Vector2 mousePosition)
|
||||
{
|
||||
if (window == null || inputType != "mousedrag")
|
||||
return false;
|
||||
|
||||
string windowTypeName = window.GetType().FullName ?? window.GetType().Name;
|
||||
if (!s_SyntheticDragGestureTracking ||
|
||||
string.IsNullOrEmpty(s_SyntheticDragWindowTypeName) ||
|
||||
!string.Equals(s_SyntheticDragWindowTypeName, windowTypeName, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float sqrDistance = (mousePosition - s_SyntheticDragStartPosition).sqrMagnitude;
|
||||
bool reachedThreshold = sqrDistance >= kSyntheticDragStartThresholdSqr;
|
||||
if (!reachedThreshold)
|
||||
{
|
||||
s_SyntheticDragThresholdLogCount++;
|
||||
if (s_SyntheticDragThresholdLogCount <= 10 || s_SyntheticDragThresholdLogCount % 100 == 0)
|
||||
{
|
||||
LogVerbose(
|
||||
$"[NWB-DnD] Ignore tiny drag before threshold: " +
|
||||
$"win={windowTypeName} dist={Mathf.Sqrt(sqrDistance):F2}px " +
|
||||
$"threshold={kSyntheticDragStartThreshold:F1}px");
|
||||
}
|
||||
}
|
||||
return reachedThreshold;
|
||||
}
|
||||
|
||||
private static void ClearSyntheticDragGestureState()
|
||||
{
|
||||
s_SyntheticDragGestureTracking = false;
|
||||
s_SyntheticDragStartPosition = Vector2.zero;
|
||||
s_SyntheticDragWindowTypeName = null;
|
||||
}
|
||||
|
||||
private static bool TryStartSyntheticProjectAssetDrag(
|
||||
EditorWindow window,
|
||||
string inputType,
|
||||
Vector2 mousePosition)
|
||||
{
|
||||
if (window == null) return false;
|
||||
if (s_RemoteNativeDragAndDropActive && !HasActiveUnityDragAndDropData())
|
||||
FinishRemoteNativeDragAndDrop();
|
||||
if (s_RemoteNativeDragAndDropActive) return false;
|
||||
if (!ShouldStartSyntheticProjectAssetDragForInput(inputType, window.GetType().FullName ?? window.GetType().Name))
|
||||
return false;
|
||||
if (!HasSyntheticDragThresholdMet(window, inputType, mousePosition))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var refs = new System.Collections.Generic.List<UnityEngine.Object>();
|
||||
var paths = new System.Collections.Generic.List<string>();
|
||||
AddCachedProjectAssetDragObjects(refs, paths);
|
||||
if (refs.Count == 0)
|
||||
{
|
||||
foreach (UnityEngine.Object obj in Selection.objects)
|
||||
{
|
||||
if (obj == null) continue;
|
||||
string path = AssetDatabase.GetAssetPath(obj);
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
refs.Add(obj);
|
||||
paths.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (refs.Count == 0)
|
||||
return false;
|
||||
|
||||
BeginRemoteNativeDragAndDrop(
|
||||
window,
|
||||
refs.ToArray(),
|
||||
paths.ToArray(),
|
||||
DragAndDropVisualMode.Generic,
|
||||
$"Started synthetic Project asset drag: count={refs.Count}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-DnD] Failed to start synthetic Project asset drag: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryStartSyntheticHierarchyGameObjectDrag(
|
||||
EditorWindow window,
|
||||
string inputType,
|
||||
Vector2 mousePosition)
|
||||
{
|
||||
if (window == null) return false;
|
||||
if (s_RemoteNativeDragAndDropActive && !HasActiveUnityDragAndDropData())
|
||||
FinishRemoteNativeDragAndDrop();
|
||||
if (s_RemoteNativeDragAndDropActive) return false;
|
||||
if (!ShouldStartSyntheticHierarchyGameObjectDragForInput(inputType, window.GetType().FullName ?? window.GetType().Name))
|
||||
return false;
|
||||
if (!HasSyntheticDragThresholdMet(window, inputType, mousePosition))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var refs = new System.Collections.Generic.List<UnityEngine.Object>();
|
||||
AddHierarchyDragGameObjects(window, refs);
|
||||
if (refs.Count == 0)
|
||||
{
|
||||
foreach (UnityEngine.Object obj in Selection.objects)
|
||||
{
|
||||
if (obj is GameObject)
|
||||
refs.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
if (refs.Count == 0)
|
||||
return false;
|
||||
|
||||
BeginRemoteNativeDragAndDrop(
|
||||
window,
|
||||
refs.ToArray(),
|
||||
new string[0],
|
||||
DragAndDropVisualMode.Move,
|
||||
$"Started synthetic Hierarchy GameObject drag: count={refs.Count}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-DnD] Failed to start synthetic Hierarchy GameObject drag: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeginRemoteNativeDragAndDrop(
|
||||
EditorWindow sourceWindow,
|
||||
UnityEngine.Object[] objectReferences,
|
||||
string[] paths,
|
||||
DragAndDropVisualMode visualMode,
|
||||
string logMessage)
|
||||
{
|
||||
DragAndDrop.PrepareStartDrag();
|
||||
DragAndDrop.objectReferences = objectReferences;
|
||||
DragAndDrop.paths = paths;
|
||||
DragAndDrop.visualMode = visualMode;
|
||||
ClearRemoteDragControls();
|
||||
s_CompositeMouseCaptureView = null;
|
||||
s_CompositeMouseCaptureSlot = null;
|
||||
s_RemoteNativeDragAndDropActive = true;
|
||||
s_RemoteNativeDragSourceWindow = sourceWindow;
|
||||
ClearSyntheticDragGestureState();
|
||||
s_DragAndDropTestingLogCount++;
|
||||
if (s_DragAndDropTestingLogCount <= 10 || s_DragAndDropTestingLogCount % 100 == 0)
|
||||
LogVerbose($"[NWB-DnD] {logMessage}");
|
||||
}
|
||||
|
||||
private static bool TryBlockNativeEditorDragStart(
|
||||
EditorWindow window,
|
||||
string inputType,
|
||||
Vector2 mousePosition)
|
||||
{
|
||||
if (window == null) return false;
|
||||
if (!ShouldBlockNativeEditorDragStart(inputType, window.GetType().FullName ?? window.GetType().Name))
|
||||
return false;
|
||||
if (!HasSyntheticDragThresholdMet(window, inputType, mousePosition))
|
||||
return false;
|
||||
|
||||
ClearRemoteDragControls();
|
||||
s_CompositeMouseCaptureView = null;
|
||||
s_CompositeMouseCaptureSlot = null;
|
||||
ClearSourceDragSelection(window);
|
||||
ScheduleNativeDragDropCancelWatchdog();
|
||||
s_BlockedNativeProjectDragLogCount++;
|
||||
if (s_BlockedNativeProjectDragLogCount <= 10 || s_BlockedNativeProjectDragLogCount % 100 == 0)
|
||||
LogVerbose("[NWB-DnD] Blocked native editor DragAndDrop.StartDrag fallback");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryForwardRemoteNativeDragAndDropEvent(
|
||||
EditorWindow target,
|
||||
string inputType,
|
||||
Vector2 mousePosition,
|
||||
EventModifiers modifiers)
|
||||
{
|
||||
if (!s_RemoteNativeDragAndDropActive) return false;
|
||||
if (target == null || !ShouldForwardNativeDragAndDropEvent(inputType)) return false;
|
||||
if (!HasActiveUnityDragAndDropData())
|
||||
{
|
||||
FinishRemoteNativeDragAndDrop();
|
||||
return false;
|
||||
}
|
||||
|
||||
Event dragUpdated = new Event
|
||||
{
|
||||
type = EventType.DragUpdated,
|
||||
mousePosition = mousePosition,
|
||||
modifiers = modifiers
|
||||
};
|
||||
target.SendEvent(dragUpdated);
|
||||
|
||||
if (inputType == "mouseup")
|
||||
{
|
||||
try
|
||||
{
|
||||
ScheduleRemoteDragModalCancelWatchdog();
|
||||
Event dragPerform = new Event
|
||||
{
|
||||
type = EventType.DragPerform,
|
||||
mousePosition = mousePosition,
|
||||
modifiers = modifiers
|
||||
};
|
||||
target.SendEvent(dragPerform);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FinishRemoteNativeDragAndDrop();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void FinishRemoteNativeDragAndDrop()
|
||||
{
|
||||
s_RemoteNativeDragAndDropActive = false;
|
||||
s_NativeDragCancelWatchdogVersion++;
|
||||
ClearSyntheticDragGestureState();
|
||||
ClearSourceDragSelection(s_RemoteNativeDragSourceWindow);
|
||||
s_RemoteNativeDragSourceWindow = null;
|
||||
ClearProjectAssetDragSnapshot();
|
||||
DragAndDrop.PrepareStartDrag();
|
||||
}
|
||||
|
||||
private static void ClearRemoteDragControls()
|
||||
{
|
||||
GUIUtility.hotControl = 0;
|
||||
DragAndDrop.activeControlID = 0;
|
||||
}
|
||||
|
||||
private static void ClearSourceDragSelection(EditorWindow window)
|
||||
{
|
||||
ClearProjectBrowserListAreaDragSelection(window);
|
||||
ClearSceneHierarchyDragSelection(window);
|
||||
}
|
||||
|
||||
private static bool HasActiveUnityDragAndDropData()
|
||||
{
|
||||
bool hasObjects = DragAndDrop.objectReferences != null &&
|
||||
DragAndDrop.objectReferences.Length > 0;
|
||||
bool hasPaths = DragAndDrop.paths != null &&
|
||||
DragAndDrop.paths.Length > 0;
|
||||
return hasObjects || hasPaths;
|
||||
}
|
||||
|
||||
private static void CaptureProjectAssetSelectionAfterMouseDown(EditorWindow window, string inputType)
|
||||
{
|
||||
if (window == null) return;
|
||||
string windowTypeName = window.GetType().FullName ?? window.GetType().Name;
|
||||
if (!ShouldCaptureProjectAssetSelectionForInput(inputType, windowTypeName))
|
||||
{
|
||||
if (inputType == "mousedown")
|
||||
ClearProjectAssetDragSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ids = new System.Collections.Generic.List<int>();
|
||||
AddProjectBrowserListAreaDragSelection(window, ids);
|
||||
AddProjectBrowserListAreaSelection(window, ids);
|
||||
AddProjectBrowserTreeStateSelection(window, "m_AssetTreeState", ids);
|
||||
AddProjectBrowserTreeStateSelection(window, "m_FolderTreeState", ids);
|
||||
SetProjectAssetDragSnapshot(ids);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ClearProjectAssetDragSnapshot();
|
||||
if (s_ProjectSelectionSnapshotLogCount <= 5)
|
||||
CodelyLogger.LogWarning($"[NWB-DnD] Failed to capture Project selection snapshot: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddProjectBrowserListAreaSelection(
|
||||
EditorWindow window,
|
||||
System.Collections.Generic.List<int> ids)
|
||||
{
|
||||
object listArea = GetMemberValue(window, "ListArea") ?? GetMemberValue(window, "m_ListArea");
|
||||
if (listArea == null) return;
|
||||
|
||||
var getSelection = listArea.GetType().GetMethod(
|
||||
"GetSelection",
|
||||
System.Reflection.BindingFlags.Instance |
|
||||
System.Reflection.BindingFlags.Public |
|
||||
System.Reflection.BindingFlags.NonPublic);
|
||||
if (getSelection?.Invoke(listArea, null) is int[] selection)
|
||||
AddUniqueIds(ids, selection);
|
||||
}
|
||||
|
||||
private static void AddProjectBrowserListAreaDragSelection(
|
||||
EditorWindow window,
|
||||
System.Collections.Generic.List<int> ids)
|
||||
{
|
||||
object listArea = GetMemberValue(window, "ListArea") ?? GetMemberValue(window, "m_ListArea");
|
||||
object localAssets = GetMemberValue(listArea, "m_LocalAssets");
|
||||
object dragSelection = GetMemberValue(localAssets, "m_DragSelection");
|
||||
if (dragSelection is IEnumerable enumerable)
|
||||
AddUniqueIds(ids, enumerable);
|
||||
}
|
||||
|
||||
private static void ClearProjectBrowserListAreaDragSelection(EditorWindow window)
|
||||
{
|
||||
if (window == null) return;
|
||||
object listArea = GetMemberValue(window, "ListArea") ?? GetMemberValue(window, "m_ListArea");
|
||||
object localAssets = GetMemberValue(listArea, "m_LocalAssets");
|
||||
object dragSelection = GetMemberValue(localAssets, "m_DragSelection");
|
||||
ClearListLikeObject(dragSelection);
|
||||
}
|
||||
|
||||
private static void AddHierarchyDragGameObjects(
|
||||
EditorWindow window,
|
||||
System.Collections.Generic.List<UnityEngine.Object> refs)
|
||||
{
|
||||
object treeView = GetSceneHierarchyTreeView(window);
|
||||
object dragSelection = GetMemberValue(treeView, "m_DragSelection");
|
||||
object selected = InvokeMember(dragSelection, "Get");
|
||||
if (selected is IEnumerable enumerable)
|
||||
{
|
||||
foreach (object value in enumerable)
|
||||
{
|
||||
if (!(value is int id)) continue;
|
||||
UnityEngine.Object obj = InstanceIdExtensions.InstanceIdToObject(id);
|
||||
if (obj is GameObject && !refs.Contains(obj))
|
||||
refs.Add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearSceneHierarchyDragSelection(EditorWindow window)
|
||||
{
|
||||
object treeView = GetSceneHierarchyTreeView(window);
|
||||
object dragSelection = GetMemberValue(treeView, "m_DragSelection");
|
||||
ClearListLikeObject(dragSelection);
|
||||
}
|
||||
|
||||
private static object GetSceneHierarchyTreeView(EditorWindow window)
|
||||
{
|
||||
object sceneHierarchy = GetMemberValue(window, "sceneHierarchy") ?? GetMemberValue(window, "m_SceneHierarchy");
|
||||
return GetMemberValue(sceneHierarchy, "treeView") ?? GetMemberValue(sceneHierarchy, "m_TreeView");
|
||||
}
|
||||
|
||||
private static void AddProjectBrowserTreeStateSelection(
|
||||
EditorWindow window,
|
||||
string fieldName,
|
||||
System.Collections.Generic.List<int> ids)
|
||||
{
|
||||
object state = GetMemberValue(window, fieldName);
|
||||
object selected = GetMemberValue(state, "selectedIDs") ?? GetMemberValue(state, "m_SelectedIDs");
|
||||
if (selected is IEnumerable enumerable)
|
||||
{
|
||||
foreach (object value in enumerable)
|
||||
{
|
||||
if (value is int id && !ids.Contains(id))
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetMemberValue(object target, string name)
|
||||
{
|
||||
if (target == null) return null;
|
||||
var type = target.GetType();
|
||||
var flags = System.Reflection.BindingFlags.Instance |
|
||||
System.Reflection.BindingFlags.Public |
|
||||
System.Reflection.BindingFlags.NonPublic;
|
||||
var property = type.GetProperty(name, flags);
|
||||
if (property != null)
|
||||
return property.GetValue(target);
|
||||
var field = type.GetField(name, flags);
|
||||
return field?.GetValue(target);
|
||||
}
|
||||
|
||||
private static object InvokeMember(object target, string name)
|
||||
{
|
||||
if (target == null) return null;
|
||||
var method = target.GetType().GetMethod(
|
||||
name,
|
||||
System.Reflection.BindingFlags.Instance |
|
||||
System.Reflection.BindingFlags.Public |
|
||||
System.Reflection.BindingFlags.NonPublic,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
return method?.Invoke(target, null);
|
||||
}
|
||||
|
||||
private static void AddUniqueIds(System.Collections.Generic.List<int> ids, int[] values)
|
||||
{
|
||||
if (values == null) return;
|
||||
foreach (int id in values)
|
||||
{
|
||||
if (id != 0 && !ids.Contains(id))
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddUniqueIds(System.Collections.Generic.List<int> ids, IEnumerable values)
|
||||
{
|
||||
if (values == null) return;
|
||||
foreach (object value in values)
|
||||
{
|
||||
if (value is int id && id != 0 && !ids.Contains(id))
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ClearListLikeObject(object value)
|
||||
{
|
||||
if (value == null) return false;
|
||||
var clear = value.GetType().GetMethod(
|
||||
"Clear",
|
||||
System.Reflection.BindingFlags.Instance |
|
||||
System.Reflection.BindingFlags.Public |
|
||||
System.Reflection.BindingFlags.NonPublic,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
if (clear == null) return false;
|
||||
clear.Invoke(value, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SetProjectAssetDragSnapshot(System.Collections.Generic.List<int> ids)
|
||||
{
|
||||
var refs = new System.Collections.Generic.List<UnityEngine.Object>();
|
||||
var paths = new System.Collections.Generic.List<string>();
|
||||
foreach (int id in ids)
|
||||
{
|
||||
string path = InstanceIdExtensions.AssetPathFromInstanceId(id);
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
UnityEngine.Object obj = InstanceIdExtensions.InstanceIdToObject(id);
|
||||
if (obj == null)
|
||||
obj = AssetDatabase.LoadMainAssetAtPath(path);
|
||||
if (obj == null) continue;
|
||||
refs.Add(obj);
|
||||
paths.Add(path);
|
||||
}
|
||||
|
||||
s_ProjectAssetDragObjects = refs.Count > 0 ? refs.ToArray() : null;
|
||||
s_ProjectAssetDragPaths = paths.Count > 0 ? paths.ToArray() : null;
|
||||
s_ProjectSelectionSnapshotLogCount++;
|
||||
if (s_ProjectSelectionSnapshotLogCount <= 10 || s_ProjectSelectionSnapshotLogCount % 100 == 0)
|
||||
LogVerbose($"[NWB-DnD] Project selection snapshot: ids={ids.Count} assets={refs.Count}");
|
||||
}
|
||||
|
||||
private static void AddCachedProjectAssetDragObjects(
|
||||
System.Collections.Generic.List<UnityEngine.Object> refs,
|
||||
System.Collections.Generic.List<string> paths)
|
||||
{
|
||||
if (s_ProjectAssetDragObjects == null || s_ProjectAssetDragPaths == null)
|
||||
return;
|
||||
int count = Math.Min(s_ProjectAssetDragObjects.Length, s_ProjectAssetDragPaths.Length);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
UnityEngine.Object obj = s_ProjectAssetDragObjects[i];
|
||||
string path = s_ProjectAssetDragPaths[i];
|
||||
if (obj == null || string.IsNullOrEmpty(path)) continue;
|
||||
refs.Add(obj);
|
||||
paths.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearProjectAssetDragSnapshot()
|
||||
{
|
||||
s_ProjectAssetDragObjects = null;
|
||||
s_ProjectAssetDragPaths = null;
|
||||
}
|
||||
|
||||
private static void ScheduleNativeDragDropCancelWatchdog()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
var hwnds = GetAllUnityHwnds();
|
||||
if (hwnds == null || hwnds.Count == 0)
|
||||
return;
|
||||
|
||||
int version = ++s_NativeDragCancelWatchdogVersion;
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
const int vkEscape = 0x1B;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
if (version != s_NativeDragCancelWatchdogVersion)
|
||||
return;
|
||||
|
||||
foreach (IntPtr hwnd in hwnds)
|
||||
{
|
||||
if (hwnd == IntPtr.Zero) continue;
|
||||
PostMessageW(hwnd, WM_CANCELMODE, IntPtr.Zero, IntPtr.Zero);
|
||||
PostMessageW(hwnd, WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero);
|
||||
PostMessageW(hwnd, WM_KEYDOWN, (IntPtr)vkEscape, IntPtr.Zero);
|
||||
PostMessageW(hwnd, WM_KEYUP, (IntPtr)vkEscape, IntPtr.Zero);
|
||||
}
|
||||
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void ScheduleRemoteDragModalCancelWatchdog()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
var baseline = new HashSet<IntPtr>(GetAllUnityHwnds());
|
||||
foreach (var hwnd in s_TransparentHwnds)
|
||||
baseline.Add(hwnd);
|
||||
if (s_MaskWindowHwnd != IntPtr.Zero)
|
||||
baseline.Add(s_MaskWindowHwnd);
|
||||
if (s_OffscreenTargetContainerHwnd != IntPtr.Zero)
|
||||
baseline.Add(s_OffscreenTargetContainerHwnd);
|
||||
|
||||
int version = ++s_NativeDragCancelWatchdogVersion;
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
const int maxAttempts = 160; // ~4s at 25ms; enough for modal creation during DragPerform.
|
||||
const int sleepMs = 25;
|
||||
const uint vkEscape = 0x1B;
|
||||
const uint wmClose = 0x0010;
|
||||
|
||||
for (int i = 0; i < maxAttempts; i++)
|
||||
{
|
||||
if (version != s_NativeDragCancelWatchdogVersion)
|
||||
return;
|
||||
|
||||
IntPtr modalHwnd = FindNewRemoteDragBlockingModal(baseline);
|
||||
if (modalHwnd != IntPtr.Zero)
|
||||
{
|
||||
string className = GetHwndClassName(modalHwnd);
|
||||
string title = GetHwndTitle(modalHwnd);
|
||||
PostMessageW(modalHwnd, WM_CANCELMODE, IntPtr.Zero, IntPtr.Zero);
|
||||
PostMessageW(modalHwnd, WM_KEYDOWN, (IntPtr)vkEscape, IntPtr.Zero);
|
||||
PostMessageW(modalHwnd, WM_KEYUP, (IntPtr)vkEscape, IntPtr.Zero);
|
||||
PostMessageW(modalHwnd, wmClose, IntPtr.Zero, IntPtr.Zero);
|
||||
CodelyLogger.Log(
|
||||
$"[NWB-DnD] Auto-canceled blocking modal after remote drag: " +
|
||||
$"hwnd=0x{modalHwnd.ToInt64():X} class={className} title=\"{title}\"");
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.Sleep(sleepMs);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
private static IntPtr FindNewRemoteDragBlockingModal(HashSet<IntPtr> baseline)
|
||||
{
|
||||
IntPtr result = IntPtr.Zero;
|
||||
uint currentPid = (uint)System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
|
||||
IntPtr hwnd = GetWindow(GetDesktopWindow(), GW_CHILD);
|
||||
int safety = 0;
|
||||
while (hwnd != IntPtr.Zero && safety < 4096)
|
||||
{
|
||||
if (!baseline.Contains(hwnd) && hwnd != s_MaskWindowHwnd)
|
||||
{
|
||||
GetWindowThreadProcessId(hwnd, out uint pid);
|
||||
if (pid == currentPid && IsWindowVisible(hwnd))
|
||||
{
|
||||
string className = GetHwndClassName(hwnd);
|
||||
if (IsRemoteDragBlockingModalClass(hwnd, className))
|
||||
{
|
||||
result = hwnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hwnd = GetWindow(hwnd, GW_HWNDNEXT);
|
||||
safety++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsRemoteDragBlockingModalClass(IntPtr hwnd, string className)
|
||||
{
|
||||
if (string.IsNullOrEmpty(className) || className == "#32768")
|
||||
return false;
|
||||
if (className == "#32770")
|
||||
return true;
|
||||
|
||||
string title = GetHwndTitle(hwnd);
|
||||
if (string.IsNullOrEmpty(title) || title.EndsWith(" - Streaming", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
IntPtr owner = GetWindow(hwnd, GW_OWNER);
|
||||
bool ownedUnityWindow = owner != IntPtr.Zero &&
|
||||
className.IndexOf("Unity", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool containerLikeDialog = className.IndexOf("Container", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
className.IndexOf("Unity", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
return ownedUnityWindow || containerLikeDialog;
|
||||
}
|
||||
|
||||
private static string GetHwndClassName(IntPtr hwnd)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(256);
|
||||
return GetClassNameW(hwnd, sb, sb.Capacity) > 0 ? sb.ToString() : "";
|
||||
}
|
||||
|
||||
private static string GetHwndTitle(IntPtr hwnd)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(256);
|
||||
return GetWindowTextW(hwnd, sb, sb.Capacity) > 0 ? sb.ToString() : "";
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetWindowTextW(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
|
||||
|
||||
private const uint GW_OWNER = 4;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b4c0cb574e24535b75009a3555ad4d0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private const float kIconSelectorPopupWidth = 140f;
|
||||
private const float kIconSelectorPopupHeightWithLabels = 126f;
|
||||
private const float kIconSelectorPopupHeightNoLabels = 86f;
|
||||
private const int kIconSelectorLabelCount = 8;
|
||||
private const int kIconSelectorDotCount = 16;
|
||||
|
||||
private const string kIconSelectorPathNone = "__none__";
|
||||
|
||||
private static bool s_IconSelectorReflectionDone;
|
||||
private static Type s_IconSelectorType;
|
||||
private static FieldInfo s_IconSelectorTargetObjectField;
|
||||
private static FieldInfo s_IconSelectorTargetObjectListField;
|
||||
private static FieldInfo s_IconSelectorShowLabelIconsField;
|
||||
private static FieldInfo s_IconSelectorMultipleSelectedIconsField;
|
||||
private static MethodInfo s_IconSelectorCopyIconToImporterMethod;
|
||||
|
||||
private static EditorWindow s_CachedIconSelectorWindow;
|
||||
private static UnityEngine.Object[] s_IconSelectorTargetObjects;
|
||||
private static bool s_IconSelectorSessionActive;
|
||||
private static bool s_IconSelectorShowLabelIcons;
|
||||
private static string s_IconSelectorSelectedPath = "";
|
||||
|
||||
private static void EnsureIconSelectorReflection()
|
||||
{
|
||||
if (s_IconSelectorReflectionDone) return;
|
||||
s_IconSelectorReflectionDone = true;
|
||||
|
||||
try
|
||||
{
|
||||
var editorAsm = typeof(EditorWindow).Assembly;
|
||||
s_IconSelectorType = editorAsm.GetType("UnityEditor.IconSelector");
|
||||
if (s_IconSelectorType == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-IconSelector] IconSelector type not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const BindingFlags inst = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
|
||||
const BindingFlags stat = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
|
||||
|
||||
s_IconSelectorTargetObjectField = s_IconSelectorType.GetField("m_TargetObject", inst);
|
||||
s_IconSelectorTargetObjectListField = s_IconSelectorType.GetField("m_TargetObjectList", inst);
|
||||
s_IconSelectorShowLabelIconsField = s_IconSelectorType.GetField("m_ShowLabelIcons", inst);
|
||||
s_IconSelectorMultipleSelectedIconsField = s_IconSelectorType.GetField("m_MultipleSelectedIcons", inst);
|
||||
s_IconSelectorCopyIconToImporterMethod = s_IconSelectorType.GetMethod(
|
||||
"CopyIconToImporter", stat, null, new[] { typeof(MonoScript) }, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] Reflection init failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExtractIconSelectorPopup(
|
||||
EditorWindow hostedWindow, float relX, float relY, float popW, float popH)
|
||||
{
|
||||
EnsureIconSelectorReflection();
|
||||
if (hostedWindow == null || s_IconSelectorType == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
s_CachedIconSelectorWindow = hostedWindow;
|
||||
s_IconSelectorTargetObjects = s_IconSelectorTargetObjectListField?.GetValue(hostedWindow) as UnityEngine.Object[];
|
||||
if (s_IconSelectorTargetObjects == null || s_IconSelectorTargetObjects.Length == 0)
|
||||
{
|
||||
UnityEngine.Object single =
|
||||
s_IconSelectorTargetObjectField?.GetValue(hostedWindow) as UnityEngine.Object;
|
||||
if (single != null)
|
||||
s_IconSelectorTargetObjects = new[] { single };
|
||||
}
|
||||
|
||||
if (s_IconSelectorTargetObjects == null || s_IconSelectorTargetObjects.Length == 0)
|
||||
{
|
||||
LogVerbose("[NWB-IconSelector] No target objects on IconSelector");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_IconSelectorShowLabelIcons = s_IconSelectorShowLabelIconsField != null &&
|
||||
(bool)s_IconSelectorShowLabelIconsField.GetValue(hostedWindow);
|
||||
bool multiple = s_IconSelectorMultipleSelectedIconsField != null &&
|
||||
(bool)s_IconSelectorMultipleSelectedIconsField.GetValue(hostedWindow);
|
||||
s_IconSelectorSelectedPath = multiple ? "" : ResolveIconSelectorSelectedPath(s_IconSelectorTargetObjects[0]);
|
||||
|
||||
TryHideIconSelectorNativeWindow(hostedWindow);
|
||||
|
||||
s_LastFrontendPopupX = relX;
|
||||
s_LastFrontendPopupY = relY;
|
||||
|
||||
float height = s_IconSelectorShowLabelIcons
|
||||
? kIconSelectorPopupHeightWithLabels
|
||||
: kIconSelectorPopupHeightNoLabels;
|
||||
float width = Mathf.Max(popW, kIconSelectorPopupWidth);
|
||||
height = Mathf.Max(popH, height);
|
||||
|
||||
return TrySendIconSelectorPopup(relX, relY, width, height);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] Extract error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryHideIconSelectorNativeWindow(EditorWindow window)
|
||||
{
|
||||
if (window == null) return;
|
||||
try
|
||||
{
|
||||
Rect pos = window.position;
|
||||
pos.x = -20000f;
|
||||
pos.y = -20000f;
|
||||
window.position = pos;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] Hide native window failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySendIconSelectorPopup(float relX, float relY, float popW, float popH)
|
||||
{
|
||||
try
|
||||
{
|
||||
s_FrontendPopupIdCounter++;
|
||||
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
|
||||
s_PopupSourceType = "icon_selector";
|
||||
s_IconSelectorSessionActive = true;
|
||||
|
||||
var sb = new StringBuilder(8192);
|
||||
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
|
||||
sb.Append(s_CurrentFrontendPopupId);
|
||||
sb.Append("\",\"sourceType\":\"icon_selector\",\"x\":");
|
||||
sb.Append(Mathf.RoundToInt(relX));
|
||||
sb.Append(",\"y\":");
|
||||
sb.Append(Mathf.RoundToInt(relY));
|
||||
sb.Append(",\"width\":");
|
||||
sb.Append(Mathf.RoundToInt(popW));
|
||||
sb.Append(",\"height\":");
|
||||
sb.Append(Mathf.RoundToInt(popH));
|
||||
sb.Append(",\"title\":\"Select Icon\",\"showLabelIcons\":");
|
||||
sb.Append(s_IconSelectorShowLabelIcons ? "true" : "false");
|
||||
sb.Append(",\"selectedPath\":\"");
|
||||
sb.Append(EscapeJsonString(s_IconSelectorSelectedPath ?? ""));
|
||||
sb.Append("\"");
|
||||
|
||||
if (s_IconSelectorShowLabelIcons)
|
||||
{
|
||||
sb.Append(",\"labelIcons\":[");
|
||||
AppendIconSelectorIconArray(sb, "label", kIconSelectorLabelCount, true);
|
||||
sb.Append(']');
|
||||
}
|
||||
|
||||
sb.Append(",\"dotIcons\":[");
|
||||
AppendIconSelectorIconArray(sb, "dot", kIconSelectorDotCount, false);
|
||||
sb.Append("]}");
|
||||
|
||||
bool sent = SendDataChannelMessage(sb.ToString());
|
||||
if (sent)
|
||||
{
|
||||
s_FrontendPopupSent = true;
|
||||
LogVerbose("[NWB-IconSelector] Sent icon selector to frontend");
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearIconSelectorPopupState();
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] Send popup failed: {ex.Message}");
|
||||
ClearIconSelectorPopupState();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendIconSelectorIconArray(StringBuilder sb, string kind, int count, bool labelIcon)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
string iconName = labelIcon
|
||||
? "sv_icon_name" + i
|
||||
: "sv_icon_dot" + i + "_sml";
|
||||
string image = EncodeEditorIconDataUri(iconName);
|
||||
sb.Append("{\"path\":\"");
|
||||
sb.Append(kind);
|
||||
sb.Append(':');
|
||||
sb.Append(i);
|
||||
sb.Append("\",\"image\":\"");
|
||||
sb.Append(EscapeJsonString(image));
|
||||
sb.Append("\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string EncodeEditorIconDataUri(string iconName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iconName))
|
||||
return "";
|
||||
try
|
||||
{
|
||||
GUIContent content = EditorGUIUtility.IconContent(iconName);
|
||||
return TryEncodeAnnotationIconPngDataUri(content?.image as Texture2D);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodInfo s_GetIconForObjectMethod;
|
||||
private static MethodInfo s_SetIconForObjectMethod;
|
||||
|
||||
private static Texture2D GetIconForObjectReflect(UnityEngine.Object obj)
|
||||
{
|
||||
if (obj == null) return null;
|
||||
try
|
||||
{
|
||||
if (s_GetIconForObjectMethod == null)
|
||||
{
|
||||
s_GetIconForObjectMethod = typeof(EditorGUIUtility).GetMethod(
|
||||
"GetIconForObject",
|
||||
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
|
||||
null,
|
||||
new[] { typeof(UnityEngine.Object) },
|
||||
null);
|
||||
}
|
||||
|
||||
return s_GetIconForObjectMethod?.Invoke(null, new object[] { obj }) as Texture2D;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetIconForObjectReflect(UnityEngine.Object obj, Texture2D icon)
|
||||
{
|
||||
if (obj == null) return;
|
||||
try
|
||||
{
|
||||
if (s_SetIconForObjectMethod == null)
|
||||
{
|
||||
s_SetIconForObjectMethod = typeof(EditorGUIUtility).GetMethod(
|
||||
"SetIconForObject",
|
||||
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
|
||||
null,
|
||||
new[] { typeof(UnityEngine.Object), typeof(Texture2D) },
|
||||
null);
|
||||
}
|
||||
|
||||
s_SetIconForObjectMethod?.Invoke(null, new object[] { obj, icon });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] SetIconForObject failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveIconSelectorSelectedPath(UnityEngine.Object target)
|
||||
{
|
||||
Texture2D icon = GetIconForObjectReflect(target);
|
||||
if (icon == null)
|
||||
return kIconSelectorPathNone;
|
||||
|
||||
if (s_IconSelectorShowLabelIcons)
|
||||
{
|
||||
for (int i = 0; i < kIconSelectorLabelCount; i++)
|
||||
{
|
||||
if (IconMatchesName(icon, "sv_icon_name" + i) || IconMatchesName(icon, "sv_label_" + i))
|
||||
return "label:" + i;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < kIconSelectorDotCount; i++)
|
||||
{
|
||||
if (IconMatchesName(icon, "sv_icon_dot" + i + "_sml") ||
|
||||
IconMatchesName(icon, "sv_icon_dot" + i + "_pix16_gizmo"))
|
||||
{
|
||||
return "dot:" + i;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static bool IconMatchesName(Texture2D icon, string iconName)
|
||||
{
|
||||
if (icon == null || string.IsNullOrEmpty(iconName))
|
||||
return false;
|
||||
GUIContent content = EditorGUIUtility.IconContent(iconName);
|
||||
return content?.image != null && ReferenceEquals(content.image, icon);
|
||||
}
|
||||
|
||||
private static Texture2D ResolveLargeIconTexture(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
if (path == kIconSelectorPathNone)
|
||||
return null;
|
||||
|
||||
if (path.StartsWith("label:", StringComparison.Ordinal))
|
||||
{
|
||||
if (int.TryParse(path.Substring("label:".Length), out int index))
|
||||
{
|
||||
GUIContent content = EditorGUIUtility.IconContent("sv_label_" + index);
|
||||
return content?.image as Texture2D;
|
||||
}
|
||||
}
|
||||
|
||||
if (path.StartsWith("dot:", StringComparison.Ordinal))
|
||||
{
|
||||
if (int.TryParse(path.Substring("dot:".Length), out int index))
|
||||
{
|
||||
GUIContent content = EditorGUIUtility.IconContent("sv_icon_dot" + index + "_pix16_gizmo");
|
||||
return content?.image as Texture2D;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ApplyIconToIconSelectorTargets(Texture2D icon)
|
||||
{
|
||||
UnityEngine.Object[] targets = s_IconSelectorTargetObjects;
|
||||
if (targets == null || targets.Length == 0)
|
||||
return;
|
||||
|
||||
Undo.RecordObjects(targets, "Set Icon On GameObject");
|
||||
foreach (UnityEngine.Object target in targets)
|
||||
{
|
||||
if (target == null) continue;
|
||||
SetIconForObjectReflect(target, icon);
|
||||
|
||||
if (target is MonoScript monoScript && icon != null)
|
||||
s_IconSelectorCopyIconToImporterMethod?.Invoke(null, new object[] { monoScript });
|
||||
}
|
||||
|
||||
TryForceReloadInspectors();
|
||||
TryInvokeAnnotationWindowIconChanged();
|
||||
}
|
||||
|
||||
private static void TryForceReloadInspectors()
|
||||
{
|
||||
try
|
||||
{
|
||||
MethodInfo method = typeof(EditorUtility).GetMethod(
|
||||
"ForceReloadInspectors",
|
||||
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
method?.Invoke(null, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// internal API; fall back to repaint-only below
|
||||
}
|
||||
|
||||
InternalEditorUtility.RepaintAllViews();
|
||||
}
|
||||
|
||||
private static void TryInvokeAnnotationWindowIconChanged()
|
||||
{
|
||||
try
|
||||
{
|
||||
Type annotationWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnnotationWindow");
|
||||
MethodInfo iconChanged = annotationWindowType?.GetMethod(
|
||||
"IconChanged",
|
||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
iconChanged?.Invoke(null, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// optional
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the icon selector popup should stay open.
|
||||
/// </summary>
|
||||
private static bool TryHandleIconSelectorPopupSelect(string path)
|
||||
{
|
||||
if (!s_IconSelectorSessionActive)
|
||||
return false;
|
||||
|
||||
if (path == kIconSelectorPathNone)
|
||||
{
|
||||
ApplyIconToIconSelectorTargets(null);
|
||||
s_IconSelectorSelectedPath = kIconSelectorPathNone;
|
||||
return true;
|
||||
}
|
||||
|
||||
Texture2D icon = ResolveLargeIconTexture(path);
|
||||
if (icon == null && path != kIconSelectorPathNone)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] Unknown icon path: {path}");
|
||||
return true;
|
||||
}
|
||||
|
||||
ApplyIconToIconSelectorTargets(icon);
|
||||
s_IconSelectorSelectedPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CloseNativeIconSelectorWindow()
|
||||
{
|
||||
if (s_CachedIconSelectorWindow == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
s_CachedIconSelectorWindow.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-IconSelector] Close failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CancelIconSelectorSession()
|
||||
{
|
||||
CloseNativeIconSelectorWindow();
|
||||
ClearIconSelectorPopupState();
|
||||
if (s_OffscreenTarget != null)
|
||||
{
|
||||
s_OffscreenTarget.Repaint();
|
||||
InternalEditorUtility.RepaintAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearIconSelectorPopupState()
|
||||
{
|
||||
s_CachedIconSelectorWindow = null;
|
||||
s_IconSelectorTargetObjects = null;
|
||||
s_IconSelectorSessionActive = false;
|
||||
s_IconSelectorShowLabelIcons = false;
|
||||
s_IconSelectorSelectedPath = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce4f20ec6ac047238d7a3c651f3ab061
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
// Minimal JSON parsing helpers shared across the NativeWindowBridgeHost
|
||||
// partial class files. These avoid pulling in a full JSON library for
|
||||
// the small, well-known message structures used by the bridge protocol.
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private static string ExtractJsonString(string json, string key)
|
||||
{
|
||||
string search = "\"" + key + "\":\"";
|
||||
int pos = json.IndexOf(search, StringComparison.Ordinal);
|
||||
if (pos < 0) return "";
|
||||
pos += search.Length;
|
||||
int end = json.IndexOf('"', pos);
|
||||
return end < 0 ? "" : json.Substring(pos, end - pos);
|
||||
}
|
||||
|
||||
private static float ExtractJsonFloat(string json, string key)
|
||||
{
|
||||
string search = "\"" + key + "\":";
|
||||
int pos = json.IndexOf(search, StringComparison.Ordinal);
|
||||
if (pos < 0) return 0f;
|
||||
pos += search.Length;
|
||||
int end = pos;
|
||||
while (end < json.Length && (char.IsDigit(json[end]) || json[end] == '.' || json[end] == '-' || json[end] == 'e' || json[end] == 'E' || json[end] == '+'))
|
||||
end++;
|
||||
if (float.TryParse(json.Substring(pos, end - pos), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out float val))
|
||||
return val;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static int ExtractJsonInt(string json, string key)
|
||||
{
|
||||
return (int)ExtractJsonFloat(json, key);
|
||||
}
|
||||
|
||||
private static bool ExtractJsonBool(string json, string key)
|
||||
{
|
||||
string search = "\"" + key + "\":";
|
||||
int pos = json.IndexOf(search, StringComparison.Ordinal);
|
||||
if (pos < 0) return false;
|
||||
pos += search.Length;
|
||||
while (pos < json.Length && json[pos] == ' ') pos++;
|
||||
return pos < json.Length && json[pos] == 't';
|
||||
}
|
||||
|
||||
private static float ExtractJsonArrayFloat(string json, string key, int index)
|
||||
{
|
||||
string search = "\"" + key + "\":[";
|
||||
int pos = json.IndexOf(search, StringComparison.Ordinal);
|
||||
if (pos < 0) return 0f;
|
||||
pos += search.Length;
|
||||
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
int comma = json.IndexOf(',', pos);
|
||||
if (comma < 0) return 0f;
|
||||
pos = comma + 1;
|
||||
}
|
||||
|
||||
int end = pos;
|
||||
while (end < json.Length && json[end] != ',' && json[end] != ']')
|
||||
end++;
|
||||
|
||||
if (float.TryParse(json.Substring(pos, end - pos).Trim(),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out float val))
|
||||
return val;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// Used only by SaveCompositeSlotMapping / PrePopulateCompositeSlots
|
||||
// for the internal {s, i, t} JSON format. Kept separate from the
|
||||
// protocol helpers above because the key format differs slightly
|
||||
// (returns null instead of "" on miss).
|
||||
private static string ExtractJsonStringValue(string json, string key)
|
||||
{
|
||||
string pattern = "\"" + key + "\":\"";
|
||||
int start = json.IndexOf(pattern);
|
||||
if (start < 0) return null;
|
||||
start += pattern.Length;
|
||||
int end = json.IndexOf('"', start);
|
||||
if (end < 0) return null;
|
||||
return json.Substring(start, end - start);
|
||||
}
|
||||
|
||||
private static int ExtractJsonIntValue(string json, string key)
|
||||
{
|
||||
string pattern = "\"" + key + "\":";
|
||||
int start = json.IndexOf(pattern);
|
||||
if (start < 0) return 0;
|
||||
start += pattern.Length;
|
||||
int end = start;
|
||||
while (end < json.Length && (char.IsDigit(json[end]) || json[end] == '-'))
|
||||
end++;
|
||||
if (end == start) return 0;
|
||||
int val;
|
||||
return int.TryParse(json.Substring(start, end - start), out val) ? val : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 598f8ac157764224683170b05221725c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+539
@@ -0,0 +1,539 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
// ── New Input System (com.unity.inputsystem) input injection ──
|
||||
//
|
||||
// The new Input System (activeInputHandler=2) processes keyboard through
|
||||
// WM_INPUT (Raw Input), NOT WM_KEYDOWN. The WM_INPUT path is guarded by
|
||||
// `isFocused = GetApplication().IsFocused()` which returns false when the
|
||||
// editor is offscreen. So neither WM_KEYDOWN nor WM_INPUT reaches the new
|
||||
// Input System in streaming/offscreen mode.
|
||||
//
|
||||
// Solution: use InputSystem.QueueStateEvent to directly inject keyboard
|
||||
// state into the new Input System's managed event queue, bypassing the
|
||||
// native Win32 message pipeline and the isFocused check entirely.
|
||||
//
|
||||
// QueueStateEvent replaces the ENTIRE keyboard state, so we must track
|
||||
// all held keys and send the complete state on each keydown/keyup.
|
||||
// s_WinHeldKeys already tracks held keys as browser key strings.
|
||||
private static Type s_InputSystemType;
|
||||
private static Type s_KeyboardType;
|
||||
private static Type s_KeyboardStateType;
|
||||
private static Type s_KeyType;
|
||||
private static System.Reflection.MethodInfo s_QueueStateEventMethod;
|
||||
private static System.Reflection.MethodInfo s_GetDeviceMethod;
|
||||
private static System.Reflection.MethodInfo s_KeySetMethod;
|
||||
private static bool s_InputSystemRefResolved;
|
||||
|
||||
// Mouse-specific reflection cache (resolved alongside keyboard refs).
|
||||
private static Type s_MouseType;
|
||||
private static Type s_MouseStateType;
|
||||
private static System.Reflection.MethodInfo s_MouseGetDeviceMethod;
|
||||
private static System.Reflection.MethodInfo s_MouseQueueStateEventMethod;
|
||||
// Keep the latest known screen position for cleanup paths.
|
||||
private static Vector2 s_InputSystemMouseScreenPos = Vector2.zero;
|
||||
private static bool s_InputSystemMouseScreenPosValid;
|
||||
// Browser button values currently held (for building MouseState.buttons bitmask).
|
||||
private static readonly HashSet<int> s_InputSystemHeldMouseButtons = new HashSet<int>();
|
||||
private static System.Reflection.FieldInfo s_InputManagerInstanceField;
|
||||
private static System.Reflection.FieldInfo s_InputManagerHasFocusField;
|
||||
private static int s_InputSystemDiagCount = 0;
|
||||
|
||||
// Consume one diagnostic log budget slot for noisy reflection/input probes.
|
||||
private static bool TryConsumeInputSystemDiagBudget(int limit = 3)
|
||||
{
|
||||
if (s_InputSystemDiagCount >= limit) return false;
|
||||
s_InputSystemDiagCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Emit InputSystem diagnostics with a shared rate limit.
|
||||
private static void LogInputSystemDiagLimited(string message, int limit = 3)
|
||||
{
|
||||
if (TryConsumeInputSystemDiagBudget(limit))
|
||||
LogVerbose(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazily resolve Input System types via reflection. The package may not
|
||||
/// be installed in all projects, so all access is reflection-based.
|
||||
/// </summary>
|
||||
private static bool TryResolveInputSystemRefs()
|
||||
{
|
||||
if (s_InputSystemRefResolved) return true;
|
||||
|
||||
try
|
||||
{
|
||||
// Find the InputSystem assembly by name first (more reliable than GetType)
|
||||
const string asmName = "Unity.InputSystem";
|
||||
System.Reflection.Assembly inputAsm = null;
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (asm.GetName().Name == asmName)
|
||||
{
|
||||
inputAsm = asm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search by type name
|
||||
if (inputAsm == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var type = asm.GetType("UnityEngine.InputSystem.InputSystem");
|
||||
if (type != null)
|
||||
{
|
||||
inputAsm = asm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputAsm == null)
|
||||
{
|
||||
if (TryConsumeInputSystemDiagBudget())
|
||||
{
|
||||
var asmNames = new List<string>();
|
||||
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
string n = a.GetName().Name;
|
||||
if (n.Contains("Input") || n.Contains("input"))
|
||||
asmNames.Add(n);
|
||||
}
|
||||
LogVerbose($"[NWB-NewInputSystem] Could not find InputSystem assembly. Input-related assemblies: [{string.Join(", ", asmNames)}]");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
s_InputSystemType = inputAsm.GetType("UnityEngine.InputSystem.InputSystem");
|
||||
if (s_InputSystemType == null)
|
||||
{
|
||||
LogInputSystemDiagLimited("[NWB-NewInputSystem] Found assembly but InputSystem type is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_KeyboardType = s_InputSystemType.Assembly.GetType("UnityEngine.InputSystem.Keyboard");
|
||||
s_KeyboardStateType = s_InputSystemType.Assembly.GetType("UnityEngine.InputSystem.LowLevel.KeyboardState");
|
||||
s_KeyType = s_InputSystemType.Assembly.GetType("UnityEngine.InputSystem.Key");
|
||||
|
||||
if (s_KeyboardType == null || s_KeyboardStateType == null || s_KeyType == null)
|
||||
{
|
||||
LogInputSystemDiagLimited($"[NWB-NewInputSystem] Type resolution failed: Keyboard={s_KeyboardType != null} KeyboardState={s_KeyboardStateType != null} Key={s_KeyType != null}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// InputSystem.GetDevice<Keyboard>() -> Keyboard.current
|
||||
// Use GetMethods + filter to avoid AmbiguousMatchException from multiple GetDevice overloads.
|
||||
foreach (var m in s_InputSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
{
|
||||
if (m.Name == "GetDevice" && m.IsGenericMethod && m.GetParameters().Length == 0)
|
||||
{
|
||||
s_GetDeviceMethod = m.MakeGenericMethod(s_KeyboardType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_GetDeviceMethod == null)
|
||||
{
|
||||
LogInputSystemDiagLimited("[NWB-NewInputSystem] GetDevice method not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// InputSystem.QueueStateEvent<TState>(InputDevice device, TState state, double time = -1)
|
||||
foreach (var m in s_InputSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
{
|
||||
if (m.Name == "QueueStateEvent" && m.IsGenericMethod)
|
||||
{
|
||||
var parms = m.GetParameters();
|
||||
if (parms.Length == 3 && parms[2].ParameterType == typeof(double))
|
||||
{
|
||||
s_QueueStateEventMethod = m.MakeGenericMethod(s_KeyboardStateType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_QueueStateEventMethod == null)
|
||||
{
|
||||
LogInputSystemDiagLimited("[NWB-NewInputSystem] QueueStateEvent method not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// KeyboardState.Set(Key key, bool state) - used to set individual key bits.
|
||||
foreach (var m in s_KeyboardStateType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
|
||||
{
|
||||
if (m.Name == "Set")
|
||||
{
|
||||
var parms = m.GetParameters();
|
||||
if (parms.Length == 2 && parms[0].ParameterType == s_KeyType && parms[1].ParameterType == typeof(bool))
|
||||
{
|
||||
s_KeySetMethod = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InputManager.s_Manager (singleton) and m_HasFocus field.
|
||||
var inputManagerType = s_InputSystemType.Assembly.GetType("UnityEngine.InputSystem.InputManager");
|
||||
if (inputManagerType != null)
|
||||
{
|
||||
s_InputManagerInstanceField = s_InputSystemType.GetField("s_Manager",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
if (s_InputManagerInstanceField != null)
|
||||
{
|
||||
s_InputManagerHasFocusField = inputManagerType.GetField("m_HasFocus",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mouse type resolution ──
|
||||
// Same pattern as Keyboard: resolve Mouse device type, MouseState struct,
|
||||
// GetDevice<Mouse>() and QueueStateEvent<MouseState>() via reflection.
|
||||
s_MouseType = s_InputSystemType.Assembly.GetType("UnityEngine.InputSystem.Mouse");
|
||||
s_MouseStateType = s_InputSystemType.Assembly.GetType("UnityEngine.InputSystem.LowLevel.MouseState");
|
||||
|
||||
if (s_MouseType != null && s_MouseStateType != null)
|
||||
{
|
||||
// GetDevice<Mouse>()
|
||||
foreach (var m in s_InputSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
{
|
||||
if (m.Name == "GetDevice" && m.IsGenericMethod && m.GetParameters().Length == 0)
|
||||
{
|
||||
s_MouseGetDeviceMethod = m.MakeGenericMethod(s_MouseType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// QueueStateEvent<MouseState>(InputDevice, MouseState, double)
|
||||
foreach (var m in s_InputSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
{
|
||||
if (m.Name == "QueueStateEvent" && m.IsGenericMethod)
|
||||
{
|
||||
var parms = m.GetParameters();
|
||||
if (parms.Length == 3 && parms[2].ParameterType == typeof(double))
|
||||
{
|
||||
s_MouseQueueStateEventMethod = m.MakeGenericMethod(s_MouseStateType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as resolved only after everything above succeeds.
|
||||
s_InputSystemRefResolved = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogInputSystemDiagLimited($"[NWB-NewInputSystem] TryResolveInputSystemRefs exception: type={ex.GetType().Name} msg={ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an InputSystem.Key enum name using Enum.IsDefined/Enum.Parse,
|
||||
/// returning null on failure instead of throwing ArgumentException.
|
||||
/// </summary>
|
||||
private static object TryParseKey(string keyName)
|
||||
{
|
||||
if (Enum.IsDefined(s_KeyType, keyName))
|
||||
return Enum.Parse(s_KeyType, keyName);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a browser key string to an InputSystem.Key enum value.
|
||||
/// Uses TryParse to avoid throwing on cross-version enum name changes.
|
||||
/// </summary>
|
||||
private static object BrowserKeyToInputSystemKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || s_KeyType == null) return null;
|
||||
|
||||
// Single-character keys: a-z, 0-9, symbols
|
||||
if (key.Length == 1)
|
||||
{
|
||||
char c = key[0];
|
||||
if (c >= 'a' && c <= 'z') return TryParseKey(c.ToString().ToUpperInvariant());
|
||||
if (c >= 'A' && c <= 'Z') return TryParseKey(c.ToString());
|
||||
if (c >= '0' && c <= '9') return TryParseKey("Digit" + c);
|
||||
switch (c)
|
||||
{
|
||||
case ' ': return TryParseKey("Space");
|
||||
case '-': return TryParseKey("Minus");
|
||||
case '=': return TryParseKey("Equals");
|
||||
case '[': return TryParseKey("LeftBracket");
|
||||
case ']': return TryParseKey("RightBracket");
|
||||
case ';': return TryParseKey("Semicolon");
|
||||
case ',': return TryParseKey("Comma");
|
||||
case '.': return TryParseKey("Period");
|
||||
case '/': return TryParseKey("Slash");
|
||||
case '`': return TryParseKey("Backquote");
|
||||
case '\\': return TryParseKey("Backslash");
|
||||
case '\'': return TryParseKey("Quote");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "Enter":
|
||||
case "Return": return TryParseKey("Enter");
|
||||
case "Tab": return TryParseKey("Tab");
|
||||
case "Escape": return TryParseKey("Escape");
|
||||
case "Backspace": return TryParseKey("Backspace");
|
||||
case "Delete": return TryParseKey("Delete");
|
||||
case "Insert": return TryParseKey("Insert");
|
||||
case "Home": return TryParseKey("Home");
|
||||
case "End": return TryParseKey("End");
|
||||
case "PageUp": return TryParseKey("PageUp");
|
||||
case "PageDown": return TryParseKey("PageDown");
|
||||
case "ArrowUp": return TryParseKey("UpArrow");
|
||||
case "ArrowDown": return TryParseKey("DownArrow");
|
||||
case "ArrowLeft": return TryParseKey("LeftArrow");
|
||||
case "ArrowRight": return TryParseKey("RightArrow");
|
||||
case "Shift": return TryParseKey("LeftShift");
|
||||
case "Control": return TryParseKey("LeftCtrl");
|
||||
case "Alt": return TryParseKey("LeftAlt");
|
||||
case "Meta": return TryParseKey("LeftMeta");
|
||||
case "CapsLock": return TryParseKey("CapsLock");
|
||||
case "F1": return TryParseKey("F1");
|
||||
case "F2": return TryParseKey("F2");
|
||||
case "F3": return TryParseKey("F3");
|
||||
case "F4": return TryParseKey("F4");
|
||||
case "F5": return TryParseKey("F5");
|
||||
case "F6": return TryParseKey("F6");
|
||||
case "F7": return TryParseKey("F7");
|
||||
case "F8": return TryParseKey("F8");
|
||||
case "F9": return TryParseKey("F9");
|
||||
case "F10": return TryParseKey("F10");
|
||||
case "F11": return TryParseKey("F11");
|
||||
case "F12": return TryParseKey("F12");
|
||||
// Numpad keys
|
||||
case "Numpad0": return TryParseKey("Numpad0");
|
||||
case "Numpad1": return TryParseKey("Numpad1");
|
||||
case "Numpad2": return TryParseKey("Numpad2");
|
||||
case "Numpad3": return TryParseKey("Numpad3");
|
||||
case "Numpad4": return TryParseKey("Numpad4");
|
||||
case "Numpad5": return TryParseKey("Numpad5");
|
||||
case "Numpad6": return TryParseKey("Numpad6");
|
||||
case "Numpad7": return TryParseKey("Numpad7");
|
||||
case "Numpad8": return TryParseKey("Numpad8");
|
||||
case "Numpad9": return TryParseKey("Numpad9");
|
||||
case "NumpadEnter": return TryParseKey("NumpadEnter");
|
||||
case "NumpadMultiply": return TryParseKey("NumpadMultiply");
|
||||
case "NumpadAdd": return TryParseKey("NumpadAdd");
|
||||
case "NumpadSubtract": return TryParseKey("NumpadSubtract");
|
||||
case "NumpadDecimal": return TryParseKey("NumpadDecimal");
|
||||
case "NumpadDivide": return TryParseKey("NumpadDivide");
|
||||
// Extended keys
|
||||
case "ContextMenu": return TryParseKey("ContextMenu");
|
||||
case "PrintScreen": return TryParseKey("PrintScreen");
|
||||
case "ScrollLock": return TryParseKey("ScrollLock");
|
||||
case "Pause": return TryParseKey("Pause");
|
||||
case "NumLock": return TryParseKey("NumLock");
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject the current set of held keys into the new Input System via QueueStateEvent.
|
||||
/// This bypasses the native Win32 message pipeline and the isFocused check that blocks
|
||||
/// WM_INPUT in offscreen mode. Call after updating s_WinHeldKeys.
|
||||
/// </summary>
|
||||
private static void QueueInputSystemKeyboardState()
|
||||
{
|
||||
if (!TryResolveInputSystemRefs())
|
||||
{
|
||||
LogInputSystemDiagLimited("[NWB-NewInputSystem] TryResolveInputSystemRefs returned false - InputSystem package not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get Keyboard.current
|
||||
object keyboard = s_GetDeviceMethod.Invoke(null, null);
|
||||
if (keyboard == null) return;
|
||||
|
||||
if (s_InputManagerInstanceField != null && s_InputManagerHasFocusField != null)
|
||||
{
|
||||
object manager = s_InputManagerInstanceField.GetValue(null);
|
||||
if (manager != null)
|
||||
{
|
||||
bool currentFocus = (bool)s_InputManagerHasFocusField.GetValue(manager);
|
||||
if (!currentFocus)
|
||||
s_InputManagerHasFocusField.SetValue(manager, true);
|
||||
}
|
||||
}
|
||||
|
||||
object state = Activator.CreateInstance(s_KeyboardStateType);
|
||||
foreach (string heldKey in s_WinHeldKeys)
|
||||
{
|
||||
object inputKey = BrowserKeyToInputSystemKey(heldKey);
|
||||
if (inputKey != null && s_KeySetMethod != null)
|
||||
s_KeySetMethod.Invoke(state, new object[] { inputKey, true });
|
||||
}
|
||||
|
||||
double eventTime = EditorApplication.timeSinceStartup;
|
||||
s_QueueStateEventMethod.Invoke(null, new object[] { keyboard, state, eventTime });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogVerbose($"[NWB-NewInputSystem] QueueInputSystemKeyboardState error: type={ex.GetType().Name} msg={ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject mouse position and button state into the new Input System via QueueStateEvent.
|
||||
/// Used during streaming (browser mouse events) and by the LegacyMouseBridge (after streaming).
|
||||
/// </summary>
|
||||
private static void QueueInputSystemMouseState(Vector2 screenPos, int browserButton, bool isPressed)
|
||||
{
|
||||
if (!s_InputSystemRefResolved)
|
||||
{
|
||||
if (!TryResolveInputSystemRefs()) return;
|
||||
}
|
||||
|
||||
if (s_MouseGetDeviceMethod == null || s_MouseQueueStateEventMethod == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
object mouse = s_MouseGetDeviceMethod.Invoke(null, null);
|
||||
if (mouse == null) return;
|
||||
|
||||
// Update held button set.
|
||||
if (browserButton >= 0)
|
||||
{
|
||||
if (isPressed)
|
||||
s_InputSystemHeldMouseButtons.Add(browserButton);
|
||||
else
|
||||
s_InputSystemHeldMouseButtons.Remove(browserButton);
|
||||
}
|
||||
|
||||
s_InputSystemMouseScreenPos = screenPos;
|
||||
s_InputSystemMouseScreenPosValid = true;
|
||||
|
||||
if (s_InputManagerInstanceField != null && s_InputManagerHasFocusField != null)
|
||||
{
|
||||
object manager = s_InputManagerInstanceField.GetValue(null);
|
||||
if (manager != null)
|
||||
{
|
||||
bool currentFocus = (bool)s_InputManagerHasFocusField.GetValue(manager);
|
||||
if (!currentFocus)
|
||||
s_InputManagerHasFocusField.SetValue(manager, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Build MouseState with position and button bitmask.
|
||||
// buttons bitmask: bit0=left, bit1=right, bit2=middle
|
||||
// Browser button mapping: 0=left→bit0, 1=middle→bit2, 2=right→bit1
|
||||
ushort buttons = 0;
|
||||
foreach (int btn in s_InputSystemHeldMouseButtons)
|
||||
{
|
||||
switch (btn)
|
||||
{
|
||||
case 0: buttons |= 0x0001; break; // left - > bit 0
|
||||
case 1: buttons |= 0x0004; break; // middle - > bit 2
|
||||
case 2: buttons |= 0x0002; break; // right - > bit 1
|
||||
}
|
||||
}
|
||||
|
||||
object state = Activator.CreateInstance(s_MouseStateType);
|
||||
var posField = s_MouseStateType.GetField("position");
|
||||
posField?.SetValue(state, screenPos);
|
||||
|
||||
var buttonsField = s_MouseStateType.GetField("buttons");
|
||||
buttonsField?.SetValue(state, buttons);
|
||||
|
||||
// Set clickCount=1 on mousedown events to prevent FastMouse.MergeForward
|
||||
// from merging the mousedown (position A) with subsequent mousedrag
|
||||
// events (position B). Without this, the merged event keeps only the
|
||||
// mousedrag's position, so the game's InputReader sets
|
||||
// startingTouchPosition = mousedrag position instead of mousedown position,
|
||||
// making dragLen = 0 and preventing drag detection.
|
||||
// MergeForward skips merging when clickCount differs between events.
|
||||
if (browserButton >= 0 && isPressed)
|
||||
{
|
||||
var clickCountField = s_MouseStateType.GetField("clickCount");
|
||||
clickCountField?.SetValue(state, (ushort)1);
|
||||
}
|
||||
|
||||
double eventTime = EditorApplication.timeSinceStartup;
|
||||
s_MouseQueueStateEventMethod.Invoke(null, new object[] { mouse, state, eventTime });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogInputSystemDiagLimited($"[NWB-NewInputSystem] QueueInputSystemMouseState error: type={ex.GetType().Name} msg={ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all held mouse buttons and queue a neutral MouseState.
|
||||
/// Called from cleanup paths (play mode transition, stop offscreen, etc.)
|
||||
/// </summary>
|
||||
private static void ClearInputSystemMouseState()
|
||||
{
|
||||
if (s_InputSystemHeldMouseButtons.Count == 0) return;
|
||||
s_InputSystemHeldMouseButtons.Clear();
|
||||
Vector2 releasePos = s_InputSystemMouseScreenPosValid ? s_InputSystemMouseScreenPos : Vector2.zero;
|
||||
QueueInputSystemMouseState(releasePos, -1, false);
|
||||
}
|
||||
|
||||
// ── Post-streaming InputSystem recovery via domain reload ──
|
||||
//
|
||||
// Root cause: After streaming stops, PollAbsoluteMousePosition() stops queuing
|
||||
// events into the native event buffer. The native NewInput state (event buffer,
|
||||
// window handle in ScreenCoordinatesToPlayerDisplayCoordinates) is corrupted.
|
||||
//
|
||||
// A domain reload fixes this because:
|
||||
// 1. Native side: RuntimeStatic<InputSystemState> is destroyed and recreated
|
||||
// → InputEventState::Reset() clears buffers
|
||||
// 2. NewInput::Open() is called again with the correct GameView window handle
|
||||
// → RegisterRawInput() re-registers with fresh state
|
||||
// 3. InputSystem package's InputManager re-initializes
|
||||
// → onUpdate, onShouldRunUpdate callbacks re-registered
|
||||
//
|
||||
// Domain reload can only be triggered in Edit mode (not during Play).
|
||||
// If the user stops streaming while in Play mode, we fall back to the
|
||||
// LegacyMouseBridge which polls Input.mousePosition and injects via
|
||||
// QueueStateEvent until the native pipeline recovers.
|
||||
|
||||
// SessionState key to request a domain reload after exiting Play mode.
|
||||
private const string s_PendingReloadAfterPlayKey = "Codely_NWB_PendingReloadAfterPlay";
|
||||
|
||||
/// <summary>
|
||||
/// Called from StopOffscreenCapture to recover the native InputSystem pipeline.
|
||||
/// Triggers a domain reload if in Edit mode, or schedules one for after Play
|
||||
/// mode exits. Falls back to LegacyMouseBridge if domain reload is unavailable.
|
||||
/// </summary>
|
||||
internal static void RecoverInputSystemAfterStreaming()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
// Edit mode: domain reload is safe and clean.
|
||||
// It fully resets native InputSystem state (event buffers, window handles,
|
||||
// raw input registration) and re-initializes the InputSystem package.
|
||||
LogVerbose("[NWB-NewInputSystem] RecoverInputSystemAfterStreaming: requesting domain reload (Edit mode)");
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
EditorUtility.RequestScriptReload();
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Play mode: can't domain reload.
|
||||
LogVerbose("[NWB-NewInputSystem] RecoverInputSystemAfterStreaming: in Play mode, using LegacyMouseBridge fallback");
|
||||
// Schedule a domain reload for when Play mode exits.
|
||||
SessionState.SetBool(s_PendingReloadAfterPlayKey, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a37b20dc39f4ab6afbfd7ae2fbc9bc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1351
File diff suppressed because it is too large.
Load diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 545f82bbdd67e0a40884db8207f8837e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+541
@@ -0,0 +1,541 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
// DockArea.kTabHeight — duplicated here to avoid coupling to Unity internals.
|
||||
private const float kDockTabStripHeight = 19f;
|
||||
// HostView/DockArea Styles.genericMenuTopOffset default (--window-generic-menu-top-offset).
|
||||
private const float kGenericMenuTopOffset = 20f;
|
||||
private const float kPaneOptionsButtonHeight = 18f;
|
||||
private const float kPaneOptionsHitWidth = 52f;
|
||||
|
||||
private static bool s_PaneOptionsMouseDownPending;
|
||||
private static bool s_PaneOptionsReflectionDone;
|
||||
private static Type s_HostViewType;
|
||||
private static MethodInfo s_HostViewAddDefaultItemsToMenu;
|
||||
private static MethodInfo s_HostViewAddWindowActionMenu;
|
||||
private static FieldInfo s_GenericMenuItemsField;
|
||||
private static Type s_GenericMenuItemType;
|
||||
private static FieldInfo s_MenuItemContentField;
|
||||
private static FieldInfo s_MenuItemSeparatorField;
|
||||
private static FieldInfo s_MenuItemOnField;
|
||||
private static FieldInfo s_MenuItemFuncField;
|
||||
private static FieldInfo s_MenuItemFunc2Field;
|
||||
private static FieldInfo s_MenuItemUserDataField;
|
||||
|
||||
private struct CachedPaneOptionsEntry
|
||||
{
|
||||
public Delegate callback;
|
||||
public object userData;
|
||||
public bool enabled;
|
||||
}
|
||||
|
||||
private static CachedPaneOptionsEntry[] s_CachedPaneOptionsEntries;
|
||||
|
||||
private static void EnsurePaneOptionsReflection()
|
||||
{
|
||||
if (s_PaneOptionsReflectionDone) return;
|
||||
s_PaneOptionsReflectionDone = true;
|
||||
|
||||
try
|
||||
{
|
||||
var editorAsm = typeof(EditorWindow).Assembly;
|
||||
s_HostViewType = editorAsm.GetType("UnityEditor.HostView");
|
||||
if (s_HostViewType != null)
|
||||
{
|
||||
s_HostViewAddDefaultItemsToMenu = s_HostViewType.GetMethod(
|
||||
"AddDefaultItemsToMenu",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
s_HostViewAddWindowActionMenu = s_HostViewType.GetMethod(
|
||||
"AddWindowActionMenu",
|
||||
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
}
|
||||
|
||||
s_GenericMenuItemsField = typeof(GenericMenu).GetField(
|
||||
"m_MenuItems", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (s_GenericMenuItemsField != null)
|
||||
{
|
||||
var sample = new GenericMenu();
|
||||
IList list = s_GenericMenuItemsField.GetValue(sample) as IList;
|
||||
if (list != null && list.Count == 0)
|
||||
{
|
||||
sample.AddItem(new GUIContent("x"), false, () => { });
|
||||
list = s_GenericMenuItemsField.GetValue(sample) as IList;
|
||||
}
|
||||
if (list != null && list.Count > 0)
|
||||
s_GenericMenuItemType = list[0].GetType();
|
||||
}
|
||||
|
||||
if (s_GenericMenuItemType != null)
|
||||
{
|
||||
const BindingFlags f = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
s_MenuItemContentField = s_GenericMenuItemType.GetField("content", f);
|
||||
s_MenuItemSeparatorField = s_GenericMenuItemType.GetField("separator", f);
|
||||
s_MenuItemOnField = s_GenericMenuItemType.GetField("on", f);
|
||||
s_MenuItemFuncField = s_GenericMenuItemType.GetField("func", f);
|
||||
s_MenuItemFunc2Field = s_GenericMenuItemType.GetField("func2", f);
|
||||
s_MenuItemUserDataField = s_GenericMenuItemType.GetField("userData", f);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-PaneOptions] Reflection init failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strict DockArea tab-strip hit-test (tab header only).
|
||||
/// Keep this narrow so toolbar controls (e.g. GameView Gizmos) are not
|
||||
/// misclassified as tab-header clicks.
|
||||
/// </summary>
|
||||
private static bool IsInDockTitleBarRegion(float mouseY)
|
||||
{
|
||||
float tabStripH = Mathf.Max(s_TabBarOffsetY, kDockTabStripHeight);
|
||||
return mouseY >= -2f && mouseY <= tabStripH + 2f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PaneOptions (⋮) button vertical band in DockArea header.
|
||||
/// Unity draws it around tabStrip + genericMenuTopOffset.
|
||||
/// </summary>
|
||||
private static bool IsInPaneOptionsVerticalBand(float mouseY)
|
||||
{
|
||||
float tabStripH = Mathf.Max(s_TabBarOffsetY, kDockTabStripHeight);
|
||||
float top = tabStripH + kGenericMenuTopOffset - 2f;
|
||||
float bottom = top + kPaneOptionsButtonHeight + 4f;
|
||||
return mouseY >= top && mouseY <= bottom;
|
||||
}
|
||||
|
||||
private static bool IsPaneOptionsClick(float mouseX, float mouseY, float viewWidth)
|
||||
{
|
||||
// In Play mode, the game area starts below the toolbar (tab strip + toolbar).
|
||||
// The PaneOptions vertical band extends into this region, so game UI buttons
|
||||
// at the top-right (e.g. "Menu") can be mistaken for the ⋮ pane-options button
|
||||
// when the GameView is narrow (composite mode). Skip PaneOptions interception
|
||||
// for clicks that fall in the game play area.
|
||||
bool isGameViewPlayMode = s_OffscreenTargetType != null
|
||||
&& s_OffscreenTargetType.Name == "GameView"
|
||||
&& EditorApplication.isPlaying;
|
||||
if (isGameViewPlayMode && mouseY >= s_TabBarOffsetY + kGameViewToolbarHeight)
|
||||
return false;
|
||||
|
||||
if (!IsInPaneOptionsVerticalBand(mouseY)) return false;
|
||||
if (viewWidth <= kPaneOptionsHitWidth + 4f) return false;
|
||||
return mouseX >= viewWidth - kPaneOptionsHitWidth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity menu shortcuts appear as either "Label _%s" (GenericMenu / GUIContent)
|
||||
/// or "Label %F5" (ShortcutManager SequenceToMenuString, space-separated).
|
||||
/// </summary>
|
||||
private static void SplitUnityMenuText(string text, out string label, out string shortcut)
|
||||
{
|
||||
label = text ?? "";
|
||||
shortcut = "";
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
int sep = text.LastIndexOf(" _", StringComparison.Ordinal);
|
||||
if (sep >= 0)
|
||||
{
|
||||
label = text.Substring(0, sep);
|
||||
shortcut = FormatUnityShortcutDisplay(text.Substring(sep + 2));
|
||||
return;
|
||||
}
|
||||
|
||||
int space = text.LastIndexOf(' ');
|
||||
if (space < 0) return;
|
||||
|
||||
string tail = text.Substring(space + 1);
|
||||
if (!LooksLikeUnityMenuShortcutTail(tail)) return;
|
||||
|
||||
label = text.Substring(0, space);
|
||||
shortcut = FormatUnityShortcutDisplay(tail);
|
||||
}
|
||||
|
||||
private static bool LooksLikeUnityMenuShortcutTail(string tail)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tail) || tail.Length < 2) return false;
|
||||
|
||||
int i = 0;
|
||||
while (i < tail.Length)
|
||||
{
|
||||
char c = tail[i];
|
||||
if (c == '%' || c == '#' || c == '&' || c == '^' || c == '_')
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (i >= tail.Length || i == 0) return false;
|
||||
|
||||
for (int j = i; j < tail.Length; j++)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(tail[j])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatUnityShortcutDisplay(string raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw)) return "";
|
||||
|
||||
if (raw.Length > 1 && raw[0] == '_')
|
||||
raw = raw.Substring(1);
|
||||
|
||||
var sb = new StringBuilder(raw.Length + 8);
|
||||
for (int i = 0; i < raw.Length; i++)
|
||||
{
|
||||
switch (raw[i])
|
||||
{
|
||||
case '%': sb.Append("Ctrl+"); break;
|
||||
case '#': sb.Append("Shift+"); break;
|
||||
case '&': sb.Append("Alt+"); break;
|
||||
case '^': sb.Append("Ctrl+"); break;
|
||||
default: sb.Append(raw[i]); break;
|
||||
}
|
||||
}
|
||||
|
||||
string result = sb.ToString();
|
||||
if (result.EndsWith("+", StringComparison.Ordinal))
|
||||
result = result.Substring(0, result.Length - 1);
|
||||
|
||||
if (result.Length >= 2 && result[0] == 'f' && char.IsDigit(result[1]))
|
||||
result = result.ToUpperInvariant();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AppendPaneOptionsMenuItemJson(
|
||||
StringBuilder sb, int index, string text, string tooltip,
|
||||
bool enabled, bool isOn, bool isSep, ref int validCount)
|
||||
{
|
||||
SplitUnityMenuText(text, out string displayLabel, out string shortcut);
|
||||
|
||||
if (validCount > 0) sb.Append(',');
|
||||
sb.Append("{\"index\":");
|
||||
sb.Append(index);
|
||||
sb.Append(",\"path\":\"");
|
||||
sb.Append(EscapeJsonString(text));
|
||||
sb.Append("\",\"label\":\"");
|
||||
sb.Append(EscapeJsonString(displayLabel));
|
||||
sb.Append("\",\"tooltip\":\"");
|
||||
sb.Append(EscapeJsonString(tooltip));
|
||||
sb.Append("\",\"enabled\":");
|
||||
sb.Append(enabled ? "true" : "false");
|
||||
sb.Append(",\"checked\":");
|
||||
sb.Append(isOn ? "true" : "false");
|
||||
sb.Append(",\"separator\":");
|
||||
sb.Append(isSep ? "true" : "false");
|
||||
if (!string.IsNullOrEmpty(shortcut))
|
||||
{
|
||||
sb.Append(",\"shortcut\":\"");
|
||||
sb.Append(EscapeJsonString(shortcut));
|
||||
sb.Append('"');
|
||||
}
|
||||
sb.Append('}');
|
||||
validCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// mousedown on ⋮: skip SendEvent so SceneView picking / DropdownButton
|
||||
/// hotControl are not activated before mouseup builds the frontend menu.
|
||||
/// </summary>
|
||||
private static bool TryBeginPaneOptionsClick(float mouseX, float mouseY, float viewWidth)
|
||||
{
|
||||
if (s_FrontendPopupSent || s_OffscreenTarget == null) return false;
|
||||
if (!IsPaneOptionsClick(mouseX, mouseY, viewWidth)) return false;
|
||||
|
||||
s_PaneOptionsMouseDownPending = true;
|
||||
if (GUIUtility.hotControl != 0)
|
||||
GUIUtility.hotControl = 0;
|
||||
|
||||
LogVerbose($"[NWB-PaneOptions] mousedown captured at ({mouseX:F0},{mouseY:F0})");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the DockArea PaneOptions menu without GenericMenu.DropDown,
|
||||
/// send popup_show to the browser, cache callbacks for popup_select.
|
||||
/// </summary>
|
||||
private static bool TryInterceptPaneOptionsClick(float mouseX, float mouseY, float viewWidth, float ppp)
|
||||
{
|
||||
if (s_FrontendPopupSent || s_OffscreenTarget == null) return false;
|
||||
|
||||
bool pendingUp = s_PaneOptionsMouseDownPending;
|
||||
if (!pendingUp && !IsPaneOptionsClick(mouseX, mouseY, viewWidth)) return false;
|
||||
|
||||
EnsurePaneOptionsReflection();
|
||||
if (s_GenericMenuItemsField == null || s_GenericMenuItemType == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-PaneOptions] Reflection not ready, cannot intercept");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object dockArea = GetParentView(s_OffscreenTarget);
|
||||
if (dockArea == null) return false;
|
||||
|
||||
GenericMenu menu = BuildPaneOptionsGenericMenu(dockArea, s_OffscreenTarget);
|
||||
if (menu == null) return false;
|
||||
|
||||
IList rawItems = s_GenericMenuItemsField.GetValue(menu) as IList;
|
||||
if (rawItems == null || rawItems.Count == 0) return false;
|
||||
|
||||
float tabStripH = Mathf.Max(s_TabBarOffsetY, kDockTabStripHeight);
|
||||
ComputePopupPositionFromLocalClick(
|
||||
mouseX,
|
||||
mouseY,
|
||||
tabStripH + kGenericMenuTopOffset + 2f,
|
||||
2f,
|
||||
out float popX,
|
||||
out float popY,
|
||||
explicitSourceView: dockArea,
|
||||
explicitPpp: ppp);
|
||||
|
||||
s_FrontendPopupIdCounter++;
|
||||
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
|
||||
s_PopupSourceType = "pane_options";
|
||||
s_CachedPaneOptionsEntries = new CachedPaneOptionsEntry[rawItems.Count];
|
||||
|
||||
var sb = new StringBuilder(2048);
|
||||
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
|
||||
sb.Append(s_CurrentFrontendPopupId);
|
||||
sb.Append("\",\"sourceType\":\"pane_options\",\"x\":");
|
||||
sb.Append(Mathf.RoundToInt(popX));
|
||||
sb.Append(",\"y\":");
|
||||
sb.Append(Mathf.RoundToInt(popY));
|
||||
sb.Append(",\"items\":[");
|
||||
|
||||
int validCount = 0;
|
||||
for (int i = 0; i < rawItems.Count; i++)
|
||||
{
|
||||
object mi = rawItems[i];
|
||||
if (mi == null) continue;
|
||||
|
||||
bool isSep = s_MenuItemSeparatorField != null && (bool)s_MenuItemSeparatorField.GetValue(mi);
|
||||
bool isOn = s_MenuItemOnField != null && (bool)s_MenuItemOnField.GetValue(mi);
|
||||
object content = s_MenuItemContentField?.GetValue(mi);
|
||||
string text = "";
|
||||
string tooltip = "";
|
||||
if (content is GUIContent gc)
|
||||
{
|
||||
text = gc.text ?? "";
|
||||
tooltip = gc.tooltip ?? "";
|
||||
}
|
||||
|
||||
Delegate func = s_MenuItemFuncField?.GetValue(mi) as Delegate;
|
||||
Delegate func2 = s_MenuItemFunc2Field?.GetValue(mi) as Delegate;
|
||||
object userData = s_MenuItemUserDataField?.GetValue(mi);
|
||||
bool enabled = func != null || func2 != null;
|
||||
|
||||
s_CachedPaneOptionsEntries[i] = new CachedPaneOptionsEntry
|
||||
{
|
||||
callback = func2 ?? func,
|
||||
userData = userData,
|
||||
enabled = enabled
|
||||
};
|
||||
|
||||
AppendPaneOptionsMenuItemJson(sb, i, text, tooltip, enabled, isOn, isSep, ref validCount);
|
||||
}
|
||||
|
||||
sb.Append("]}");
|
||||
|
||||
if (validCount == 0) return false;
|
||||
|
||||
if (GUIUtility.hotControl != 0)
|
||||
GUIUtility.hotControl = 0;
|
||||
|
||||
CancelNativeDelayedPopup(forceRemove: true);
|
||||
TryDismissWin32TrackPopupMenu();
|
||||
s_PendingNativePopupCloseFrames = Math.Max(s_PendingNativePopupCloseFrames, 60);
|
||||
s_SuppressTargetRepaintWhilePopupOpen = true;
|
||||
|
||||
bool sent = SendDataChannelMessage(sb.ToString());
|
||||
s_FrontendPopupSent = sent;
|
||||
if (sent)
|
||||
{
|
||||
s_PaneOptionsMouseDownPending = false;
|
||||
CodelyLogger.Log($"[NWB-PaneOptions] Sent frontend menu: id={s_CurrentFrontendPopupId} items={validCount} target={s_OffscreenTargetType?.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-PaneOptions] Failed to send popup_show via DataChannel");
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-PaneOptions] Intercept failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Right-click on tab header: build and send the same DockArea menu
|
||||
/// (Close Tab, Lock, Maximize, etc.) used by the ⋮ pane-options button.
|
||||
/// Called from TrySynthesizeAndForwardContextMenuPopup when the click
|
||||
/// falls in the tab strip region (y <= s_TabBarOffsetY).
|
||||
/// </summary>
|
||||
private static bool TrySendTabHeaderContextMenu(float clickX, float clickY)
|
||||
{
|
||||
EnsurePaneOptionsReflection();
|
||||
if (s_OffscreenTarget == null || s_GenericMenuItemsField == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
object dockArea = GetParentView(s_OffscreenTarget);
|
||||
if (dockArea == null) return false;
|
||||
|
||||
GenericMenu menu = BuildPaneOptionsGenericMenu(dockArea, s_OffscreenTarget);
|
||||
if (menu == null) return false;
|
||||
|
||||
IList rawItems = s_GenericMenuItemsField.GetValue(menu) as IList;
|
||||
if (rawItems == null || rawItems.Count == 0) return false;
|
||||
|
||||
s_FrontendPopupIdCounter++;
|
||||
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
|
||||
s_PopupSourceType = "pane_options";
|
||||
s_CachedPaneOptionsEntries = new CachedPaneOptionsEntry[rawItems.Count];
|
||||
|
||||
var sb = new StringBuilder(2048);
|
||||
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
|
||||
sb.Append(s_CurrentFrontendPopupId);
|
||||
sb.Append("\",\"sourceType\":\"pane_options\",\"x\":");
|
||||
float ppp = EditorGUIUtility.pixelsPerPoint;
|
||||
ComputePopupPositionFromLocalClick(
|
||||
clickX,
|
||||
clickY,
|
||||
clickY + 2f,
|
||||
2f,
|
||||
out float popX,
|
||||
out float popY,
|
||||
explicitSourceView: dockArea,
|
||||
explicitPpp: ppp);
|
||||
|
||||
sb.Append(Mathf.RoundToInt(popX));
|
||||
sb.Append(",\"y\":");
|
||||
sb.Append(Mathf.RoundToInt(popY));
|
||||
sb.Append(",\"items\":[");
|
||||
|
||||
int validCount = 0;
|
||||
for (int i = 0; i < rawItems.Count; i++)
|
||||
{
|
||||
object mi = rawItems[i];
|
||||
if (mi == null) continue;
|
||||
|
||||
bool isSep = s_MenuItemSeparatorField != null && (bool)s_MenuItemSeparatorField.GetValue(mi);
|
||||
bool isOn = s_MenuItemOnField != null && (bool)s_MenuItemOnField.GetValue(mi);
|
||||
object content = s_MenuItemContentField?.GetValue(mi);
|
||||
string text = "";
|
||||
string tooltip = "";
|
||||
if (content is GUIContent gc)
|
||||
{
|
||||
text = gc.text ?? "";
|
||||
tooltip = gc.tooltip ?? "";
|
||||
}
|
||||
|
||||
Delegate func = s_MenuItemFuncField?.GetValue(mi) as Delegate;
|
||||
Delegate func2 = s_MenuItemFunc2Field?.GetValue(mi) as Delegate;
|
||||
object userData = s_MenuItemUserDataField?.GetValue(mi);
|
||||
bool enabled = func != null || func2 != null;
|
||||
|
||||
s_CachedPaneOptionsEntries[i] = new CachedPaneOptionsEntry
|
||||
{
|
||||
callback = func2 ?? func,
|
||||
userData = userData,
|
||||
enabled = enabled
|
||||
};
|
||||
|
||||
AppendPaneOptionsMenuItemJson(sb, i, text, tooltip, enabled, isOn, isSep, ref validCount);
|
||||
}
|
||||
|
||||
sb.Append("]}");
|
||||
if (validCount == 0) return false;
|
||||
|
||||
if (GUIUtility.hotControl != 0)
|
||||
GUIUtility.hotControl = 0;
|
||||
|
||||
CancelNativeDelayedPopup(forceRemove: true);
|
||||
TryDismissWin32TrackPopupMenu();
|
||||
s_PendingNativePopupCloseFrames = Math.Max(s_PendingNativePopupCloseFrames, 60);
|
||||
s_SuppressTargetRepaintWhilePopupOpen = true;
|
||||
|
||||
bool sent = SendDataChannelMessage(sb.ToString());
|
||||
s_FrontendPopupSent = sent;
|
||||
if (sent)
|
||||
CodelyLogger.Log($"[NWB-PaneOptions] Sent tab-header context menu: id={s_CurrentFrontendPopupId} items={validCount} target={s_OffscreenTargetType?.Name}");
|
||||
else
|
||||
CodelyLogger.LogWarning("[NWB-PaneOptions] Failed to send tab-header context menu");
|
||||
|
||||
return sent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-PaneOptions] Tab header context menu failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static GenericMenu BuildPaneOptionsGenericMenu(object dockArea, EditorWindow view)
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
|
||||
if (view is IHasCustomMenu customMenu)
|
||||
customMenu.AddItemsToMenu(menu);
|
||||
|
||||
if (s_HostViewAddDefaultItemsToMenu != null && dockArea != null)
|
||||
s_HostViewAddDefaultItemsToMenu.Invoke(dockArea, new object[] { menu, view });
|
||||
|
||||
if (s_HostViewAddWindowActionMenu != null)
|
||||
s_HostViewAddWindowActionMenu.Invoke(null, new object[] { menu, view });
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private static void HandlePaneOptionsPopupSelect(int index)
|
||||
{
|
||||
if (s_CachedPaneOptionsEntries == null ||
|
||||
index < 0 || index >= s_CachedPaneOptionsEntries.Length)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-PaneOptions] Invalid select index={index}");
|
||||
return;
|
||||
}
|
||||
|
||||
CachedPaneOptionsEntry entry = s_CachedPaneOptionsEntries[index];
|
||||
if (!entry.enabled || entry.callback == null)
|
||||
{
|
||||
LogVerbose($"[NWB-PaneOptions] Select ignored: index={index} enabled={entry.enabled}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (entry.callback is GenericMenu.MenuFunction2 mf2)
|
||||
mf2(entry.userData);
|
||||
else if (entry.callback is GenericMenu.MenuFunction mf)
|
||||
mf();
|
||||
|
||||
LogVerbose($"[NWB-PaneOptions] Invoked callback for index={index}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-PaneOptions] Callback error index={index}: {ex.Message}");
|
||||
}
|
||||
|
||||
s_CachedPaneOptionsEntries = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d8e7d5db7d64f5bab21ee3b8cde1f2d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1291
File diff suppressed because it is too large.
Load diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d85f27e739a9aae48970ceff2bf63921
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
// Popup window interception — auto-dock to composite DockArea.
|
||||
// Detects new EditorWindows created after composite streaming starts
|
||||
// (e.g. via menu items or context-menu "Add Tab") and automatically
|
||||
// docks them as tabs in the existing composite DockArea.
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
// Countdown frames for scanning untracked panes after native "Add Tab".
|
||||
// Set to >0 when a trigger is detected; decremented each frame until 0.
|
||||
// Used by composite pane tracking loops on supported streaming platforms.
|
||||
#pragma warning disable CS0414
|
||||
private static int s_CompositeTrackPanesCountdown;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
// Baseline set of EditorWindow InstanceIDs captured when composite
|
||||
// streaming starts. Windows not in this set are considered "new".
|
||||
private static HashSet<long> s_CompositeKnownWindowIds = new HashSet<long>();
|
||||
|
||||
/// <summary>
|
||||
/// Detect EditorWindows in the composite ContainerWindow that are NOT
|
||||
/// tracked in s_CompositeSlots (e.g. tabs added via native "Add Tab"
|
||||
/// context menu) and create slots for them so the capture loop can
|
||||
/// capture their content. Called each frame from CaptureAndPushCompositeFrame.
|
||||
/// </summary>
|
||||
private static void TrackUntrackedCompositePanes()
|
||||
{
|
||||
if (!s_CompositeActive || s_CompositeSlots.Count == 0) return;
|
||||
|
||||
// Collect tracked InstanceIDs
|
||||
var trackedIds = new HashSet<long>();
|
||||
foreach (var slot in s_CompositeSlots.Values)
|
||||
if (slot?.Window != null)
|
||||
trackedIds.Add(slot.Window.GetStableInstanceId());
|
||||
|
||||
object compositeHostWindow = GetCompositeHostWindow();
|
||||
if (compositeHostWindow == null) return;
|
||||
|
||||
// Get NormalizedRect from first slot (new tab shares the same rect)
|
||||
Rect defaultRect = new Rect(0, 0, 1, 1);
|
||||
foreach (var slot in s_CompositeSlots.Values)
|
||||
{
|
||||
if (slot != null) { defaultRect = slot.NormalizedRect; break; }
|
||||
}
|
||||
|
||||
bool anyAdded = false;
|
||||
foreach (var w in Resources.FindObjectsOfTypeAll<EditorWindow>())
|
||||
{
|
||||
if (w == null || trackedIds.Contains(w.GetStableInstanceId())) continue;
|
||||
|
||||
object parentView = GetParentView(w);
|
||||
if (parentView == null) continue;
|
||||
object windowHost = GetViewWindow(parentView);
|
||||
if (windowHost == null || !ReferenceEquals(windowHost, compositeHostWindow))
|
||||
continue;
|
||||
|
||||
string typeName = w.GetType().FullName ?? w.GetType().Name;
|
||||
if (typeName == "UnityTcp.Editor.Native.StreamingMaskWindow") continue;
|
||||
|
||||
string slotId = "native-" + typeName.Replace(".", "-").ToLowerInvariant()
|
||||
+ "-" + w.GetStableInstanceId();
|
||||
var newSlot = new CompositeCaptureSlot
|
||||
{
|
||||
SlotId = slotId,
|
||||
WindowTypeName = typeName,
|
||||
WindowType = w.GetType(),
|
||||
Window = w,
|
||||
OwnsWindow = false,
|
||||
CloseWindowOnRelease = true,
|
||||
NormalizedRect = defaultRect
|
||||
};
|
||||
#if UNITY_EDITOR_WIN
|
||||
newSlot.ContainerHwnd = Cn.Tuanjie.Codely.Editor.NativeWindowHelper.GetHWND(w);
|
||||
IntPtr reflectedGUIView = Cn.Tuanjie.Codely.Editor.EditorWindowNativeHandleHelper
|
||||
.GetGUIViewHandle(w);
|
||||
if (reflectedGUIView != IntPtr.Zero)
|
||||
newSlot.GUIViewHwnd = reflectedGUIView;
|
||||
// Remove WS_EX_LAYERED to prevent DoPaint transparent mode
|
||||
if (newSlot.ContainerHwnd != IntPtr.Zero)
|
||||
{
|
||||
int style = GetWindowLongW(newSlot.ContainerHwnd, GWL_EXSTYLE);
|
||||
if ((style & WS_EX_LAYERED) != 0)
|
||||
SetWindowLongW(newSlot.ContainerHwnd, GWL_EXSTYLE, style & ~WS_EX_LAYERED);
|
||||
}
|
||||
#endif
|
||||
#if UNITY_EDITOR_OSX
|
||||
foreach (var existSlot in s_CompositeSlots.Values)
|
||||
{
|
||||
if (existSlot?.Window != null && existSlot.NSWindow != IntPtr.Zero)
|
||||
{ newSlot.NSWindow = existSlot.NSWindow; break; }
|
||||
}
|
||||
#endif
|
||||
s_CompositeSlots[slotId] = newSlot;
|
||||
w.autoRepaintOnSceneChange = true;
|
||||
ForceDoubleLayoutRepaint(w);
|
||||
w.Repaint();
|
||||
anyAdded = true;
|
||||
CodelyLogger.Log($"[NWB-Composite] Auto-tracked native tab: {typeName} (id={w.GetStableInstanceId()})");
|
||||
}
|
||||
|
||||
if (anyAdded)
|
||||
EditorApplication.QueuePlayerLoopUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot all current EditorWindow InstanceIDs so new windows created
|
||||
/// AFTER this call can be detected by InterceptAndDockNewPopupWindows.
|
||||
/// Called once when composite streaming first starts.
|
||||
/// </summary>
|
||||
private static void InitCompositeWindowTracking()
|
||||
{
|
||||
s_CompositeKnownWindowIds.Clear();
|
||||
foreach (var w in Resources.FindObjectsOfTypeAll<EditorWindow>())
|
||||
{
|
||||
if (w != null)
|
||||
s_CompositeKnownWindowIds.Add(w.GetStableInstanceId());
|
||||
}
|
||||
CodelyLogger.Log($"[NWB-Composite] InitCompositeWindowTracking: baseline={s_CompositeKnownWindowIds.Count} windows");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EditorWindows that appeared AFTER composite streaming started
|
||||
/// and automatically dock them as tabs in the composite DockArea.
|
||||
/// Called from ManageWindowBridge when the window list changes.
|
||||
/// Returns true if any windows were docked (caller should re-push the
|
||||
/// window list so the /windows endpoint reflects the new docked state).
|
||||
/// The frontend learns about the new tab via editor_windows_changed →
|
||||
/// refreshDynamicWindows, so no separate DataChannel notification is needed.
|
||||
/// </summary>
|
||||
internal static bool InterceptAndDockNewPopupWindows()
|
||||
{
|
||||
if (!s_CompositeActive || s_CompositeSlots.Count == 0)
|
||||
return false;
|
||||
|
||||
var allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
|
||||
var currentIds = new HashSet<long>();
|
||||
var newWindows = new System.Collections.Generic.List<EditorWindow>();
|
||||
|
||||
foreach (var w in allWindows)
|
||||
{
|
||||
if (w == null) continue;
|
||||
long id = w.GetStableInstanceId();
|
||||
currentIds.Add(id);
|
||||
if (!s_CompositeKnownWindowIds.Contains(id))
|
||||
newWindows.Add(w);
|
||||
}
|
||||
|
||||
// Update known set so closed-then-reopened windows are still tracked
|
||||
s_CompositeKnownWindowIds = currentIds;
|
||||
|
||||
if (newWindows.Count == 0) return false;
|
||||
|
||||
// Collect composite slot InstanceIDs for fast lookup
|
||||
var compositeInstanceIds = new HashSet<long>();
|
||||
foreach (var slot in s_CompositeSlots.Values)
|
||||
{
|
||||
if (slot?.Window != null)
|
||||
compositeInstanceIds.Add(slot.Window.GetStableInstanceId());
|
||||
}
|
||||
|
||||
object compositeDockArea = FindExistingCompositeDockArea();
|
||||
if (compositeDockArea == null)
|
||||
{
|
||||
CodelyLogger.Log("[NWB-PopupDock] InterceptPopup: no composite DockArea found, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the composite's ContainerWindow for comparison
|
||||
object compositeHostWindow = GetCompositeHostWindow();
|
||||
bool anyDocked = false;
|
||||
|
||||
foreach (var w in newWindows)
|
||||
{
|
||||
string typeName = w.GetType().FullName ?? w.GetType().Name;
|
||||
string title = w.titleContent?.text ?? "";
|
||||
|
||||
if (compositeInstanceIds.Contains(w.GetStableInstanceId()))
|
||||
{
|
||||
LogVerbose($"[NWB-PopupDock] Skip already-tracked: {typeName} \"{title}\"");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip streaming infrastructure windows
|
||||
if (typeName == "UnityTcp.Editor.Native.StreamingMaskWindow")
|
||||
continue;
|
||||
if (title.EndsWith(" - Streaming", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
// Skip windows that have no parent (not yet fully initialized)
|
||||
object parentView = GetParentView(w);
|
||||
if (parentView == null)
|
||||
{
|
||||
LogVerbose($"[NWB-PopupDock] Skip no-parent: {typeName} \"{title}\"");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip windows already in the composite ContainerWindow
|
||||
if (compositeHostWindow != null)
|
||||
{
|
||||
object windowHost = GetViewWindow(parentView);
|
||||
if (windowHost != null && ReferenceEquals(windowHost, compositeHostWindow))
|
||||
{
|
||||
LogVerbose($"[NWB-PopupDock] Skip already-in-composite: {typeName} \"{title}\"");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Dock this popup into the composite DockArea
|
||||
if (TryDockWindowToCompositeDockArea(w, compositeDockArea, typeName, title))
|
||||
{
|
||||
anyDocked = true;
|
||||
CodelyLogger.Log($"[NWB-PopupDock] Auto-docked popup as tab: {typeName} \"{title}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-PopupDock] Failed to dock: {typeName} \"{title}\"");
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule pane tracking so newly-docked windows get a proper
|
||||
// CompositeCaptureSlot (required for capture after drag-out).
|
||||
if (anyDocked)
|
||||
s_CompositeTrackPanesCountdown = 3;
|
||||
|
||||
return anyDocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move an EditorWindow from its current DockArea into the composite
|
||||
/// DockArea using RemoveTab + AddTab (same proven pattern as TabDrag).
|
||||
/// The old DockArea / ContainerWindow is cleaned up automatically
|
||||
/// when killIfEmpty=true.
|
||||
/// </summary>
|
||||
private static bool TryDockWindowToCompositeDockArea(
|
||||
EditorWindow window, object targetDockArea, string typeName, string title)
|
||||
{
|
||||
if (window == null || targetDockArea == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
EnsureTabDragReflection();
|
||||
|
||||
object sourceDockArea = GetParentView(window);
|
||||
if (sourceDockArea == null || sourceDockArea.GetType().Name != "DockArea")
|
||||
{
|
||||
LogVerbose($"[NWB-Composite] InterceptPopup: {typeName} has no source DockArea, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from old DockArea (killIfEmpty=true closes empty container)
|
||||
if (s_DockRemoveTabMethod != null)
|
||||
{
|
||||
bool killIfEmpty = !ReferenceEquals(sourceDockArea, targetDockArea);
|
||||
s_DockRemoveTabMethod.Invoke(sourceDockArea, new object[] { window, killIfEmpty, false });
|
||||
}
|
||||
else
|
||||
{
|
||||
LogVerbose("[NWB-Composite] InterceptPopup: RemoveTab method unavailable, trying direct AddTab");
|
||||
}
|
||||
|
||||
// Add as tab to composite DockArea
|
||||
if (s_DockAddTabIndexedMethod != null)
|
||||
{
|
||||
int insertIndex = GetDockPaneCount(targetDockArea);
|
||||
s_DockAddTabIndexedMethod.Invoke(targetDockArea, new object[] { insertIndex, window, false });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: use the simpler AddTab(EditorWindow, bool) overload
|
||||
System.Type dockAreaType = targetDockArea.GetType();
|
||||
MethodInfo addTab = dockAreaType.GetMethod("AddTab",
|
||||
BindingFlags.Instance | BindingFlags.Public,
|
||||
null,
|
||||
new System.Type[] { typeof(EditorWindow), typeof(bool) },
|
||||
null);
|
||||
if (addTab == null)
|
||||
{
|
||||
CodelyLogger.LogWarning("[NWB-Composite] InterceptPopup: no AddTab method found");
|
||||
return false;
|
||||
}
|
||||
addTab.Invoke(targetDockArea, new object[] { window, false });
|
||||
}
|
||||
|
||||
// Enable autoRepaintOnSceneChange for proper capture rendering
|
||||
window.autoRepaintOnSceneChange = true;
|
||||
|
||||
// Initialize GPU buffers for capture
|
||||
ForceDoubleLayoutRepaint(window);
|
||||
window.Repaint();
|
||||
EditorApplication.QueuePlayerLoopUpdate();
|
||||
|
||||
// Repaint composite windows to reflect new tab bar
|
||||
foreach (CompositeCaptureSlot slot in s_CompositeSlots.Values)
|
||||
slot?.Window?.Repaint();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning(
|
||||
$"[NWB-Composite] InterceptPopup: failed to dock {typeName}: " +
|
||||
(ex.InnerException?.Message ?? ex.Message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4dae5430152464ea32a160ba046a027
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+2356
File diff suppressed because it is too large.
Load diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8ca3f52a6bb41eda025a75d988e23ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
internal static partial class NativeWindowBridgeHost
|
||||
{
|
||||
private static Vector2 s_LastInputScreenPoint;
|
||||
private static bool s_TabDragReflectionDone;
|
||||
private static Type s_DockAreaType;
|
||||
private static FieldInfo s_DockIsDraggingField;
|
||||
private static FieldInfo s_DockDragPaneField;
|
||||
private static FieldInfo s_DockDropInfoField;
|
||||
private static FieldInfo s_DockOriginalDragSourceField;
|
||||
private static FieldInfo s_DockPanesField;
|
||||
private static MethodInfo s_DockResetDragVarsMethod;
|
||||
private static MethodInfo s_DockRemoveTabMethod;
|
||||
private static MethodInfo s_DockAddTabIndexedMethod;
|
||||
private static PropertyInfo s_DockSelectedProperty;
|
||||
private static Type s_DropInfoType;
|
||||
private static FieldInfo s_DropInfoDropAreaField;
|
||||
private static Type s_PaneDragTabType;
|
||||
private static PropertyInfo s_PaneDragTabGetProperty;
|
||||
private static MethodInfo s_PaneDragTabCloseMethod;
|
||||
|
||||
private static void EnsureTabDragReflection()
|
||||
{
|
||||
if (s_TabDragReflectionDone) return;
|
||||
s_TabDragReflectionDone = true;
|
||||
|
||||
try
|
||||
{
|
||||
var editorAsm = typeof(EditorWindow).Assembly;
|
||||
s_DockAreaType = editorAsm.GetType("UnityEditor.DockArea");
|
||||
s_DropInfoType = editorAsm.GetType("UnityEditor.DropInfo");
|
||||
s_PaneDragTabType = editorAsm.GetType("UnityEditor.PaneDragTab");
|
||||
|
||||
if (s_DockAreaType != null)
|
||||
{
|
||||
const BindingFlags f = BindingFlags.Static | BindingFlags.NonPublic;
|
||||
s_DockIsDraggingField = s_DockAreaType.GetField("s_IsDragging", f);
|
||||
s_DockDragPaneField = s_DockAreaType.GetField("s_DragPane", f);
|
||||
s_DockDropInfoField = s_DockAreaType.GetField("s_DropInfo", f);
|
||||
s_DockOriginalDragSourceField = s_DockAreaType.GetField("s_OriginalDragSource", f);
|
||||
s_DockPanesField = s_DockAreaType.GetField("m_Panes", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
s_DockResetDragVarsMethod = s_DockAreaType.GetMethod("ResetDragVars", f);
|
||||
s_DockRemoveTabMethod = s_DockAreaType.GetMethod(
|
||||
"RemoveTab",
|
||||
BindingFlags.Instance | BindingFlags.Public,
|
||||
null,
|
||||
new[] { typeof(EditorWindow), typeof(bool), typeof(bool) },
|
||||
null);
|
||||
s_DockAddTabIndexedMethod = s_DockAreaType.GetMethod(
|
||||
"AddTab",
|
||||
BindingFlags.Instance | BindingFlags.Public,
|
||||
null,
|
||||
new[] { typeof(int), typeof(EditorWindow), typeof(bool) },
|
||||
null);
|
||||
s_DockSelectedProperty = s_DockAreaType.GetProperty(
|
||||
"selected", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (s_DropInfoType != null)
|
||||
s_DropInfoDropAreaField = s_DropInfoType.GetField("dropArea", BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
if (s_PaneDragTabType != null)
|
||||
{
|
||||
s_PaneDragTabGetProperty = s_PaneDragTabType.GetProperty(
|
||||
"get", BindingFlags.Static | BindingFlags.Public);
|
||||
s_PaneDragTabCloseMethod = s_PaneDragTabType.GetMethod(
|
||||
"Close", BindingFlags.Instance | BindingFlags.Public);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-TabDrag] Reflection init failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateLastInputScreenPoint(float rawX, float rawY, float ppp, Vector2 localMousePos)
|
||||
{
|
||||
if (s_CompositeActive)
|
||||
{
|
||||
s_LastInputScreenPoint = new Vector2(
|
||||
s_CompositeFrameBoundsLogical.xMin +
|
||||
(rawX - s_CompositeFrameOffsetPixels.x) /
|
||||
Mathf.Max(s_CompositeFrameScalePixels.x * ppp, 0.001f),
|
||||
s_CompositeFrameBoundsLogical.yMin +
|
||||
(rawY - s_CompositeFrameOffsetPixels.y) /
|
||||
Mathf.Max(s_CompositeFrameScalePixels.y * ppp, 0.001f));
|
||||
return;
|
||||
}
|
||||
|
||||
object parentView = s_OffscreenTarget != null ? GetParentView(s_OffscreenTarget) : null;
|
||||
if (parentView != null)
|
||||
{
|
||||
Rect viewRect = GetViewScreenPosition(parentView);
|
||||
s_LastInputScreenPoint = new Vector2(viewRect.xMin + localMousePos.x, viewRect.yMin + localMousePos.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
s_LastInputScreenPoint = localMousePos;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsStreamingTabDragPendingFloatingWindow()
|
||||
{
|
||||
if (!s_CompositeActive && !s_OffscreenActive) return false;
|
||||
|
||||
EnsureTabDragReflection();
|
||||
if (s_DockIsDraggingField == null || !(bool)s_DockIsDraggingField.GetValue(null))
|
||||
return false;
|
||||
|
||||
object dropInfo = s_DockDropInfoField?.GetValue(null);
|
||||
if (dropInfo != null && s_DropInfoDropAreaField != null)
|
||||
{
|
||||
object dropArea = s_DropInfoDropAreaField.GetValue(dropInfo);
|
||||
if (dropArea != null)
|
||||
return false;
|
||||
}
|
||||
|
||||
return s_DockDragPaneField?.GetValue(null) is EditorWindow;
|
||||
}
|
||||
|
||||
private static object FindDockAreaAtScreenPoint(Vector2 screenPoint)
|
||||
{
|
||||
object bestDock = null;
|
||||
float bestArea = float.MaxValue;
|
||||
|
||||
if (s_CompositeActive)
|
||||
{
|
||||
foreach (CompositeCaptureSlot slot in s_CompositeSlots.Values)
|
||||
{
|
||||
if (slot?.Window == null) continue;
|
||||
object dock = GetParentView(slot.Window);
|
||||
if (dock == null || dock.GetType().Name != "DockArea") continue;
|
||||
|
||||
Rect rect = GetViewScreenPosition(dock);
|
||||
if (!rect.Contains(screenPoint)) continue;
|
||||
|
||||
float area = rect.width * rect.height;
|
||||
if (area < bestArea)
|
||||
{
|
||||
bestArea = area;
|
||||
bestDock = dock;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDock != null) return bestDock;
|
||||
return FindExistingCompositeDockArea();
|
||||
}
|
||||
|
||||
if (s_OffscreenTarget != null)
|
||||
{
|
||||
object dock = GetParentView(s_OffscreenTarget);
|
||||
if (dock != null && dock.GetType().Name == "DockArea")
|
||||
return dock;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int GetDockPaneCount(object dockArea)
|
||||
{
|
||||
if (dockArea == null || s_DockPanesField == null) return 0;
|
||||
IList panes = s_DockPanesField.GetValue(dockArea) as IList;
|
||||
return panes?.Count ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a tab is released over the streaming canvas center, Unity would
|
||||
/// create a new floating ContainerWindow (DropInfo.dropArea == null).
|
||||
/// Re-route that drop as AddTab on the DockArea under the cursor instead.
|
||||
/// Edge/pane drops (dropArea set) still go through SendEvent normally.
|
||||
/// </summary>
|
||||
private static bool TryCompleteStreamingTabDragAsAddTab(Vector2 screenMousePos)
|
||||
{
|
||||
if (!IsStreamingTabDragPendingFloatingWindow()) return false;
|
||||
|
||||
EnsureTabDragReflection();
|
||||
if (s_DockRemoveTabMethod == null || s_DockAddTabIndexedMethod == null)
|
||||
return false;
|
||||
|
||||
var dragPane = s_DockDragPaneField?.GetValue(null) as EditorWindow;
|
||||
var originalSource = s_DockOriginalDragSourceField?.GetValue(null);
|
||||
if (dragPane == null || originalSource == null) return false;
|
||||
|
||||
object targetDock = FindDockAreaAtScreenPoint(screenMousePos);
|
||||
if (targetDock == null)
|
||||
{
|
||||
LogVerbose("[NWB-TabDrag] No target DockArea for AddTab fallback");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bool killIfEmpty = !ReferenceEquals(originalSource, targetDock);
|
||||
s_DockRemoveTabMethod.Invoke(originalSource, new object[] { dragPane, killIfEmpty, false });
|
||||
|
||||
int insertIndex = GetDockPaneCount(targetDock);
|
||||
s_DockAddTabIndexedMethod.Invoke(targetDock, new object[] { insertIndex, dragPane, false });
|
||||
|
||||
if (s_DockSelectedProperty != null && s_DockSelectedProperty.CanWrite)
|
||||
s_DockSelectedProperty.SetValue(targetDock, insertIndex);
|
||||
|
||||
CloseActivePaneDragTab();
|
||||
s_DockResetDragVarsMethod?.Invoke(null, null);
|
||||
if (GUIUtility.hotControl != 0)
|
||||
GUIUtility.hotControl = 0;
|
||||
|
||||
dragPane.Repaint();
|
||||
if (targetDock is ScriptableObject dockSo)
|
||||
{
|
||||
MethodInfo repaint = dockSo.GetType().GetMethod(
|
||||
"Repaint", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
null, Type.EmptyTypes, null);
|
||||
repaint?.Invoke(dockSo, null);
|
||||
}
|
||||
|
||||
if (s_CompositeActive)
|
||||
{
|
||||
foreach (CompositeCaptureSlot slot in s_CompositeSlots.Values)
|
||||
slot?.Window?.Repaint();
|
||||
}
|
||||
else
|
||||
{
|
||||
s_OffscreenTarget?.Repaint();
|
||||
}
|
||||
|
||||
CodelyLogger.Log(
|
||||
$"[NWB-TabDrag] AddTab fallback: {dragPane.GetType().Name} -> DockArea at ({screenMousePos.x:F0},{screenMousePos.y:F0})");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[NWB-TabDrag] AddTab fallback failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseActivePaneDragTab()
|
||||
{
|
||||
if (s_PaneDragTabGetProperty == null || s_PaneDragTabCloseMethod == null) return;
|
||||
object paneDrag = s_PaneDragTabGetProperty.GetValue(null, null);
|
||||
if (paneDrag != null)
|
||||
s_PaneDragTabCloseMethod.Invoke(paneDrag, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 671d1e5f6ca4427ca5de143bfb9a7e2c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+14670
File diff suppressed because it is too large.
Load diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37098c3bc4e96b248976396cda60381b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
using UnityTcp.Editor.Helpers;
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// A utility EditorWindow shown during offscreen streaming.
|
||||
/// Displays a message that the editor is hidden
|
||||
/// and provides a button to disconnect and restore the editor.
|
||||
///
|
||||
/// The native window handle is excluded from hide operations in
|
||||
/// NativeWindowBridgeHost, so it remains visible while other Unity
|
||||
/// windows are hidden.
|
||||
///
|
||||
/// Focus() calls triggered by popup creation target specific Unity
|
||||
/// ContainerWindows (not all windows), so this mask window is NOT
|
||||
/// brought to front by those calls.
|
||||
/// </summary>
|
||||
internal class StreamingMaskWindow : EditorWindow
|
||||
{
|
||||
private static StreamingMaskWindow s_Instance;
|
||||
private static bool s_ClosingFromCode;
|
||||
|
||||
private GUIStyle _titleStyle;
|
||||
private GUIStyle _messageStyle;
|
||||
private GUIStyle _buttonStyle;
|
||||
private bool _stylesInitialized;
|
||||
|
||||
private const float kWindowWidth = 380f;
|
||||
private const float kWindowHeight = 170f;
|
||||
|
||||
#if TUANJIE_1_OR_NEWER || TUANJIE_1
|
||||
private const string kEditorName = "Tuanjie Editor";
|
||||
#else
|
||||
private const string kEditorName = "Unity Editor";
|
||||
#endif
|
||||
|
||||
internal static string WindowTitle => kEditorName + " - Streaming";
|
||||
|
||||
internal static StreamingMaskWindow TryGetInstance() => s_Instance;
|
||||
|
||||
internal static StreamingMaskWindow ShowMask()
|
||||
{
|
||||
if (s_Instance != null)
|
||||
{
|
||||
ApplyBottomRightPosition(s_Instance);
|
||||
s_Instance.ShowUtility();
|
||||
CodelyLogger.Log("[StreamingMask] Mask window re-shown");
|
||||
return s_Instance;
|
||||
}
|
||||
|
||||
// After domain reload, s_Instance is null but a serialized
|
||||
// EditorWindow instance may still exist. Reuse it — Close()+create
|
||||
// races OnDestroy (Unity Close is often deferred) and the dying
|
||||
// window would clear the new s_Instance after a few reconnects.
|
||||
var existing = Resources.FindObjectsOfTypeAll<StreamingMaskWindow>();
|
||||
StreamingMaskWindow first = null;
|
||||
foreach (var w in existing)
|
||||
{
|
||||
if (w == null) continue;
|
||||
if (first == null)
|
||||
{
|
||||
first = w;
|
||||
continue;
|
||||
}
|
||||
// Extra domain-reload leftovers. Close is deferred, but
|
||||
// OnDestroy only clears s_Instance when s_Instance == this.
|
||||
try { w.Close(); }
|
||||
catch (System.Exception) { }
|
||||
}
|
||||
if (first != null)
|
||||
{
|
||||
s_Instance = first;
|
||||
first.titleContent = new GUIContent(WindowTitle);
|
||||
first.minSize = new Vector2(kWindowWidth, kWindowHeight);
|
||||
first.maxSize = new Vector2(kWindowWidth, kWindowHeight);
|
||||
ApplyBottomRightPosition(first);
|
||||
first.ShowUtility();
|
||||
CodelyLogger.Log("[StreamingMask] Mask window reused after domain reload");
|
||||
return first;
|
||||
}
|
||||
|
||||
var win = CreateInstance<StreamingMaskWindow>();
|
||||
win.titleContent = new GUIContent(WindowTitle);
|
||||
win.minSize = new Vector2(kWindowWidth, kWindowHeight);
|
||||
win.maxSize = new Vector2(kWindowWidth, kWindowHeight);
|
||||
win.ShowUtility();
|
||||
ApplyBottomRightPosition(win);
|
||||
|
||||
s_Instance = win;
|
||||
CodelyLogger.Log("[StreamingMask] Mask window shown");
|
||||
return win;
|
||||
}
|
||||
|
||||
internal static void ApplyBottomRightPosition(EditorWindow win)
|
||||
{
|
||||
if (win == null) return;
|
||||
// Bottom-right of the primary screen, just above the taskbar.
|
||||
// Screen.currentResolution is physical pixels; EditorWindow.position is logical.
|
||||
const float kTaskbarHeight = 48f;
|
||||
const float kBottomMargin = 8f;
|
||||
float dpiScale = EditorGUIUtility.pixelsPerPoint;
|
||||
float logicalScreenW = Screen.currentResolution.width / dpiScale;
|
||||
float logicalScreenH = Screen.currentResolution.height / dpiScale;
|
||||
win.position = new Rect(
|
||||
logicalScreenW - kWindowWidth,
|
||||
logicalScreenH - kTaskbarHeight - kWindowHeight - kBottomMargin,
|
||||
kWindowWidth,
|
||||
kWindowHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Programmatically close the mask window (called from StopOffscreenCapture).
|
||||
/// Sets a guard flag to prevent OnDestroy from triggering a recursive stop.
|
||||
/// </summary>
|
||||
internal static void HideMask()
|
||||
{
|
||||
s_ClosingFromCode = true;
|
||||
try
|
||||
{
|
||||
if (s_Instance != null)
|
||||
{
|
||||
s_Instance.Close();
|
||||
}
|
||||
|
||||
// Domain reload can clear s_Instance while the serialized
|
||||
// EditorWindow object still exists. Close any such zombies.
|
||||
foreach (var w in Resources.FindObjectsOfTypeAll<StreamingMaskWindow>())
|
||||
{
|
||||
if (w != null && w != s_Instance)
|
||||
{
|
||||
w.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
CodelyLogger.LogWarning($"[StreamingMask] HideMask close failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
s_ClosingFromCode = false;
|
||||
s_Instance = null;
|
||||
}
|
||||
CodelyLogger.Log("[StreamingMask] Mask window hidden");
|
||||
}
|
||||
|
||||
internal static bool IsActive => s_Instance != null;
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
InitStyles();
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
EditorGUILayout.LabelField(kEditorName, _titleStyle);
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.LabelField(
|
||||
"Editor is hidden while streaming via Codely Cowork.",
|
||||
_messageStyle);
|
||||
|
||||
EditorGUILayout.Space(16);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button(
|
||||
"Disconnect & Restore",
|
||||
_buttonStyle,
|
||||
GUILayout.Width(200),
|
||||
GUILayout.Height(32)))
|
||||
{
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
NativeWindowBridgeHost.StopOffscreenCapture();
|
||||
};
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Close() is often deferred. A dying leftover instance must not
|
||||
// clear a newer s_Instance or stop an active stream.
|
||||
if (s_Instance != this)
|
||||
return;
|
||||
|
||||
s_Instance = null;
|
||||
if (s_ClosingFromCode)
|
||||
return;
|
||||
|
||||
// User closed the mask window manually (e.g. clicked X).
|
||||
// Treat this as a disconnect request.
|
||||
if (NativeWindowBridgeHost.IsOffscreenActive)
|
||||
{
|
||||
CodelyLogger.Log("[StreamingMask] User closed mask window, stopping offscreen capture");
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
NativeWindowBridgeHost.StopOffscreenCapture();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void InitStyles()
|
||||
{
|
||||
if (_stylesInitialized) return;
|
||||
_stylesInitialized = true;
|
||||
|
||||
_titleStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
fontSize = 18,
|
||||
alignment = TextAnchor.MiddleCenter
|
||||
};
|
||||
|
||||
_messageStyle = new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
fontSize = 12,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
wordWrap = true,
|
||||
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
|
||||
};
|
||||
|
||||
_buttonStyle = new GUIStyle(GUI.skin.button)
|
||||
{
|
||||
fontSize = 13,
|
||||
fontStyle = FontStyle.Bold,
|
||||
padding = new RectOffset(16, 16, 6, 6)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityTcp.Editor.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// No-op placeholder for non-Windows/macOS editor platforms.
|
||||
/// This keeps cross-platform compilation valid when shared code
|
||||
/// references StreamingMaskWindow in common paths.
|
||||
/// </summary>
|
||||
internal class StreamingMaskWindow : EditorWindow
|
||||
{
|
||||
internal static string WindowTitle => "Editor - Streaming";
|
||||
|
||||
internal static StreamingMaskWindow TryGetInstance() => null;
|
||||
|
||||
internal static StreamingMaskWindow ShowMask()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static void ApplyBottomRightPosition(EditorWindow win)
|
||||
{
|
||||
}
|
||||
|
||||
internal static void HideMask()
|
||||
{
|
||||
// Intentionally empty on unsupported platforms.
|
||||
}
|
||||
|
||||
internal static bool IsActive
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d48a4e6a1b3c7d5e9f0a2b4c6d8e0f12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user