From 9c0c57db2fd3e15ddc34f5287ebecdadd040d3c4 Mon Sep 17 00:00:00 2001 From: dianna Date: Wed, 13 May 2026 21:39:24 -0700 Subject: [PATCH 1/3] Add Commands Window for running ADB and shell commands --- .../Editor/AndroidLogcatAdbCommandCatalog.cs | 148 +++++ .../AndroidLogcatAdbCommandCatalog.cs.meta | 2 + .../Editor/AndroidLogcatAddCommandDialog.cs | 71 +++ .../AndroidLogcatAddCommandDialog.cs.meta | 2 + .../Editor/AndroidLogcatCommandEntry.cs | 54 ++ .../Editor/AndroidLogcatCommandEntry.cs.meta | 2 + .../AndroidLogcatCommandOutputWindow.cs | 106 ++++ .../AndroidLogcatCommandOutputWindow.cs.meta | 2 + .../AndroidLogcatCommandSearchWindow.cs | 159 ++++++ .../AndroidLogcatCommandSearchWindow.cs.meta | 2 + .../Editor/AndroidLogcatCommandsWindow.cs | 523 ++++++++++++++++++ .../AndroidLogcatCommandsWindow.cs.meta | 2 + .../Editor/AndroidLogcatConsoleWindow.cs | 4 + .../Editor/AndroidLogcatContextMenu.cs | 1 + .../Editor/AndroidLogcatPlaceholderDialog.cs | 93 ++++ .../AndroidLogcatPlaceholderDialog.cs.meta | 2 + .../Editor/AndroidLogcatUserSettings.cs | 14 + 17 files changed, 1187 insertions(+) create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs.meta create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs.meta create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs.meta create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs.meta create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs.meta create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs.meta create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs create mode 100644 com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs.meta diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs new file mode 100644 index 00000000..f9d36505 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs @@ -0,0 +1,148 @@ +namespace Unity.Android.Logcat +{ + internal static class AndroidLogcatAdbCommandCatalog + { + internal static readonly AndroidLogcatCommandEntry[] All = new[] + { + // --- Device Management --- + new AndroidLogcatCommandEntry("List Devices", "adb devices"), + new AndroidLogcatCommandEntry("List Devices (Verbose)", "adb devices -l"), + new AndroidLogcatCommandEntry("Start Server", "adb start-server"), + new AndroidLogcatCommandEntry("Kill Server", "adb kill-server"), + new AndroidLogcatCommandEntry("Reboot Device", "adb reboot"), + new AndroidLogcatCommandEntry("Reboot to Bootloader", "adb reboot bootloader"), + new AndroidLogcatCommandEntry("Reboot to Recovery", "adb reboot recovery"), + new AndroidLogcatCommandEntry("Get Device State", "adb get-state"), + new AndroidLogcatCommandEntry("Get Serial Number", "adb get-serialno"), + new AndroidLogcatCommandEntry("Wait for Device", "adb wait-for-device"), + + // --- App Management --- + new AndroidLogcatCommandEntry("List All Packages", "adb shell pm list packages"), + new AndroidLogcatCommandEntry("List Third-Party Packages", "adb shell pm list packages -3"), + new AndroidLogcatCommandEntry("List System Packages", "adb shell pm list packages -s"), + new AndroidLogcatCommandEntry("Clear App Data", "adb shell pm clear "), + new AndroidLogcatCommandEntry("Force Stop App", "adb shell am force-stop "), + new AndroidLogcatCommandEntry("Start Activity", "adb shell am start -n "), + new AndroidLogcatCommandEntry("Start App (Launcher)", "adb shell monkey -p -c android.intent.category.LAUNCHER 1"), + new AndroidLogcatCommandEntry("Start App (UnityPlayerActivity)", "adb shell am start -n /com.unity3d.player.UnityPlayerActivity"), + new AndroidLogcatCommandEntry("Start App (GameActivity)", "adb shell am start -n /com.google.androidgamesdk.GameActivity"), + new AndroidLogcatCommandEntry("Uninstall App", "adb uninstall "), + new AndroidLogcatCommandEntry("Install APK", "adb install "), + new AndroidLogcatCommandEntry("Install APK (Replace)", "adb install -r "), + new AndroidLogcatCommandEntry("Install Multiple APKs (Split)", "adb install-multiple -r "), + new AndroidLogcatCommandEntry("Push OBB File", "adb push /sdcard/Android/obb//"), + new AndroidLogcatCommandEntry("Create OBB Directory", "adb shell mkdir -p /sdcard/Android/obb/"), + new AndroidLogcatCommandEntry("Grant Permission", "adb shell pm grant "), + new AndroidLogcatCommandEntry("Revoke Permission", "adb shell pm revoke "), + new AndroidLogcatCommandEntry("Dump App Info", "adb shell dumpsys package "), + + // --- File Transfer --- + new AndroidLogcatCommandEntry("Push File to Device", "adb push "), + new AndroidLogcatCommandEntry("Pull File from Device", "adb pull "), + new AndroidLogcatCommandEntry("List Directory", "adb shell ls -la "), + new AndroidLogcatCommandEntry("Remove File", "adb shell rm "), + new AndroidLogcatCommandEntry("Make Directory", "adb shell mkdir -p "), + + // --- Logcat --- + new AndroidLogcatCommandEntry("View Logcat", "adb logcat"), + new AndroidLogcatCommandEntry("Clear Logcat", "adb logcat -c"), + new AndroidLogcatCommandEntry("Dump Logcat & Exit", "adb logcat -d"), + new AndroidLogcatCommandEntry("Logcat Errors Only", "adb logcat *:E"), + new AndroidLogcatCommandEntry("Logcat Warnings & Above", "adb logcat *:W"), + new AndroidLogcatCommandEntry("Logcat with Timestamps", "adb logcat -v time"), + new AndroidLogcatCommandEntry("Logcat by Tag", "adb logcat -s "), + new AndroidLogcatCommandEntry("Logcat Unity Only", "adb logcat -s Unity"), + + // --- Screen Capture --- + new AndroidLogcatCommandEntry("Take Screenshot", "adb shell screencap /sdcard/screenshot.png"), + new AndroidLogcatCommandEntry("Record Screen", "adb shell screenrecord /sdcard/recording.mp4"), + new AndroidLogcatCommandEntry("Record Screen (30s)", "adb shell screenrecord --time-limit 30 /sdcard/recording.mp4"), + + // --- System Info --- + new AndroidLogcatCommandEntry("Get Android Version", "adb shell getprop ro.build.version.release"), + new AndroidLogcatCommandEntry("Get SDK Version", "adb shell getprop ro.build.version.sdk"), + new AndroidLogcatCommandEntry("Get Device Model", "adb shell getprop ro.product.model"), + new AndroidLogcatCommandEntry("Get Device Manufacturer", "adb shell getprop ro.product.manufacturer"), + new AndroidLogcatCommandEntry("Get All Properties", "adb shell getprop"), + new AndroidLogcatCommandEntry("Get Screen Resolution", "adb shell wm size"), + new AndroidLogcatCommandEntry("Get Screen Density", "adb shell wm density"), + new AndroidLogcatCommandEntry("Get Battery Info", "adb shell dumpsys battery"), + new AndroidLogcatCommandEntry("Get CPU Info", "adb shell cat /proc/cpuinfo"), + new AndroidLogcatCommandEntry("Get Memory Info", "adb shell cat /proc/meminfo"), + new AndroidLogcatCommandEntry("Get Disk Usage", "adb shell df"), + new AndroidLogcatCommandEntry("Get Running Processes", "adb shell ps"), + new AndroidLogcatCommandEntry("Get Top Processes", "adb shell top -n 1"), + new AndroidLogcatCommandEntry("Get Build Properties", "adb shell cat /system/build.prop"), + + // --- Input Simulation --- + new AndroidLogcatCommandEntry("Send Keyevent (Home)", "adb shell input keyevent KEYCODE_HOME"), + new AndroidLogcatCommandEntry("Send Keyevent (Back)", "adb shell input keyevent KEYCODE_BACK"), + new AndroidLogcatCommandEntry("Send Keyevent (Menu)", "adb shell input keyevent KEYCODE_MENU"), + new AndroidLogcatCommandEntry("Send Keyevent (Power)", "adb shell input keyevent KEYCODE_POWER"), + new AndroidLogcatCommandEntry("Send Keyevent (Volume Up)", "adb shell input keyevent KEYCODE_VOLUME_UP"), + new AndroidLogcatCommandEntry("Send Keyevent (Volume Down)", "adb shell input keyevent KEYCODE_VOLUME_DOWN"), + new AndroidLogcatCommandEntry("Send Keyevent (Enter)", "adb shell input keyevent KEYCODE_ENTER"), + new AndroidLogcatCommandEntry("Send Keyevent (Tab)", "adb shell input keyevent KEYCODE_TAB"), + new AndroidLogcatCommandEntry("Send Text", "adb shell input text "), + new AndroidLogcatCommandEntry("Tap Screen", "adb shell input tap "), + new AndroidLogcatCommandEntry("Swipe Screen", "adb shell input swipe "), + + // --- Networking --- + new AndroidLogcatCommandEntry("Enable WiFi ADB", "adb tcpip 5555"), + new AndroidLogcatCommandEntry("Connect via IP", "adb connect :5555"), + new AndroidLogcatCommandEntry("Disconnect All", "adb disconnect"), + new AndroidLogcatCommandEntry("Forward Port", "adb forward tcp: tcp:"), + new AndroidLogcatCommandEntry("Reverse Port", "adb reverse tcp: tcp:"), + new AndroidLogcatCommandEntry("Get IP Address", "adb shell ip addr show wlan0"), + new AndroidLogcatCommandEntry("Ping Host", "adb shell ping -c 4 "), + + // --- Dumpsys --- + new AndroidLogcatCommandEntry("Dump Activity Stack", "adb shell dumpsys activity activities"), + new AndroidLogcatCommandEntry("Dump Memory Info", "adb shell dumpsys meminfo"), + new AndroidLogcatCommandEntry("Dump Window Info", "adb shell dumpsys window"), + new AndroidLogcatCommandEntry("Dump Display Info", "adb shell dumpsys display"), + new AndroidLogcatCommandEntry("Dump CPU Info", "adb shell dumpsys cpuinfo"), + new AndroidLogcatCommandEntry("Dump Battery Stats", "adb shell dumpsys batterystats"), + new AndroidLogcatCommandEntry("Dump SurfaceFlinger", "adb shell dumpsys SurfaceFlinger"), + new AndroidLogcatCommandEntry("Dump Graphics Stats", "adb shell dumpsys gfxinfo "), + new AndroidLogcatCommandEntry("Dump Wifi Info", "adb shell dumpsys wifi"), + new AndroidLogcatCommandEntry("Dump Alarm Info", "adb shell dumpsys alarm"), + new AndroidLogcatCommandEntry("Dump Notification Info", "adb shell dumpsys notification"), + + // --- Settings --- + new AndroidLogcatCommandEntry("Enable Stay Awake", "adb shell settings put global stay_on_while_plugged_in 3"), + new AndroidLogcatCommandEntry("Disable Stay Awake", "adb shell settings put global stay_on_while_plugged_in 0"), + new AndroidLogcatCommandEntry("Show Touches On", "adb shell settings put system show_touches 1"), + new AndroidLogcatCommandEntry("Show Touches Off", "adb shell settings put system show_touches 0"), + new AndroidLogcatCommandEntry("Enable USB Debugging", "adb shell settings put global adb_enabled 1"), + new AndroidLogcatCommandEntry("Set Screen Off Timeout", "adb shell settings put system screen_off_timeout "), + + // --- Debug / Advanced --- + new AndroidLogcatCommandEntry("Bug Report", "adb bugreport"), + new AndroidLogcatCommandEntry("Remount System", "adb remount"), + new AndroidLogcatCommandEntry("Root Shell", "adb root"), + new AndroidLogcatCommandEntry("Unroot Shell", "adb unroot"), + new AndroidLogcatCommandEntry("Disable Verity", "adb disable-verity"), + new AndroidLogcatCommandEntry("Enable Verity", "adb enable-verity"), + new AndroidLogcatCommandEntry("Wait for Device Boot", "adb wait-for-device shell getprop sys.boot_completed"), + new AndroidLogcatCommandEntry("List Tombstones", "adb shell ls -lt /data/tombstones/"), + new AndroidLogcatCommandEntry("Pull Tombstone", "adb pull /data/tombstones/ "), + + // --- Meta Quest / XR --- + new AndroidLogcatCommandEntry("List OVR Packages", "adb shell pm list packages | grep oculus"), + new AndroidLogcatCommandEntry("Get Guardian State", "adb shell dumpsys OVRGuardianService"), + new AndroidLogcatCommandEntry("Get Compositor Stats", "adb shell dumpsys OVRCompositor"), + new AndroidLogcatCommandEntry("Set GPU Level", "adb shell setprop debug.oculus.gpuLevel <0-4>"), + new AndroidLogcatCommandEntry("Set CPU Level", "adb shell setprop debug.oculus.cpuLevel <0-4>"), + new AndroidLogcatCommandEntry("Enable Perf Overlay", "adb shell setprop debug.oculus.enablePerfOverlay 1"), + new AndroidLogcatCommandEntry("Disable Perf Overlay", "adb shell setprop debug.oculus.enablePerfOverlay 0"), + new AndroidLogcatCommandEntry("Set Fixed Foveation", "adb shell setprop debug.oculus.foveation.level <0-4>"), + new AndroidLogcatCommandEntry("Set Refresh Rate", "adb shell setprop debug.oculus.refreshRate <72|90|120>"), + + // --- AAB / Bundletool --- + new AndroidLogcatCommandEntry("bundletool: Build Split APKs", "java -jar bundletool.jar build-apks --bundle= --output= --connected-device"), + new AndroidLogcatCommandEntry("bundletool: Install APKs", "java -jar bundletool.jar install-apks --apks="), + new AndroidLogcatCommandEntry("bundletool: Get Device Spec", "java -jar bundletool.jar get-device-spec --output="), + }; + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs.meta new file mode 100644 index 00000000..0a6503d9 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAdbCommandCatalog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c184fe6fa853c48a29bcb98002dc77fd \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs new file mode 100644 index 00000000..100ed17d --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs @@ -0,0 +1,71 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace Unity.Android.Logcat +{ + internal class AndroidLogcatAddCommandDialog : EditorWindow + { + string m_Name = ""; + string m_Command = ""; + Action m_OnSave; + bool m_IsEdit; + Vector2 m_ScrollPos; + + internal static void Show(Action onSave, AndroidLogcatCommandEntry existing = null) + { + var wnd = CreateInstance(); + wnd.titleContent = new GUIContent(existing != null ? "Edit Command" : "Add Command"); + wnd.m_OnSave = onSave; + wnd.minSize = new Vector2(400, 200); + wnd.maxSize = new Vector2(600, 400); + + if (existing != null) + { + wnd.m_Name = existing.name; + wnd.m_Command = existing.command; + wnd.m_IsEdit = true; + } + + wnd.ShowUtility(); + } + + void OnGUI() + { + EditorGUILayout.Space(8); + + m_Name = EditorGUILayout.TextField("Name", m_Name); + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("Commands", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Enter one command per line. All commands run sequentially.", + EditorStyles.miniLabel); + + m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos, GUILayout.MinHeight(80), GUILayout.MaxHeight(200)); + m_Command = EditorGUILayout.TextArea(m_Command, GUILayout.ExpandHeight(true)); + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(8); + + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Cancel", GUILayout.Width(80))) + Close(); + + if (GUILayout.Button(m_IsEdit ? "Save" : "Add", GUILayout.Width(80))) + { + if (string.IsNullOrWhiteSpace(m_Name) || string.IsNullOrWhiteSpace(m_Command)) + { + EditorUtility.DisplayDialog("Android Logcat", "Both name and command are required.", "OK"); + return; + } + + m_OnSave?.Invoke(new AndroidLogcatCommandEntry(m_Name.Trim(), m_Command.Trim())); + Close(); + } + + EditorGUILayout.EndHorizontal(); + } + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs.meta new file mode 100644 index 00000000..91361f81 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b9328feb6e55441ecaaf37b9b821eedd \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs new file mode 100644 index 00000000..c0dda411 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Android.Logcat +{ + [Serializable] + internal class AndroidLogcatCommandEntry + { + [SerializeField] internal string name; + [SerializeField] internal string command; + + internal AndroidLogcatCommandEntry() { } + + internal AndroidLogcatCommandEntry(string name, string command) + { + this.name = name; + this.command = command; + } + } + + [Serializable] + internal class AndroidLogcatCommandEntryList + { + public AndroidLogcatCommandEntry[] items = Array.Empty(); + } + + [Serializable] + internal class AndroidLogcatCommandExportData + { + public AndroidLogcatCommandEntry[] favorites = Array.Empty(); + public AndroidLogcatCommandEntry[] general = Array.Empty(); + } + + internal static class AndroidLogcatCommandJsonHelper + { + internal static string ToJsonArray(List list) + { + var wrapper = new Wrapper { items = list.ToArray() }; + var json = JsonUtility.ToJson(wrapper); + var start = json.IndexOf('['); + var end = json.LastIndexOf(']'); + if (start >= 0 && end > start) + return json.Substring(start, end - start + 1); + return "[]"; + } + + [Serializable] + private class Wrapper + { + public T[] items; + } + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs.meta new file mode 100644 index 00000000..7587f65f --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandEntry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 751f57b4a1fa443a6a25067ac8f7e7cc \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs new file mode 100644 index 00000000..5da3f9d8 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Text; +using UnityEditor; +using UnityEngine; + +namespace Unity.Android.Logcat +{ + internal class AndroidLogcatCommandOutputWindow : EditorWindow + { + static AndroidLogcatCommandOutputWindow s_Instance; + + Vector2 m_ScrollPos; + readonly List m_Lines = new List(); + bool m_AutoScroll = true; + const int kMaxLines = 5000; + + internal struct OutputLine + { + internal string text; + internal Color color; + } + + internal static void Open(List existingLines) + { + var wnd = GetWindow(); + wnd.titleContent = new GUIContent("Command Output"); + wnd.minSize = new Vector2(500, 300); + s_Instance = wnd; + + wnd.m_Lines.Clear(); + if (existingLines != null) + { + foreach (var ol in existingLines) + wnd.m_Lines.Add(new OutputLine { text = ol.text, color = ol.color }); + } + } + + internal static void AppendIfOpen(string text, Color color) + { + if (s_Instance == null) + return; + if (string.IsNullOrEmpty(text)) + return; + + foreach (var line in text.Split('\n')) + { + s_Instance.m_Lines.Add(new OutputLine { text = line, color = color }); + } + + while (s_Instance.m_Lines.Count > kMaxLines) + s_Instance.m_Lines.RemoveAt(0); + + s_Instance.Repaint(); + } + + void OnEnable() + { + s_Instance = this; + } + + void OnGUI() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + if (GUILayout.Button("Clear", EditorStyles.toolbarButton)) + m_Lines.Clear(); + if (GUILayout.Button("Copy All", EditorStyles.toolbarButton)) + CopyAll(); + GUILayout.FlexibleSpace(); + m_AutoScroll = GUILayout.Toggle(m_AutoScroll, "Auto Scroll", EditorStyles.toolbarButton); + EditorGUILayout.EndHorizontal(); + + m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos); + + for (int i = 0; i < m_Lines.Count; i++) + { + var line = m_Lines[i]; + var style = new GUIStyle(EditorStyles.label) + { + wordWrap = true, + richText = false + }; + style.normal.textColor = line.color; + EditorGUILayout.LabelField(line.text, style); + } + + EditorGUILayout.EndScrollView(); + + if (m_AutoScroll && Event.current.type == EventType.Repaint) + m_ScrollPos.y = float.MaxValue; + } + + void CopyAll() + { + var sb = new StringBuilder(); + foreach (var line in m_Lines) + sb.AppendLine(line.text); + EditorGUIUtility.systemCopyBuffer = sb.ToString(); + } + + void OnDestroy() + { + if (s_Instance == this) + s_Instance = null; + } + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs.meta new file mode 100644 index 00000000..4ed31bde --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandOutputWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fbfc680dc3b744bf5b317b7f336d8b11 \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs new file mode 100644 index 00000000..68f9f0c7 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace Unity.Android.Logcat +{ + internal class AndroidLogcatCommandSearchWindow : EditorWindow + { + static AndroidLogcatCommandSearchWindow s_Instance; + + List m_Favorites; + List m_GeneralCommands; + Action m_OnAddCommand; + Action m_OnRunCommand; + + string m_SearchQuery = ""; + Vector2 m_ScrollPos; + + static readonly string[] s_ChipTerms = + { "devices", "packages", "logcat", "permissions", "input", "screen", "network", "dumpsys", "quest" }; + + internal static void Open( + List favorites, + List generalCommands, + Action onAdd, + Action onRun) + { + var wnd = GetWindow(); + wnd.titleContent = new GUIContent("Search Commands"); + wnd.minSize = new Vector2(500, 400); + s_Instance = wnd; + wnd.m_Favorites = favorites; + wnd.m_GeneralCommands = generalCommands; + wnd.m_OnAddCommand = onAdd; + wnd.m_OnRunCommand = onRun; + } + + void OnGUI() + { + EditorGUILayout.Space(4); + + // Search field + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Search:", GUILayout.Width(50)); + GUI.SetNextControlName("SearchField"); + m_SearchQuery = EditorGUILayout.TextField(m_SearchQuery); + EditorGUILayout.EndHorizontal(); + + // Quick-search chips + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Try:", GUILayout.Width(30)); + foreach (var term in s_ChipTerms) + { + if (GUILayout.Button(term, EditorStyles.miniButton, GUILayout.ExpandWidth(false))) + m_SearchQuery = term; + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(4); + + if (string.IsNullOrWhiteSpace(m_SearchQuery)) + { + EditorGUILayout.HelpBox("Type a search term or click a chip to find commands.", MessageType.Info); + return; + } + + var terms = m_SearchQuery.Trim().ToLowerInvariant(); + var favorites = m_Favorites ?? new List(); + var general = m_GeneralCommands ?? new List(); + + // Collect local results + var localResults = new List<(AndroidLogcatCommandEntry entry, string source)>(); + foreach (var e in favorites) + if (MatchesSearch(e, terms)) + localResults.Add((e, "Favorite")); + foreach (var e in general) + if (MatchesSearch(e, terms)) + localResults.Add((e, "General")); + + // Collect catalog results (excluding already-saved commands) + var savedCommands = new HashSet( + favorites.Concat(general).Select(c => c.command), + StringComparer.OrdinalIgnoreCase); + + var catalogResults = new List(); + foreach (var e in AndroidLogcatAdbCommandCatalog.All) + if (MatchesSearch(e, terms) && !savedCommands.Contains(e.command)) + catalogResults.Add(e); + + if (localResults.Count == 0 && catalogResults.Count == 0) + { + EditorGUILayout.HelpBox("No matching commands found.", MessageType.Info); + return; + } + + // Results list + m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos); + + foreach (var (entry, source) in localResults) + DrawResultRow(entry, source, true); + + if (localResults.Count > 0 && catalogResults.Count > 0) + EditorGUILayout.Space(4); + + foreach (var entry in catalogResults) + DrawResultRow(entry, "Catalog", false); + + EditorGUILayout.EndScrollView(); + } + + void DrawResultRow(AndroidLogcatCommandEntry entry, string source, bool isSaved) + { + EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); + + // Name + EditorGUILayout.LabelField(entry.name, EditorStyles.boldLabel, GUILayout.Width(180)); + + // Command preview + var style = new GUIStyle(EditorStyles.label) { wordWrap = false }; + style.normal.textColor = new Color(0.6f, 0.6f, 0.6f); + EditorGUILayout.LabelField(entry.command, style); + + // Source label + var sourceStyle = new GUIStyle(EditorStyles.miniLabel); + sourceStyle.normal.textColor = isSaved + ? new Color(0.4f, 0.7f, 0.4f) + : new Color(0.5f, 0.6f, 0.9f); + GUILayout.Label(source, sourceStyle, GUILayout.Width(55)); + + // Add button (only for catalog entries not yet saved) + if (!isSaved) + { + if (GUILayout.Button("Add", EditorStyles.miniButton, GUILayout.Width(40))) + m_OnAddCommand?.Invoke(new AndroidLogcatCommandEntry(entry.name, entry.command)); + } + + // Run button + if (GUILayout.Button("Run", EditorStyles.miniButton, GUILayout.Width(40))) + m_OnRunCommand?.Invoke(entry); + + EditorGUILayout.EndHorizontal(); + } + + static bool MatchesSearch(AndroidLogcatCommandEntry entry, string terms) + { + return entry.name.ToLowerInvariant().Contains(terms) + || entry.command.ToLowerInvariant().Contains(terms); + } + + void OnDestroy() + { + if (s_Instance == this) + s_Instance = null; + } + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs.meta new file mode 100644 index 00000000..3a97a5f7 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandSearchWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f2d601788783b46278a4bb0e43b8e34d \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs new file mode 100644 index 00000000..cc8c3e59 --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs @@ -0,0 +1,523 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace Unity.Android.Logcat +{ + internal class AndroidLogcatCommandsWindow : EditorWindow + { + static readonly AndroidLogcatCommandEntry[] s_DefaultCommands = new[] + { + new AndroidLogcatCommandEntry("List Devices", "adb devices"), + new AndroidLogcatCommandEntry("Kill Server", "adb kill-server"), + new AndroidLogcatCommandEntry("Reboot Device", "adb reboot") + }; + + internal struct OutputLine + { + internal string text; + internal Color color; + } + + AndroidLogcatRuntimeBase m_Runtime; + AndroidLogcatDeviceSelection m_DeviceSelection; + + List m_Favorites = new List(); + List m_GeneralCommands = new List(); + readonly List m_OutputLines = new List(); + + bool m_EditMode; + bool m_Running; + int m_SelectedFavoriteIndex = -1; + int m_SelectedGeneralIndex = -1; + + Vector2 m_FavoritesScrollPos; + Vector2 m_GeneralScrollPos; + Vector2 m_OutputScrollPos; + float m_OutputHeight = 150; + + const int kRowHeight = 22; + const int kMaxOutputLines = 3000; + + static readonly Color kCommandColor = new Color(0.6f, 0.6f, 0.6f); + static readonly Color kAccentColor = new Color(0.4f, 0.8f, 0.4f); + static readonly Color kErrorColor = new Color(0.9f, 0.3f, 0.3f); + + internal static void ShowWindow() + { + var wnd = GetWindow("Commands"); + wnd.minSize = new Vector2(600, 400); + } + + void OnEnable() + { + if (!AndroidBridge.AndroidExtensionsInstalled) + return; + + m_Runtime = AndroidLogcatManager.instance.Runtime; + m_DeviceSelection = new AndroidLogcatDeviceSelection(m_Runtime, OnDeviceSelected, nameof(AndroidLogcatCommandsWindow) + "_DeviceId"); + m_Runtime.Closing += OnDisable; + m_Runtime.DeviceQuery.UpdateConnectedDevicesList(true); + + LoadCommands(); + } + + void OnDisable() + { + if (!AndroidBridge.AndroidExtensionsInstalled) + return; + if (m_Runtime == null) + return; + + SaveCommands(); + + m_Runtime.Closing -= OnDisable; + m_DeviceSelection.Dispose(); + m_DeviceSelection = null; + m_Runtime = null; + } + + void OnDeviceSelected(IAndroidLogcatDevice device) + { + Repaint(); + } + + void OnGUI() + { + if (!AndroidBridge.AndroidExtensionsInstalled) + { + AndroidLogcatUtilities.ShowAndroidIsNotInstalledMessage(); + return; + } + + DoToolbar(); + + var outputAreaRect = new Rect(0, position.height - m_OutputHeight, position.width, m_OutputHeight); + var listsHeight = position.height - EditorGUIUtility.singleLineHeight - 4 - m_OutputHeight - 8; + + DoCommandLists(listsHeight); + DoOutputSplitter(outputAreaRect.y - 4); + DoOutputArea(); + } + + // --- Toolbar --- + + void DoToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + m_DeviceSelection.DoGUI(); + GUILayout.Space(3); + + if (GUILayout.Button("Add Command", EditorStyles.toolbarButton)) + OnAddCommand(); + + var editLabel = m_EditMode ? "Edit Mode: ON" : "Edit Mode: OFF"; + if (GUILayout.Button(editLabel, EditorStyles.toolbarButton)) + m_EditMode = !m_EditMode; + + if (GUILayout.Button("Import", EditorStyles.toolbarButton)) + OnImportCommands(); + + if (GUILayout.Button("Export", EditorStyles.toolbarButton)) + OnExportCommands(); + + if (GUILayout.Button("Search Catalog", EditorStyles.toolbarButton)) + OpenSearchWindow(); + + GUILayout.FlexibleSpace(); + + if (m_Running) + GUILayout.Label("Running...", EditorStyles.miniLabel); + + EditorGUILayout.EndHorizontal(); + } + + // --- Command Lists --- + + void DoCommandLists(float totalHeight) + { + var favHeight = Mathf.Min(m_Favorites.Count * kRowHeight + kRowHeight + 4, totalHeight * 0.4f); + if (m_Favorites.Count == 0) + favHeight = kRowHeight + 4; + + // Favorites + EditorGUILayout.LabelField("Favorites", EditorStyles.boldLabel); + m_FavoritesScrollPos = EditorGUILayout.BeginScrollView(m_FavoritesScrollPos, GUILayout.Height(favHeight)); + if (m_Favorites.Count == 0) + { + EditorGUILayout.LabelField("No favorites. Use Edit Mode to move commands here.", EditorStyles.centeredGreyMiniLabel); + } + else + { + for (int i = 0; i < m_Favorites.Count; i++) + DoCommandRow(m_Favorites, i, true, ref m_SelectedFavoriteIndex); + } + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(2); + + // General Commands + EditorGUILayout.LabelField("General Commands", EditorStyles.boldLabel); + m_GeneralScrollPos = EditorGUILayout.BeginScrollView(m_GeneralScrollPos); + if (m_GeneralCommands.Count == 0) + { + EditorGUILayout.LabelField("No commands. Click 'Add Command' or 'Search Catalog'.", EditorStyles.centeredGreyMiniLabel); + } + else + { + for (int i = 0; i < m_GeneralCommands.Count; i++) + DoCommandRow(m_GeneralCommands, i, false, ref m_SelectedGeneralIndex); + } + EditorGUILayout.EndScrollView(); + } + + void DoCommandRow(List list, int index, bool isFavorites, ref int selectedIndex) + { + var entry = list[index]; + var isSelected = selectedIndex == index; + + if (isSelected) + GUI.backgroundColor = new Color(0.3f, 0.5f, 0.8f, 0.3f); + + EditorGUILayout.BeginHorizontal(EditorStyles.helpBox, GUILayout.Height(kRowHeight)); + + if (isSelected) + GUI.backgroundColor = Color.white; + + // Click to select + if (Event.current.type == EventType.MouseDown && GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition)) + { + selectedIndex = index; + Repaint(); + } + + // Name + EditorGUILayout.LabelField(entry.name, EditorStyles.boldLabel, GUILayout.Width(180)); + + // Command preview + var cmdDisplay = entry.command; + if (cmdDisplay.IndexOfAny(new[] { '\r', '\n' }) >= 0) + { + var cmdLines = cmdDisplay.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + cmdDisplay = cmdLines.Length > 1 + ? $"{cmdLines[0].Trim()} (+{cmdLines.Length - 1} more)" + : cmdLines[0].Trim(); + } + var cmdStyle = new GUIStyle(EditorStyles.label); + cmdStyle.normal.textColor = kCommandColor; + EditorGUILayout.LabelField(cmdDisplay, cmdStyle); + + // Edit mode buttons + if (m_EditMode) + { + // Move up + EditorGUI.BeginDisabledGroup(index == 0); + if (GUILayout.Button("\u25B2", EditorStyles.miniButton, GUILayout.Width(22))) + { + list.RemoveAt(index); + list.Insert(index - 1, entry); + if (selectedIndex == index) selectedIndex = index - 1; + SaveCommands(); + } + EditorGUI.EndDisabledGroup(); + + // Move down + EditorGUI.BeginDisabledGroup(index == list.Count - 1); + if (GUILayout.Button("\u25BC", EditorStyles.miniButton, GUILayout.Width(22))) + { + list.RemoveAt(index); + list.Insert(index + 1, entry); + if (selectedIndex == index) selectedIndex = index + 1; + SaveCommands(); + } + EditorGUI.EndDisabledGroup(); + + // Fav / Unfav + var favLabel = isFavorites ? "Unfav" : "Fav"; + if (GUILayout.Button(favLabel, EditorStyles.miniButton, GUILayout.Width(40))) + { + if (isFavorites) + { + m_Favorites.RemoveAt(index); + m_GeneralCommands.Add(entry); + selectedIndex = -1; + } + else + { + m_GeneralCommands.RemoveAt(index); + m_Favorites.Add(entry); + selectedIndex = -1; + } + SaveCommands(); + } + + // Edit + if (GUILayout.Button("Edit", EditorStyles.miniButton, GUILayout.Width(35))) + { + AndroidLogcatAddCommandDialog.Show(updated => + { + entry.name = updated.name; + entry.command = updated.command; + SaveCommands(); + Repaint(); + }, entry); + } + + // Delete + if (GUILayout.Button("Del", EditorStyles.miniButton, GUILayout.Width(30))) + { + if (EditorUtility.DisplayDialog("Delete Command", + $"Delete \"{entry.name}\"?", "Delete", "Cancel")) + { + list.RemoveAt(index); + selectedIndex = -1; + SaveCommands(); + } + } + } + + // Run button (always visible) + if (GUILayout.Button("Run", EditorStyles.miniButton, GUILayout.Width(35))) + RunCommand(entry); + + EditorGUILayout.EndHorizontal(); + } + + // --- Output Area --- + + void DoOutputSplitter(float y) + { + var splitterRect = new Rect(0, y, position.width, 4); + EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeVertical); + + if (Event.current.type == EventType.MouseDown && splitterRect.Contains(Event.current.mousePosition)) + GUIUtility.hotControl = GUIUtility.GetControlID(FocusType.Passive); + + if (GUIUtility.hotControl != 0 && Event.current.type == EventType.MouseDrag) + { + m_OutputHeight = Mathf.Clamp(position.height - Event.current.mousePosition.y, 50, position.height - 150); + Repaint(); + } + + if (Event.current.type == EventType.MouseUp) + GUIUtility.hotControl = 0; + } + + void DoOutputArea() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("Output", EditorStyles.boldLabel, GUILayout.Width(50)); + if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(45))) + m_OutputLines.Clear(); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + m_OutputScrollPos = EditorGUILayout.BeginScrollView(m_OutputScrollPos, GUILayout.Height(m_OutputHeight - 20)); + + for (int i = 0; i < m_OutputLines.Count; i++) + { + var line = m_OutputLines[i]; + var style = new GUIStyle(EditorStyles.label) + { + wordWrap = true, + richText = false + }; + style.normal.textColor = line.color; + EditorGUILayout.LabelField(line.text, style); + } + + EditorGUILayout.EndScrollView(); + } + + // --- Command Execution --- + + void RunCommand(AndroidLogcatCommandEntry entry) + { + AndroidLogcatPlaceholderDialog.Show(entry.command, resolved => ExecuteCommand(resolved)); + } + + void ExecuteCommand(string resolvedCommand) + { + var lines = resolvedCommand.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + m_Running = true; + Repaint(); + + var device = m_DeviceSelection != null ? m_DeviceSelection.SelectedDevice : null; + var deviceId = device != null ? device.Id : null; + + foreach (var rawLine in lines) + { + var cmd = rawLine.Trim(); + if (string.IsNullOrEmpty(cmd)) + continue; + + AppendOutput($"> {cmd}", kAccentColor); + + try + { + string result; + if (cmd.StartsWith("adb ", StringComparison.OrdinalIgnoreCase)) + { + var adbArgs = cmd.Substring(4); + + // Inject device serial if we have a selected device and the command doesn't already specify -s + if (!string.IsNullOrEmpty(deviceId) && !adbArgs.TrimStart().StartsWith("-s ")) + adbArgs = $"-s {deviceId} {adbArgs}"; + + result = m_Runtime.Tools.ADB.Run(new[] { adbArgs }, ""); + } + else + { + var parts = cmd.Split(new[] { ' ' }, 2); + var fileName = parts[0]; + var arguments = parts.Length > 1 ? parts[1] : ""; + var shellResult = Shell.RunProcess(fileName, arguments); + result = shellResult.GetStandardOut(); + var err = shellResult.GetStandardErr(); + if (!string.IsNullOrEmpty(err)) + result = string.IsNullOrEmpty(result) ? err : result + "\n" + err; + } + + AppendOutput(result, GUI.skin.label.normal.textColor); + } + catch (Exception ex) + { + AppendOutput($"[Error] {ex.Message}", kErrorColor); + } + } + + m_Running = false; + Repaint(); + } + + void AppendOutput(string text, Color color) + { + if (string.IsNullOrEmpty(text)) + return; + + foreach (var line in text.Split('\n')) + m_OutputLines.Add(new OutputLine { text = line, color = color }); + + while (m_OutputLines.Count > kMaxOutputLines) + m_OutputLines.RemoveAt(0); + + m_OutputScrollPos.y = float.MaxValue; + + AndroidLogcatCommandOutputWindow.AppendIfOpen(text, color); + } + + // --- Add / Import / Export --- + + void OnAddCommand() + { + AndroidLogcatAddCommandDialog.Show(entry => + { + m_GeneralCommands.Add(entry); + SaveCommands(); + Repaint(); + }); + } + + void OpenSearchWindow() + { + AndroidLogcatCommandSearchWindow.Open( + m_Favorites, m_GeneralCommands, + entry => + { + m_GeneralCommands.Add(entry); + SaveCommands(); + Repaint(); + }, + RunCommand); + } + + void OnExportCommands() + { + var data = new AndroidLogcatCommandExportData + { + favorites = m_Favorites.ToArray(), + general = m_GeneralCommands.ToArray() + }; + + var json = JsonUtility.ToJson(data, true); + var path = EditorUtility.SaveFilePanel("Export Commands", "", "logcat-commands", "json"); + if (string.IsNullOrEmpty(path)) + return; + + File.WriteAllText(path, json); + Debug.Log($"[Android Logcat] Commands exported to {path}"); + } + + void OnImportCommands() + { + var path = EditorUtility.OpenFilePanel("Import Commands", "", "json"); + if (string.IsNullOrEmpty(path)) + return; + + AndroidLogcatCommandExportData data; + try + { + var json = File.ReadAllText(path); + data = JsonUtility.FromJson(json); + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Import Failed", + $"Could not parse the selected file:\n{ex.Message}", "OK"); + return; + } + + if ((data.favorites == null || data.favorites.Length == 0) && + (data.general == null || data.general.Length == 0)) + { + EditorUtility.DisplayDialog("Import Failed", + "The file contains no commands.", "OK"); + return; + } + + if (!EditorUtility.DisplayDialog("Import Commands", + "This will replace all your current commands. Continue?", "Import", "Cancel")) + return; + + m_Favorites.Clear(); + if (data.favorites != null) + m_Favorites.AddRange(data.favorites); + + m_GeneralCommands.Clear(); + if (data.general != null) + m_GeneralCommands.AddRange(data.general); + + SaveCommands(); + Debug.Log($"[Android Logcat] Commands imported from {path}"); + } + + // --- Persistence --- + + void LoadCommands() + { + var settings = m_Runtime.UserSettings.CommandsSettings; + m_Favorites.Clear(); + m_GeneralCommands.Clear(); + + if (settings.Favorites != null) + m_Favorites.AddRange(settings.Favorites); + if (settings.GeneralCommands != null) + m_GeneralCommands.AddRange(settings.GeneralCommands); + + if (m_GeneralCommands.Count == 0 && m_Favorites.Count == 0) + m_GeneralCommands.AddRange(s_DefaultCommands); + } + + void SaveCommands() + { + var settings = m_Runtime.UserSettings.CommandsSettings; + settings.Favorites = new List(m_Favorites); + settings.GeneralCommands = new List(m_GeneralCommands); + } + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs.meta new file mode 100644 index 00000000..80fa9b3f --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCommandsWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b07e946eed7fc401a89c81b7c89a812b \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatConsoleWindow.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatConsoleWindow.cs index 5ce27cda..2bca569d 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatConsoleWindow.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatConsoleWindow.cs @@ -338,6 +338,9 @@ private void MenuToolsSelection(object userData, string[] options, int selected) case ToolsContextMenu.LayoutViewer: AndroidLogcatLayoutViewerWindow.ShowWindow(); break; + case ToolsContextMenu.Commands: + AndroidLogcatCommandsWindow.ShowWindow(); + break; case ToolsContextMenu.WindowMemory: m_Runtime.UserSettings.ExtraWindowState.Type = ExtraWindow.Memory; break; @@ -363,6 +366,7 @@ private void DoToolsGUI() contextMenu.Add(ToolsContextMenu.StacktraceUtility, "Stacktrace Utility"); if (Unsupported.IsDeveloperMode()) contextMenu.Add(ToolsContextMenu.LayoutViewer, "Experimental/Layout Viewer"); + contextMenu.Add(ToolsContextMenu.Commands, "Commands"); var b = m_Runtime.UserSettings.ExtraWindowState.Type; contextMenu.Add(ToolsContextMenu.WindowMemory, "Window/Memory", b == ExtraWindow.Memory); contextMenu.Add(ToolsContextMenu.WindowInputs, "Window/Inputs", b == ExtraWindow.Inputs); diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatContextMenu.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatContextMenu.cs index 46b747e1..242eddbe 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatContextMenu.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatContextMenu.cs @@ -28,6 +28,7 @@ internal enum ToolsContextMenu OpenTerminal, StacktraceUtility, LayoutViewer, + Commands, WindowMemory, WindowInputs, WindowHidden diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs new file mode 100644 index 00000000..71c5e2df --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEngine; + +namespace Unity.Android.Logcat +{ + internal class AndroidLogcatPlaceholderDialog : EditorWindow + { + static readonly Regex s_PlaceholderRegex = new Regex(@"<([^>]+)>", RegexOptions.Compiled); + + string m_OriginalCommand; + Action m_OnConfirm; + List m_Placeholders = new List(); + + class PlaceholderInfo + { + internal string token; + internal string value; + } + + internal static void Show(string command, Action onConfirm) + { + var matches = s_PlaceholderRegex.Matches(command); + if (matches.Count == 0) + { + onConfirm?.Invoke(command); + return; + } + + var wnd = CreateInstance(); + wnd.titleContent = new GUIContent("Resolve Placeholders"); + wnd.m_OriginalCommand = command; + wnd.m_OnConfirm = onConfirm; + wnd.minSize = new Vector2(400, 150); + wnd.maxSize = new Vector2(600, 400); + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (Match match in matches) + { + var token = match.Groups[1].Value; + if (!seen.Add(token)) + continue; + + var defaultValue = ""; + if (token.Equals("package", StringComparison.OrdinalIgnoreCase)) + defaultValue = PlayerSettings.applicationIdentifier; + + wnd.m_Placeholders.Add(new PlaceholderInfo { token = token, value = defaultValue }); + } + + wnd.ShowUtility(); + } + + void OnGUI() + { + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("Fill in the placeholder values:", EditorStyles.boldLabel); + EditorGUILayout.Space(4); + + EditorGUILayout.LabelField(m_OriginalCommand, EditorStyles.wordWrappedMiniLabel); + EditorGUILayout.Space(8); + + foreach (var placeholder in m_Placeholders) + { + placeholder.value = EditorGUILayout.TextField($"<{placeholder.token}>", placeholder.value); + } + + EditorGUILayout.Space(8); + + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Cancel", GUILayout.Width(80))) + Close(); + + if (GUILayout.Button("Run", GUILayout.Width(80))) + { + var resolved = m_OriginalCommand; + foreach (var placeholder in m_Placeholders) + { + resolved = resolved.Replace($"<{placeholder.token}>", placeholder.value); + } + + m_OnConfirm?.Invoke(resolved); + Close(); + } + + EditorGUILayout.EndHorizontal(); + } + } +} diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs.meta b/com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs.meta new file mode 100644 index 00000000..c5ff994d --- /dev/null +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatPlaceholderDialog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 46a4bebaf15ee43c5bbf7012104e2359 \ No newline at end of file diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatUserSettings.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatUserSettings.cs index 2f7edd09..84f997aa 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatUserSettings.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatUserSettings.cs @@ -89,6 +89,15 @@ internal class QueryLayoutSettings internal string LastScreenshotSaveLocation; } + [Serializable] + internal class CommandsSettingsData + { + [SerializeField] + internal List Favorites = new List(); + [SerializeField] + internal List GeneralCommands = new List(); + } + [SerializeField] private string m_SelectedDeviceId; [SerializeField] @@ -116,6 +125,8 @@ internal class QueryLayoutSettings private QueryLayoutSettings m_QueryLayoutSettings; [SerializeField] private InputSettings m_InputSettings; + [SerializeField] + private CommandsSettingsData m_CommandsSettings; [SerializeField] private AutoScroll m_AutoScroll; @@ -178,6 +189,7 @@ public Priority SelectedPriority public ScreenCaptureSettings CaptureSettings { set => m_ScreenCaptureSettings = value; get => m_ScreenCaptureSettings; } public QueryLayoutSettings LayoutSettings { set => m_QueryLayoutSettings = value; get => m_QueryLayoutSettings; } public InputSettings DeviceInputSettings { set => m_InputSettings = value; get => m_InputSettings; } + public CommandsSettingsData CommandsSettings { set => m_CommandsSettings = value; get => m_CommandsSettings; } public AutoScroll AutoScroll { set => m_AutoScroll = value; get => m_AutoScroll; } @@ -360,6 +372,8 @@ internal void Reset() SendText = string.Empty, TargetProcess = new ProcessInformation() }; + + m_CommandsSettings = new CommandsSettingsData(); } internal void ResetCaptureVideoSettings() From 6bac2c2026a1c8fd21ca0b19574309a4da3e498f Mon Sep 17 00:00:00 2001 From: dianna Date: Mon, 18 May 2026 14:30:18 -0700 Subject: [PATCH 2/3] Reworked all five Commands windows from IMGUI to UIToolkit: --- .../Editor/AndroidLogcatAddCommandDialog.cs | 54 +-- .../Editor/AndroidLogcatCommandEntry.cs | 27 -- .../AndroidLogcatCommandOutputWindow.cs | 105 +++-- .../AndroidLogcatCommandSearchWindow.cs | 164 ++++--- .../Editor/AndroidLogcatCommandsWindow.cs | 433 ++++++++---------- .../Editor/AndroidLogcatPlaceholderDialog.cs | 55 ++- .../UI/Layouts/AndroidLogcatAddCommand.uxml | 12 + .../Layouts/AndroidLogcatAddCommand.uxml.meta | 10 + .../Layouts/AndroidLogcatCommandOutput.uxml | 13 + .../AndroidLogcatCommandOutput.uxml.meta | 10 + .../Layouts/AndroidLogcatCommandSearch.uxml | 10 + .../AndroidLogcatCommandSearch.uxml.meta | 10 + .../UI/Layouts/AndroidLogcatCommands.uxml | 26 ++ .../Layouts/AndroidLogcatCommands.uxml.meta | 10 + .../UI/Layouts/AndroidLogcatPlaceholder.uxml | 11 + .../AndroidLogcatPlaceholder.uxml.meta | 10 + 16 files changed, 530 insertions(+), 430 deletions(-) create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatAddCommand.uxml create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatAddCommand.uxml.meta create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatCommandOutput.uxml create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatCommandOutput.uxml.meta create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatCommandSearch.uxml create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatCommandSearch.uxml.meta create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatCommands.uxml create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatCommands.uxml.meta create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatPlaceholder.uxml create mode 100644 com.unity.mobile.android-logcat/Editor/UI/Layouts/AndroidLogcatPlaceholder.uxml.meta diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs index 100ed17d..5301a661 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatAddCommandDialog.cs @@ -1,6 +1,7 @@ using System; using UnityEditor; using UnityEngine; +using UnityEngine.UIElements; namespace Unity.Android.Logcat { @@ -10,7 +11,6 @@ internal class AndroidLogcatAddCommandDialog : EditorWindow string m_Command = ""; Action m_OnSave; bool m_IsEdit; - Vector2 m_ScrollPos; internal static void Show(Action onSave, AndroidLogcatCommandEntry existing = null) { @@ -30,42 +30,42 @@ internal static void Show(Action onSave, AndroidLogca wnd.ShowUtility(); } - void OnGUI() + void OnEnable() { - EditorGUILayout.Space(8); - - m_Name = EditorGUILayout.TextField("Name", m_Name); + LoadUI(); + } - EditorGUILayout.Space(4); - EditorGUILayout.LabelField("Commands", EditorStyles.boldLabel); - EditorGUILayout.LabelField("Enter one command per line. All commands run sequentially.", - EditorStyles.miniLabel); + void LoadUI() + { + var r = rootVisualElement; + var tree = AndroidLogcatUtilities.LoadUXML("AndroidLogcatAddCommand.uxml"); + tree.CloneTree(r); - m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos, GUILayout.MinHeight(80), GUILayout.MaxHeight(200)); - m_Command = EditorGUILayout.TextArea(m_Command, GUILayout.ExpandHeight(true)); - EditorGUILayout.EndScrollView(); + var nameField = r.Q("NameField"); + nameField.value = m_Name; + nameField.RegisterValueChangedCallback(evt => m_Name = evt.newValue); - EditorGUILayout.Space(8); + var commandField = r.Q("CommandField"); + commandField.value = m_Command; + commandField.RegisterValueChangedCallback(evt => m_Command = evt.newValue); - EditorGUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); + var saveButton = r.Q