using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Codely.Newtonsoft.Json.Linq; namespace UnityTcp.Editor.Helpers { /// One clickable button on a native modal dialog. public sealed class ModalButtonInfo { /// Platform handle: HWND on Windows, NSButton pointer on macOS. public IntPtr Handle; /// Win32 dialog control id; 0 on macOS. public int ControlId; /// Normalized label (accelerator '&' stripped, trimmed). public string Label; } /// A native modal dialog currently open in this editor process. public sealed class ModalDialogInfo { /// Platform handle: HWND on Windows, NSWindow pointer on macOS. public IntPtr Handle; public string Title; public string Message; public List Buttons = new List(); } /// /// Platform facade over the native modal dialog scanners /// ( / ). /// /// Threading contract differs by platform and callers must respect it: /// - Windows: Scan/ClickButton are callable from any thread (that is the whole point — /// the background watcher reaches into the blocked main thread's dialog). /// - macOS: AppKit is main-thread-only, so Scan/ClickButton must run on the main thread. /// While a dialog is up that is only possible from the common-modes run-loop timer /// (see MacModalDialogScanner.StartMainThreadProbe), which DialogWatcher uses as /// its probe there instead of a background thread. /// public static class ModalDialogScanner { /// /// Strips the Win32 accelerator marker, trims, and collapses runs of whitespace: /// "&Yes " → "Yes", "Enable & Restart" → "Enable Restart". Whitespace collapsing /// matters because removing an '&' between words would otherwise leave a double /// space that button regexes would have to know about. /// public static string NormalizeButtonLabel(string label) { string stripped = (label ?? string.Empty).Replace("&", string.Empty).Trim(); return Regex.Replace(stripped, @"\s{2,}", " "); } public static JObject Describe(ModalDialogInfo dialog) => new JObject { ["title"] = dialog.Title, ["message"] = dialog.Message, ["buttons"] = new JArray(dialog.Buttons.Select(b => b.Label)), ["handle"] = dialog.Handle.ToInt64(), }; public static JArray Describe(IEnumerable dialogs) => new JArray(dialogs.Select(Describe)); public static bool IsSupported => #if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX true; #else false; #endif /// /// Records the calling thread as the editor main thread, where the platform scanner /// needs it (Windows uses it to reject dialogs pumping on other threads — those do not /// block the main thread). Call once from the main thread before any background scan. /// public static void CaptureMainThread() { #if UNITY_EDITOR_WIN Win32ModalDialogScanner.CaptureMainThread(); #endif } public static List Scan() { #if UNITY_EDITOR_WIN return Win32ModalDialogScanner.Scan(); #elif UNITY_EDITOR_OSX return MacModalDialogScanner.Scan(); #else return new List(); #endif } public static bool ClickButton(ModalDialogInfo dialog, ModalButtonInfo button) { #if UNITY_EDITOR_WIN return Win32ModalDialogScanner.ClickButton(dialog, button); #elif UNITY_EDITOR_OSX return MacModalDialogScanner.ClickButton(dialog, button); #else return false; #endif } } }