#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Native
{
///
/// 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.
///
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 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();
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);
}
///
/// Programmatically close the mask window (called from StopOffscreenCapture).
/// Sets a guard flag to prevent OnDestroy from triggering a recursive stop.
///
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())
{
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
{
///
/// No-op placeholder for non-Windows/macOS editor platforms.
/// This keeps cross-platform compilation valid when shared code
/// references StreamingMaskWindow in common paths.
///
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