├── .gitignore ├── Documents~ └── imgs │ ├── img_sample_message_banner.png │ └── img_sample_message_viewer.png ├── Editor.meta ├── Editor ├── EditorMessageUtility.cs ├── EditorMessageUtility.cs.meta ├── GBG.EditorMessages.Editor.asmdef ├── GBG.EditorMessages.Editor.asmdef.meta ├── MessageBanner.cs ├── MessageBanner.cs.meta ├── MessageDetailsElement.cs ├── MessageDetailsElement.cs.meta ├── MessageDetailsTabElement.cs ├── MessageDetailsTabElement.cs.meta ├── MessageElement.cs ├── MessageElement.cs.meta ├── MessageTypeToggle.cs ├── MessageTypeToggle.cs.meta ├── MessageViewer.cs ├── MessageViewer.cs.meta ├── ToolbarImageButton.cs └── ToolbarImageButton.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── GBG.EditorMessages.asmdef ├── GBG.EditorMessages.asmdef.meta ├── Message.cs ├── Message.cs.meta ├── MessageUtility.cs └── MessageUtility.cs.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Recordings can get excessive in size 18 | /[Rr]ecordings/ 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # Gradle cache directory 30 | .gradle/ 31 | 32 | # Autogenerated VS/MD/Consulo solution and project files 33 | ExportedObj/ 34 | .consulo/ 35 | *.csproj 36 | *.unityproj 37 | *.sln 38 | *.suo 39 | *.tmp 40 | *.user 41 | *.userprefs 42 | *.pidb 43 | *.booproj 44 | *.svd 45 | *.pdb 46 | *.mdb 47 | *.opendb 48 | *.VC.db 49 | 50 | # Unity3D generated meta files 51 | *.pidb.meta 52 | *.pdb.meta 53 | *.mdb.meta 54 | 55 | # Unity3D generated file on crash reports 56 | sysinfo.txt 57 | 58 | # Builds 59 | *.apk 60 | *.aab 61 | *.unitypackage 62 | *.app 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # Packed Addressables 68 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 69 | 70 | # Temporary auto-generated Android Assets 71 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 72 | /[Aa]ssets/[Ss]treamingAssets/aa/* 73 | -------------------------------------------------------------------------------- /Documents~/imgs/img_sample_message_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolarianZ/UnityEditorMessages/13075ba5de5efc43f2a3c00a0a9aa23e24786939/Documents~/imgs/img_sample_message_banner.png -------------------------------------------------------------------------------- /Documents~/imgs/img_sample_message_viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolarianZ/UnityEditorMessages/13075ba5de5efc43f2a3c00a0a9aa23e24786939/Documents~/imgs/img_sample_message_viewer.png -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff99dc8cd13c88a4289c8ced5641b214 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/EditorMessageUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using UnityEngine.TextCore.Text; 5 | using UnityEngine.UIElements; 6 | using UObject = UnityEngine.Object; 7 | 8 | namespace GBG.EditorMessages.Editor 9 | { 10 | public static class EditorMessageUtility 11 | { 12 | #region Style 13 | 14 | public static int GlobalIconSize = 16; 15 | 16 | public static Color TabActiveColor => EditorGUIUtility.isProSkin 17 | ? new Color32(56, 56, 56, 255) 18 | : new Color32(200, 200, 200, 255); 19 | public static Color TabInactiveColor => Color.clear; 20 | public static Color TabBackgroundColor => EditorGUIUtility.isProSkin 21 | ? new Color32(255, 255, 255, 40) 22 | : new Color32(255, 255, 255, 150); 23 | 24 | #endregion 25 | 26 | 27 | #region Icon & Font 28 | 29 | //private static Font _monospaceFont; 30 | private static FontAsset _monospaceFontAsset; 31 | 32 | 33 | public static FontAsset GetMonospaceFontAsset() 34 | { 35 | if (!_monospaceFontAsset) 36 | { 37 | //_monospaceFont = (Font)EditorGUIUtility.LoadRequired("fonts/robotomono/robotomono-regular.ttf"); 38 | _monospaceFontAsset = (FontAsset)EditorGUIUtility.LoadRequired("fonts/robotomono/robotomono-regular sdf.asset"); 39 | } 40 | return _monospaceFontAsset; 41 | } 42 | 43 | public static Texture GetMessageTypeIcon(MessageType messageType) 44 | { 45 | switch (messageType) 46 | { 47 | case MessageType.Info: 48 | return GetInfoIcon(); 49 | case MessageType.Warning: 50 | return GetWarningIcon(); 51 | case MessageType.Error: 52 | return GetErrorIcon(); 53 | default: 54 | throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null); 55 | } 56 | } 57 | 58 | public static Texture GetInfoIcon(bool inactive = false) 59 | { 60 | if (inactive) 61 | { 62 | return EditorGUIUtility.IconContent("console.infoicon.inactive.sml@2x").image; 63 | } 64 | 65 | return EditorGUIUtility.IconContent("console.infoicon.sml").image; 66 | } 67 | 68 | public static Texture GetWarningIcon(bool inactive = false) 69 | { 70 | if (inactive) 71 | { 72 | return EditorGUIUtility.IconContent("console.warnicon.inactive.sml@2x").image; 73 | } 74 | 75 | return EditorGUIUtility.IconContent("console.warnicon.sml").image; 76 | } 77 | 78 | public static Texture GetErrorIcon(bool inactive = false) 79 | { 80 | if (inactive) 81 | { 82 | return EditorGUIUtility.IconContent("console.erroricon.inactive.sml").image; 83 | } 84 | 85 | return EditorGUIUtility.IconContent("console.erroricon.sml").image; 86 | } 87 | 88 | public static Texture GetContextIcon() 89 | { 90 | return EditorGUIUtility.IconContent("gameobject icon").image; 91 | } 92 | 93 | public static Texture GetCustomDataIcon() 94 | { 95 | return EditorGUIUtility.IconContent("animation.play").image; 96 | } 97 | 98 | public static Texture GetClearIcon() 99 | { 100 | return EditorGUIUtility.IconContent("clear").image; 101 | } 102 | 103 | 104 | public static Image NewImage(Texture image = null, string tooltip = null, 105 | DisplayStyle display = DisplayStyle.Flex, PickingMode pickingMode = PickingMode.Ignore) 106 | { 107 | Image imageElement = new Image 108 | { 109 | tooltip = tooltip, 110 | image = image, 111 | pickingMode = pickingMode, 112 | style = 113 | { 114 | display = display, 115 | alignSelf = Align.Center, 116 | minWidth = GlobalIconSize, 117 | maxWidth = GlobalIconSize, 118 | minHeight = GlobalIconSize, 119 | maxHeight = GlobalIconSize, 120 | } 121 | }; 122 | return imageElement; 123 | } 124 | 125 | #endregion 126 | 127 | 128 | #region Scene Object 129 | 130 | public static void TryPingContextObject(this Message message, bool openOwnerScene) 131 | { 132 | if (message == null || string.IsNullOrWhiteSpace(message.context)) 133 | { 134 | return; 135 | } 136 | 137 | UObject context = message.GetUnityContextObject(); 138 | if (context) 139 | { 140 | EditorGUIUtility.PingObject(context); 141 | return; 142 | } 143 | 144 | if (!message.TryGetContextOwnerSceneAsset(out SceneAsset sceneAsset)) 145 | { 146 | return; 147 | } 148 | 149 | if (!openOwnerScene) 150 | { 151 | EditorGUIUtility.PingObject(sceneAsset); 152 | return; 153 | } 154 | 155 | AssetDatabase.OpenAsset(sceneAsset); 156 | message.TryPingContextObject(false); 157 | } 158 | 159 | public static bool TryGetContextOwnerSceneAsset(this Message message, out SceneAsset sceneAsset) 160 | { 161 | sceneAsset = null; 162 | if (message == null || string.IsNullOrWhiteSpace(message.context)) 163 | { 164 | return false; 165 | } 166 | 167 | if (!GlobalObjectId.TryParse(message.context, out GlobalObjectId globalObjectId)) 168 | { 169 | return false; 170 | } 171 | 172 | string sceneGuid = globalObjectId.assetGUID.ToString(); 173 | string scenePath = AssetDatabase.GUIDToAssetPath(sceneGuid); 174 | if (string.IsNullOrEmpty(scenePath) || !scenePath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase)) 175 | { 176 | return false; 177 | } 178 | 179 | sceneAsset = AssetDatabase.LoadAssetAtPath(scenePath); 180 | return sceneAsset; 181 | } 182 | 183 | #endregion 184 | 185 | 186 | //public static void DestroyAuto(this UObject obj) 187 | //{ 188 | // if (!obj) 189 | // { 190 | // return; 191 | // } 192 | // 193 | // if (Application.isPlaying) 194 | // { 195 | // UObject.Destroy(obj); 196 | // } 197 | // else 198 | // { 199 | // UObject.DestroyImmediate(obj); 200 | // } 201 | //} 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Editor/EditorMessageUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd2d7d7282b528e47b79738a80f2c45f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GBG.EditorMessages.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GBG.EditorMessages.Editor", 3 | "rootNamespace": "GBG.EditorMessages.Editor", 4 | "references": [ 5 | "GUID:3aac730def3917a4d8049cccae982d1e" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": true, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [], 17 | "noEngineReferences": false 18 | } -------------------------------------------------------------------------------- /Editor/GBG.EditorMessages.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0ae52effae494b4a8d13e21b4615d33 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/MessageBanner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | namespace GBG.EditorMessages.Editor 8 | { 9 | public class MessageBanner : VisualElement 10 | { 11 | public Image TypeImage { get; } 12 | public Label MessageLabel { get; } 13 | public Image InfoTypeImage { get; } 14 | public Label InfoCountLabel { get; } 15 | public Image WarningTypeImage { get; } 16 | public Label WarningCountLabel { get; } 17 | public Image ErrorTypeImage { get; } 18 | public Label ErrorCountLabel { get; } 19 | 20 | private bool _showMessageTypeCount; 21 | public bool ShowMessageTypeCount 22 | { 23 | get => _showMessageTypeCount; 24 | set 25 | { 26 | if (_showMessageTypeCount == value) 27 | return; 28 | 29 | _showMessageTypeCount = value; 30 | RefreshMessageTypeCountDisplay(); 31 | } 32 | } 33 | 34 | public object Source { get; set; } 35 | public string SourceName { get; set; } 36 | public bool AllowClearMessages { get; set; } 37 | public IList Messages { get; private set; } 38 | public int CurrentMessageIndex { get; private set; } 39 | 40 | private int _messageCountCache; 41 | 42 | 43 | public MessageBanner(object source, string sourceName, 44 | bool showMessageTypeCount = true, bool allowClearMessages = false) 45 | : this(null, source, sourceName, showMessageTypeCount, allowClearMessages) { } 46 | 47 | /// 48 | /// 消息横幅。 49 | /// 50 | /// 消息列表。 51 | /// 调用源。双击横幅时,作为打开的消息查看器的调用源。 52 | /// 调用源的名字。 53 | /// 是否显示各类型消息的计数。 54 | public MessageBanner(IList messages, object source, string sourceName, 55 | bool showMessageTypeCount = true, bool allowClearMessages = false) 56 | { 57 | _showMessageTypeCount = showMessageTypeCount; 58 | Messages = messages; 59 | Source = source; 60 | SourceName = sourceName; 61 | AllowClearMessages = allowClearMessages; 62 | 63 | style.flexDirection = FlexDirection.Row; 64 | style.paddingLeft = 4; 65 | style.paddingRight = 4; 66 | style.height = 20; 67 | 68 | TypeImage = EditorMessageUtility.NewImage(); 69 | Add(TypeImage); 70 | 71 | MessageLabel = new Label 72 | { 73 | style = 74 | { 75 | flexGrow = 1, 76 | flexShrink = 1, 77 | marginRight = 2, 78 | minWidth = 100, 79 | overflow = Overflow.Hidden, 80 | textOverflow = TextOverflow.Ellipsis, 81 | unityTextAlign = TextAnchor.MiddleLeft, 82 | unityFontDefinition = new StyleFontDefinition(EditorMessageUtility.GetMonospaceFontAsset()), 83 | //transitionDuration = new List 84 | //{ 85 | // new TimeValue(_messageContentTransitionDuration, TimeUnit.Millisecond) 86 | //}, 87 | //transitionTimingFunction = new List 88 | //{ 89 | // new EasingFunction(EasingMode.EaseInOut) 90 | //}, 91 | //transitionProperty = new List 92 | //{ 93 | // new StylePropertyName("scale"), 94 | //}, 95 | } 96 | }; 97 | Add(MessageLabel); 98 | 99 | InfoTypeImage = CreateMessageTypeImage(EditorMessageUtility.GetInfoIcon(true)); 100 | Add(InfoTypeImage); 101 | 102 | InfoCountLabel = CreateMessageTypeCountLabel(); 103 | Add(InfoCountLabel); 104 | 105 | WarningTypeImage = CreateMessageTypeImage(EditorMessageUtility.GetWarningIcon(true)); 106 | Add(WarningTypeImage); 107 | 108 | WarningCountLabel = CreateMessageTypeCountLabel(); 109 | Add(WarningCountLabel); 110 | 111 | ErrorTypeImage = CreateMessageTypeImage(EditorMessageUtility.GetErrorIcon(true)); 112 | Add(ErrorTypeImage); 113 | 114 | ErrorCountLabel = CreateMessageTypeCountLabel(); 115 | Add(ErrorCountLabel); 116 | 117 | RegisterCallback(OnClick); 118 | RegisterCallback(OnContextClick); 119 | 120 | InitializeMessageSwitch(); 121 | 122 | schedule.Execute(() => 123 | { 124 | if ((Messages?.Count ?? 0) != _messageCountCache) 125 | { 126 | Refresh(); 127 | } 128 | }).Every(200); 129 | } 130 | 131 | private Image CreateMessageTypeImage(Texture defaultIcon) 132 | { 133 | Image image = EditorMessageUtility.NewImage(defaultIcon, 134 | display: ShowMessageTypeCount ? DisplayStyle.Flex : DisplayStyle.None); 135 | return image; 136 | } 137 | 138 | private Label CreateMessageTypeCountLabel() 139 | { 140 | Label label = new Label 141 | { 142 | text = "0", 143 | style = 144 | { 145 | display = ShowMessageTypeCount ? DisplayStyle.Flex : DisplayStyle.None, 146 | //flexShrink = 0, 147 | marginLeft = -3, 148 | marginRight = -3, 149 | paddingLeft = 0, 150 | paddingRight = 0, 151 | overflow = Overflow.Hidden, 152 | unityTextAlign = TextAnchor.MiddleCenter, 153 | unityFontDefinition = new StyleFontDefinition(EditorMessageUtility.GetMonospaceFontAsset()), 154 | } 155 | }; 156 | return label; 157 | } 158 | 159 | 160 | public void SetMessages(IList messages) 161 | { 162 | Messages = messages; 163 | Refresh(); 164 | } 165 | 166 | public void Refresh() 167 | { 168 | CurrentMessageIndex = (Messages?.Count ?? 0) - 1; 169 | 170 | // ReSharper disable once PossibleNullReferenceException 171 | Message message = CurrentMessageIndex > -1 ? Messages[CurrentMessageIndex] : null; 172 | SetMessage(message); 173 | Messages.CountByType(out int infoCount, out int warningCount, out int errorCount); 174 | _messageCountCache = Messages?.Count ?? 0; 175 | SetMessageCount(MessageType.Info, infoCount); 176 | SetMessageCount(MessageType.Warning, warningCount); 177 | SetMessageCount(MessageType.Error, errorCount); 178 | RefreshMessageTypeCountDisplay(); 179 | } 180 | 181 | private void SetMessage(Message message) 182 | { 183 | TypeImage.image = message != null ? EditorMessageUtility.GetMessageTypeIcon(message.type) : null; 184 | MessageLabel.text = message?.message; 185 | MessageLabel.tooltip = message?.message; 186 | } 187 | 188 | private void SetMessageCount(MessageType messageType, int count) 189 | { 190 | string text = count > 999 ? "999+" : count.ToString(); 191 | bool inactive = count < 1; 192 | switch (messageType) 193 | { 194 | case MessageType.Info: 195 | InfoCountLabel.text = text; 196 | InfoTypeImage.image = EditorMessageUtility.GetInfoIcon(inactive); 197 | break; 198 | case MessageType.Warning: 199 | WarningCountLabel.text = text; 200 | WarningTypeImage.image = EditorMessageUtility.GetWarningIcon(inactive); 201 | break; 202 | case MessageType.Error: 203 | ErrorCountLabel.text = text; 204 | ErrorTypeImage.image = EditorMessageUtility.GetErrorIcon(inactive); 205 | break; 206 | default: 207 | throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null); 208 | } 209 | } 210 | 211 | private void RefreshMessageTypeCountDisplay() 212 | { 213 | DisplayStyle display = ShowMessageTypeCount ? DisplayStyle.Flex : DisplayStyle.None; 214 | InfoTypeImage.style.display = display; 215 | InfoCountLabel.style.display = display; 216 | WarningTypeImage.style.display = display; 217 | WarningCountLabel.style.display = display; 218 | ErrorTypeImage.style.display = display; 219 | ErrorCountLabel.style.display = display; 220 | } 221 | 222 | private void OnClick(ClickEvent evt) 223 | { 224 | if (evt.clickCount == 2 && Messages != null && Messages.Count > 0) 225 | { 226 | MessageViewer.Open(Messages, Source, SourceName, AllowClearMessages); 227 | } 228 | } 229 | 230 | private void OnContextClick(ContextClickEvent evt) 231 | { 232 | GenericMenu menu = new GenericMenu(); 233 | 234 | // Open Message Viewer 235 | menu.AddItem(new GUIContent("Open Message Viewer"), false, () => MessageViewer.Open(Messages, Source, SourceName)); 236 | 237 | menu.ShowAsContext(); 238 | } 239 | 240 | 241 | #region Message Transition 242 | 243 | private uint _messageSwitchInterval; 244 | /// 245 | /// 消息轮播时的切换间隔(毫秒)。 246 | /// 若为0,则不自动切换消息。 247 | /// 248 | public uint MessageSwitchInterval 249 | { 250 | get => _messageSwitchInterval; 251 | set 252 | { 253 | if (_messageSwitchInterval == value) 254 | { 255 | return; 256 | } 257 | 258 | bool prevDisabled = IsMessageSwitchDisabled(); 259 | _messageSwitchInterval = value; 260 | if (!IsMessageSwitchDisabled() && prevDisabled) 261 | { 262 | InitializeMessageSwitch(); 263 | } 264 | } 265 | } 266 | 267 | public bool IsMessageSwitchDisabled() 268 | { 269 | return MessageSwitchInterval < 1; 270 | } 271 | 272 | 273 | private void InitializeMessageSwitch() 274 | { 275 | if (IsMessageSwitchDisabled()) 276 | { 277 | return; 278 | } 279 | 280 | schedule.Execute(SwitchToNextMessage) 281 | .Every(MessageSwitchInterval) 282 | .StartingIn(MessageSwitchInterval) 283 | .Until(IsMessageSwitchDisabled); 284 | } 285 | 286 | private void SwitchToNextMessage() 287 | { 288 | if ((Messages?.Count ?? 0) < 1) 289 | { 290 | CurrentMessageIndex = -1; 291 | return; 292 | } 293 | 294 | CurrentMessageIndex++; 295 | // ReSharper disable once PossibleNullReferenceException 296 | if (CurrentMessageIndex == Messages.Count) 297 | { 298 | CurrentMessageIndex = 0; 299 | } 300 | 301 | SetMessage(Messages[CurrentMessageIndex]); 302 | } 303 | 304 | #endregion 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /Editor/MessageBanner.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b94be67a6f95fdd49a7cc040a78b5822 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MessageDetailsElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine.UIElements; 3 | 4 | namespace GBG.EditorMessages.Editor 5 | { 6 | public class MessageDetailsElement : VisualElement 7 | { 8 | private readonly MessageDetailsTabElement _messageTab; 9 | private readonly MessageDetailsTabElement _contextTab; 10 | private readonly MessageDetailsTabElement _customDataTab; 11 | private readonly Label _contentLabel; 12 | private Message _message; 13 | 14 | 15 | public MessageDetailsElement() 16 | { 17 | float iconSize = EditorMessageUtility.GlobalIconSize; 18 | 19 | style.flexDirection = FlexDirection.Row; 20 | style.flexGrow = 1; 21 | style.minHeight = iconSize * 3 + 2; 22 | 23 | 24 | #region Details Type Toggle 25 | 26 | // Type Toggle Container 27 | VisualElement tabContainer = new VisualElement 28 | { 29 | style = 30 | { 31 | backgroundColor = EditorMessageUtility.TabBackgroundColor, 32 | width = iconSize + 2, 33 | paddingLeft = 1, 34 | //paddingRight = 1, 35 | paddingTop = 1, 36 | paddingBottom = 1, 37 | } 38 | }; 39 | Add(tabContainer); 40 | 41 | _messageTab = new MessageDetailsTabElement(EditorMessageUtility.GetInfoIcon(), 42 | "Message", OnClickMessageTab); 43 | tabContainer.Add(_messageTab); 44 | 45 | _contextTab = new MessageDetailsTabElement(EditorMessageUtility.GetContextIcon(), 46 | "Context", OnClickContextTab); 47 | tabContainer.Add(_contextTab); 48 | 49 | _customDataTab = new MessageDetailsTabElement(EditorMessageUtility.GetCustomDataIcon(), 50 | "Custom Data", OnClickCustomDataTab); 51 | tabContainer.Add(_customDataTab); 52 | 53 | #endregion 54 | 55 | 56 | #region Details Content 57 | 58 | // Details Container 59 | VisualElement detailsContainer = new VisualElement 60 | { 61 | style = 62 | { 63 | flexGrow = 1, 64 | } 65 | }; 66 | Add(detailsContainer); 67 | 68 | // Message Details Scroll 69 | ScrollView detailsScrollView = new ScrollView(ScrollViewMode.Vertical); 70 | detailsContainer.Add(detailsScrollView); 71 | 72 | // Message Details Label 73 | _contentLabel = new Label 74 | { 75 | enableRichText = true, 76 | selection = { isSelectable = true, }, 77 | style = 78 | { 79 | flexGrow = 1, 80 | whiteSpace = WhiteSpace.Normal, 81 | } 82 | }; 83 | detailsScrollView.Add(_contentLabel); 84 | 85 | #endregion 86 | } 87 | 88 | public void SetMessage(Message message) 89 | { 90 | _message = message; 91 | 92 | MessageType messageType = message?.type ?? MessageType.Info; 93 | switch (messageType) 94 | { 95 | case MessageType.Info: 96 | _messageTab.Icon.image = EditorMessageUtility.GetInfoIcon(); 97 | break; 98 | case MessageType.Warning: 99 | _messageTab.Icon.image = EditorMessageUtility.GetWarningIcon(); 100 | break; 101 | case MessageType.Error: 102 | _messageTab.Icon.image = EditorMessageUtility.GetErrorIcon(); 103 | break; 104 | default: 105 | throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null); 106 | } 107 | 108 | _contextTab.style.display = string.IsNullOrEmpty(message?.context) 109 | ? DisplayStyle.None : DisplayStyle.Flex; 110 | _customDataTab.style.display = string.IsNullOrEmpty(message?.customData) 111 | ? DisplayStyle.None : DisplayStyle.Flex; 112 | 113 | OnClickMessageTab(); 114 | } 115 | 116 | 117 | private void OnClickMessageTab() 118 | { 119 | _contentLabel.text = _message?.message; 120 | 121 | _messageTab.style.backgroundColor = EditorMessageUtility.TabActiveColor; 122 | _contextTab.style.backgroundColor = EditorMessageUtility.TabInactiveColor; 123 | _customDataTab.style.backgroundColor = EditorMessageUtility.TabInactiveColor; 124 | } 125 | 126 | private void OnClickContextTab() 127 | { 128 | _contentLabel.text = _message?.context; 129 | 130 | _messageTab.style.backgroundColor = EditorMessageUtility.TabInactiveColor; 131 | _contextTab.style.backgroundColor = EditorMessageUtility.TabActiveColor; 132 | _customDataTab.style.backgroundColor = EditorMessageUtility.TabInactiveColor; 133 | } 134 | 135 | private void OnClickCustomDataTab() 136 | { 137 | _contentLabel.text = _message?.customData; 138 | 139 | _messageTab.style.backgroundColor = EditorMessageUtility.TabInactiveColor; 140 | _contextTab.style.backgroundColor = EditorMessageUtility.TabInactiveColor; 141 | _customDataTab.style.backgroundColor = EditorMessageUtility.TabActiveColor; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Editor/MessageDetailsElement.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6fad34712177c1645b0b1050b4104436 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MessageDetailsTabElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | 5 | namespace GBG.EditorMessages.Editor 6 | { 7 | public class MessageDetailsTabElement : VisualElement 8 | { 9 | public Image Icon { get; } 10 | 11 | 12 | public MessageDetailsTabElement(Texture texture, string tooltip, Action onClick) 13 | { 14 | float iconSize = EditorMessageUtility.GlobalIconSize; 15 | style.height = iconSize; 16 | this.tooltip = tooltip; 17 | 18 | Icon = EditorMessageUtility.NewImage(texture); 19 | Add(Icon); 20 | 21 | RegisterCallback(evt => onClick?.Invoke()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Editor/MessageDetailsTabElement.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3586a7e843fbab04dac05d16951caa7c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MessageElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.Assertions; 6 | using UnityEngine.UIElements; 7 | 8 | namespace GBG.EditorMessages.Editor 9 | { 10 | public class MessageElement : VisualElement 11 | { 12 | public Label LineNumberLabel { get; } 13 | public Image TypeImage { get; } 14 | public Label TimestampLabel { get; } 15 | public Label TimestampSeparatorLabel { get; } 16 | public Label MessageLabel { get; } 17 | public Image ContextImage { get; } 18 | public Image CustomDataImage { get; } 19 | 20 | public Message Message { get; private set; } 21 | public int LineNumber { get; private set; } = -1; 22 | public int LineNumberLabelWidth { get; set; } = -1; 23 | public bool ShowTimestamp { get; set; } = true; 24 | 25 | public event Action WantsToProcessCustomData; 26 | 27 | 28 | public MessageElement() 29 | { 30 | style.flexDirection = FlexDirection.Row; 31 | style.paddingLeft = 4; 32 | style.paddingRight = 4; 33 | style.minWidth = 100; 34 | 35 | LineNumberLabel = new Label 36 | { 37 | style = 38 | { 39 | flexShrink = 0, 40 | marginRight = 2, 41 | overflow = Overflow.Hidden, 42 | unityTextAlign = TextAnchor.MiddleRight, 43 | unityFontDefinition = new StyleFontDefinition(EditorMessageUtility.GetMonospaceFontAsset()), 44 | } 45 | }; 46 | Add(LineNumberLabel); 47 | 48 | TypeImage = EditorMessageUtility.NewImage(); 49 | Add(TypeImage); 50 | 51 | TimestampLabel = new Label 52 | { 53 | style = 54 | { 55 | paddingRight = 0, 56 | unityTextAlign = TextAnchor.MiddleCenter, 57 | unityFontDefinition = new StyleFontDefinition(EditorMessageUtility.GetMonospaceFontAsset()), 58 | } 59 | }; 60 | Add(TimestampLabel); 61 | 62 | TimestampSeparatorLabel = new Label 63 | { 64 | text = "|", 65 | style = 66 | { 67 | paddingLeft = 0, 68 | paddingRight = 4, 69 | unityTextAlign = TextAnchor.MiddleLeft, 70 | //unityFontDefinition = new StyleFontDefinition(ResCache.GetMonospaceFontAsset()), 71 | } 72 | }; 73 | Add(TimestampSeparatorLabel); 74 | 75 | MessageLabel = new Label 76 | { 77 | style = 78 | { 79 | flexGrow = 1, 80 | flexShrink = 1, 81 | overflow = Overflow.Hidden, 82 | textOverflow = TextOverflow.Ellipsis, 83 | unityTextAlign = TextAnchor.MiddleLeft, 84 | unityFontDefinition = new StyleFontDefinition(EditorMessageUtility.GetMonospaceFontAsset()), 85 | } 86 | }; 87 | Add(MessageLabel); 88 | 89 | ContextImage = EditorMessageUtility.NewImage(tooltip: "This message has context."); 90 | Add(ContextImage); 91 | 92 | CustomDataImage = EditorMessageUtility.NewImage(tooltip: "This message has custom data."); 93 | Add(CustomDataImage); 94 | 95 | RegisterCallback(OnClick); 96 | RegisterCallback(OnContextClick); 97 | } 98 | 99 | public void SetMessage(Message message, int lineNumber, int lineNumberLabelWidth = -1) 100 | { 101 | Assert.IsTrue(message != null); 102 | 103 | Message = message; 104 | LineNumber = lineNumber; 105 | LineNumberLabelWidth = lineNumberLabelWidth; 106 | TypeImage.image = EditorMessageUtility.GetMessageTypeIcon(message.type); 107 | MessageLabel.text = message.message; 108 | 109 | UpdateLineNumberLabel(); 110 | UpdateTimestampLabel(); 111 | UpdateContextImage(); 112 | UpdateCustomDataImage(); 113 | } 114 | 115 | private void UpdateLineNumberLabel() 116 | { 117 | if (LineNumber < 0) 118 | { 119 | LineNumberLabel.style.display = DisplayStyle.None; 120 | return; 121 | } 122 | 123 | LineNumberLabel.text = LineNumber.ToString(); 124 | LineNumberLabel.style.width = LineNumberLabelWidth > 0 ? LineNumberLabelWidth : StyleKeyword.Auto; 125 | LineNumberLabel.style.display = DisplayStyle.Flex; 126 | } 127 | 128 | private void UpdateTimestampLabel() 129 | { 130 | if (!ShowTimestamp) 131 | { 132 | TimestampLabel.style.display = DisplayStyle.None; 133 | TimestampSeparatorLabel.style.display = DisplayStyle.None; 134 | return; 135 | } 136 | 137 | TimestampLabel.text = new DateTime(Message.timestamp).ToString(CultureInfo.CurrentCulture); 138 | TimestampLabel.style.display = DisplayStyle.Flex; 139 | TimestampSeparatorLabel.style.display = DisplayStyle.Flex; 140 | } 141 | 142 | private void UpdateContextImage() 143 | { 144 | if (string.IsNullOrEmpty(Message.context)) 145 | { 146 | ContextImage.style.display = DisplayStyle.None; 147 | return; 148 | } 149 | 150 | ContextImage.image = EditorMessageUtility.GetContextIcon(); 151 | ContextImage.style.display = DisplayStyle.Flex; 152 | } 153 | 154 | private void UpdateCustomDataImage() 155 | { 156 | if (string.IsNullOrEmpty(Message.customData)) 157 | { 158 | CustomDataImage.style.display = DisplayStyle.None; 159 | return; 160 | } 161 | 162 | CustomDataImage.image = EditorMessageUtility.GetCustomDataIcon(); 163 | CustomDataImage.style.display = DisplayStyle.Flex; 164 | } 165 | 166 | private void OnClick(ClickEvent evt) 167 | { 168 | if (evt.clickCount == 1) 169 | { 170 | Message.TryPingContextObject(false); 171 | } 172 | else if (evt.clickCount == 2 && !string.IsNullOrEmpty(Message?.customData)) 173 | { 174 | if (WantsToProcessCustomData != null) 175 | { 176 | WantsToProcessCustomData(Message); 177 | } 178 | else 179 | { 180 | Debug.LogError($"Custom data handler is not registered: {Message}", Message.GetUnityContextObject()); 181 | } 182 | } 183 | } 184 | 185 | private void OnContextClick(ContextClickEvent evt) 186 | { 187 | GenericMenu menu = new GenericMenu(); 188 | 189 | // Copy Message 190 | menu.AddItem(new GUIContent("Copy Message"), false, () => EditorGUIUtility.systemCopyBuffer = Message.message); 191 | 192 | // Copy Context 193 | if (!string.IsNullOrEmpty(Message.context)) 194 | { 195 | menu.AddItem(new GUIContent("Copy Context"), false, () => EditorGUIUtility.systemCopyBuffer = Message.context); 196 | } 197 | 198 | // Show Context Object In Scene 199 | if (!Message.GetUnityContextObject() && Message.TryGetContextOwnerSceneAsset(out SceneAsset sceneAsset)) 200 | { 201 | menu.AddItem(new GUIContent("Reveal Context Object in Scene"), false, () => Message.TryPingContextObject(true)); 202 | } 203 | 204 | // Copy Custom Data 205 | if (!string.IsNullOrEmpty(Message.customData)) 206 | { 207 | menu.AddItem(new GUIContent("Copy Custom Data"), false, () => EditorGUIUtility.systemCopyBuffer = Message.customData); 208 | } 209 | 210 | menu.ShowAsContext(); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Editor/MessageElement.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 39ccb08baee2b914eaa72de32808e012 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MessageTypeToggle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor.UIElements; 3 | using UnityEngine; 4 | using UnityEngine.UIElements; 5 | 6 | namespace GBG.EditorMessages.Editor 7 | { 8 | public class MessageTypeToggle : ToolbarToggle 9 | { 10 | private readonly Image _typeImage; 11 | private Texture _icon; 12 | private Texture _iconInactive; 13 | 14 | 15 | public MessageTypeToggle(bool value) 16 | { 17 | base.value = value; 18 | 19 | float iconSize = EditorMessageUtility.GlobalIconSize; 20 | 21 | _typeImage = EditorMessageUtility.NewImage(); 22 | Insert(0, _typeImage); 23 | } 24 | 25 | public void SetMessageType(MessageType messageType, int messageCount) 26 | { 27 | switch (messageType) 28 | { 29 | case MessageType.Info: 30 | _icon = EditorMessageUtility.GetInfoIcon(); 31 | _iconInactive = EditorMessageUtility.GetInfoIcon(true); 32 | break; 33 | 34 | case MessageType.Warning: 35 | _icon = EditorMessageUtility.GetWarningIcon(); 36 | _iconInactive = EditorMessageUtility.GetWarningIcon(true); 37 | break; 38 | 39 | case MessageType.Error: 40 | _icon = EditorMessageUtility.GetErrorIcon(); 41 | _iconInactive = EditorMessageUtility.GetErrorIcon(true); 42 | break; 43 | 44 | default: 45 | throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null); 46 | } 47 | 48 | SetMessageCount(messageCount); 49 | } 50 | 51 | public void SetMessageCount(int count) 52 | { 53 | text = count > 999 ? "999+" : count.ToString(); 54 | _typeImage.image = count > 0 ? _icon : _iconInactive; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Editor/MessageTypeToggle.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 684378b813b5247438b9d41b393a1d43 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MessageViewer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | using UnityEditor; 5 | using UnityEditor.UIElements; 6 | using UnityEngine; 7 | using UnityEngine.UIElements; 8 | using UObject = UnityEngine.Object; 9 | 10 | namespace GBG.EditorMessages.Editor 11 | { 12 | public class MessageViewer : EditorWindow 13 | { 14 | private static MessageViewer _sourcelessInstance; 15 | private static Dictionary _sourcedInstanceDict; 16 | 17 | /// 18 | /// 打开消息查看器窗口。 19 | /// 20 | /// 消息列表。 21 | /// 调用源。当调用源变为null时,消息查看器窗口会自动关闭。若传入null,则消息查看器窗口不会自动关闭。 22 | /// 调用源的名字。会出现在消息查看器窗口标题中。 23 | /// 24 | public static MessageViewer Open(IList messages, object source, string sourceName, 25 | bool allowClearMessages = true) 26 | { 27 | if (source == null) 28 | { 29 | if (!_sourcelessInstance) 30 | { 31 | _sourcelessInstance = CreateInstance(); 32 | _sourcelessInstance.titleContent = new GUIContent("Message Viewer"); 33 | _sourcelessInstance._sourceless = true; 34 | } 35 | 36 | _sourcelessInstance._showClearButton = allowClearMessages; 37 | _sourcelessInstance.SetMessages(messages); 38 | _sourcelessInstance.Show(); 39 | _sourcelessInstance.Focus(); 40 | return _sourcelessInstance; 41 | } 42 | 43 | _sourcedInstanceDict ??= new Dictionary(); 44 | if (!_sourcedInstanceDict.TryGetValue(source, out MessageViewer viewer) || !viewer) 45 | { 46 | viewer = CreateInstance(); 47 | _sourcedInstanceDict[source] = viewer; 48 | } 49 | 50 | viewer.titleContent = new GUIContent($"Message Viewer({sourceName ?? source})"); 51 | viewer.Source = source; 52 | viewer._showClearButton = allowClearMessages; 53 | viewer.SetMessages(messages); 54 | viewer.Show(); 55 | viewer.Focus(); 56 | return viewer; 57 | } 58 | 59 | public static MessageViewer Open(object source, string sourceName, bool allowClearMessages = true) 60 | { 61 | return Open(null, source, sourceName, allowClearMessages); 62 | } 63 | 64 | 65 | private bool _createGuiEnd; 66 | private ToolbarToggle _lineNumberToggle; 67 | private ToolbarToggle _timestampToggle; 68 | private DropdownField _tagDropdown; 69 | private ToolbarSearchField _searchField; 70 | private Image _regexErrorImage; 71 | private ToolbarToggle _regexToggle; 72 | private MessageTypeToggle _infoMessageToggle; 73 | private MessageTypeToggle _warningMessageToggle; 74 | private MessageTypeToggle _errorMessageToggle; 75 | private ToolbarButton _clearButton; 76 | private ListView _messageListView; 77 | private MessageDetailsElement _messageDetailsElement; 78 | 79 | public object Source { get; private set; } 80 | public IList Messages { get; private set; } 81 | 82 | private bool _sourceless; 83 | private int _messageCountCache; 84 | private readonly List _tagList = new List() { TagAll }; 85 | private readonly List _filteredMessageList = new List(); 86 | private Action _customDataHandler; 87 | 88 | 89 | #region Serialized Fields 90 | 91 | public const string TagAll = "All"; 92 | 93 | [SerializeField] 94 | [HideInInspector] 95 | private bool _showLineNumber; 96 | [SerializeField] 97 | [HideInInspector] 98 | private bool _showTimestamp; 99 | [SerializeField] 100 | [HideInInspector] 101 | private string _selectedTag = TagAll; 102 | [SerializeField] 103 | [HideInInspector] 104 | private string _searchPattern = string.Empty; 105 | [SerializeField] 106 | [HideInInspector] 107 | private bool _useRegex; 108 | [SerializeField] 109 | [HideInInspector] 110 | private bool _showInfoMessages = true; 111 | [SerializeField] 112 | [HideInInspector] 113 | private bool _showWarningMessages = true; 114 | [SerializeField] 115 | [HideInInspector] 116 | private bool _showErrorMessage = true; 117 | [SerializeField] 118 | [HideInInspector] 119 | private bool _showClearButton; 120 | 121 | #endregion 122 | 123 | 124 | #region Unity Messages 125 | 126 | private void OnEnable() 127 | { 128 | minSize = new Vector2(250, 150); 129 | 130 | _createGuiEnd = false; 131 | if (_sourceless) // Used for restore status after reload assemeblies 132 | { 133 | if (!_sourcelessInstance) 134 | { 135 | _sourcelessInstance = this; 136 | } 137 | else if (_sourcelessInstance != this) 138 | { 139 | Debug.LogError("_sourcelessInstance != this", this); 140 | } 141 | } 142 | } 143 | 144 | private void CreateGUI() 145 | { 146 | float iconSize = EditorMessageUtility.GlobalIconSize; 147 | 148 | 149 | #region Toolbar 150 | 151 | // Toolbar 152 | Toolbar toolbar = new Toolbar(); 153 | rootVisualElement.Add(toolbar); 154 | 155 | // Line Number Toggle 156 | _lineNumberToggle = new ToolbarToggle 157 | { 158 | value = _showLineNumber, 159 | text = "#", 160 | tooltip = "Show Line Number", 161 | style = 162 | { 163 | flexShrink = 0, 164 | } 165 | }; 166 | _lineNumberToggle.RegisterValueChangedCallback(OnLineNumberToggleChanged); 167 | toolbar.Add(_lineNumberToggle); 168 | 169 | // Timestamp Toggle 170 | _timestampToggle = new ToolbarToggle 171 | { 172 | value = _showTimestamp, 173 | text = "Ts", 174 | tooltip = "Show Timestamp", 175 | style = 176 | { 177 | flexShrink = 0, 178 | } 179 | }; 180 | _timestampToggle.RegisterValueChangedCallback(OnTimestampToggleChanged); 181 | toolbar.Add(_timestampToggle); 182 | 183 | // Tag 184 | _tagDropdown = new DropdownField(_tagList, _selectedTag) 185 | { 186 | tooltip = "Filter by Tag", 187 | formatSelectedValueCallback = item => string.IsNullOrWhiteSpace(item) ? TagAll : item, 188 | style = 189 | { 190 | flexShrink = 0, 191 | } 192 | }; 193 | _tagDropdown.RegisterValueChangedCallback(OnSelectedTagChanged); 194 | _tagDropdown.Q(className: DropdownField.inputUssClassName).style.minWidth = StyleKeyword.Auto; 195 | toolbar.Add(_tagDropdown); 196 | 197 | // Search Field 198 | _searchField = new ToolbarSearchField 199 | { 200 | value = _searchPattern, 201 | style = 202 | { 203 | flexGrow = 1, 204 | flexShrink = 1, 205 | marginRight = 2, 206 | } 207 | }; 208 | _searchField.RegisterValueChangedCallback(OnSearchPatternChanged); 209 | toolbar.Add(_searchField); 210 | 211 | // Regex Error Image 212 | _regexErrorImage = EditorMessageUtility.NewImage(EditorMessageUtility.GetErrorIcon(), 213 | display: DisplayStyle.None); 214 | toolbar.Add(_regexErrorImage); 215 | 216 | // Regex Toggle 217 | _regexToggle = new ToolbarToggle 218 | { 219 | value = _useRegex, 220 | text = ".*", 221 | tooltip = "Use Regular Expression", 222 | style = 223 | { 224 | marginLeft = 2, 225 | } 226 | }; 227 | _regexToggle.RegisterValueChangedCallback(OnRegexToggleChanged); 228 | toolbar.Add(_regexToggle); 229 | 230 | // Message Toggle Container 231 | VisualElement typeToggleContainer = new VisualElement 232 | { 233 | style = 234 | { 235 | flexDirection = FlexDirection.Row, 236 | flexShrink = 0, 237 | } 238 | }; 239 | toolbar.Add(typeToggleContainer); 240 | 241 | // Info Message Toggle 242 | _infoMessageToggle = new MessageTypeToggle(_showInfoMessages); 243 | _infoMessageToggle.SetMessageType(MessageType.Info, 0); 244 | _infoMessageToggle.RegisterValueChangedCallback(OnMessageTypeToggleChanged); 245 | typeToggleContainer.Add(_infoMessageToggle); 246 | 247 | // Warning Message Toggle 248 | _warningMessageToggle = new MessageTypeToggle(_showWarningMessages); 249 | _warningMessageToggle.SetMessageType(MessageType.Warning, 0); 250 | _warningMessageToggle.RegisterValueChangedCallback(OnMessageTypeToggleChanged); 251 | typeToggleContainer.Add(_warningMessageToggle); 252 | 253 | // Error Message Toggle 254 | _errorMessageToggle = new MessageTypeToggle(_showErrorMessage); 255 | _errorMessageToggle.SetMessageType(MessageType.Error, 0); 256 | _errorMessageToggle.RegisterValueChangedCallback(OnMessageTypeToggleChanged); 257 | typeToggleContainer.Add(_errorMessageToggle); 258 | 259 | // Clear Button 260 | _clearButton = new ToolbarImageButton(EditorMessageUtility.GetClearIcon(), ClearMessages) 261 | { 262 | tooltip = "Clear All Messages", 263 | style = 264 | { 265 | width = 22, 266 | flexShrink = 0, 267 | display = _showClearButton ? DisplayStyle.Flex : DisplayStyle.None, 268 | } 269 | }; 270 | toolbar.Add(_clearButton); 271 | 272 | #endregion 273 | 274 | 275 | // Vertical Split View 276 | TwoPaneSplitView splitView = new TwoPaneSplitView(1, 50, TwoPaneSplitViewOrientation.Vertical); 277 | rootVisualElement.Add(splitView); 278 | 279 | // Message List Container 280 | VisualElement messageListContainer = new VisualElement 281 | { 282 | name = "message-list-container", 283 | style = 284 | { 285 | minHeight = 50, 286 | } 287 | }; 288 | splitView.Add(messageListContainer); 289 | 290 | // Message Details Container 291 | VisualElement messageDetailsContainer = new VisualElement 292 | { 293 | name = "message-details-container", 294 | style = 295 | { 296 | flexDirection = FlexDirection.Row, 297 | minHeight = 50, 298 | } 299 | }; 300 | splitView.Add(messageDetailsContainer); 301 | 302 | 303 | // Message List View 304 | _messageListView = new ListView(_filteredMessageList) 305 | { 306 | makeItem = MakeMessageElement, 307 | bindItem = BindMessageElement, 308 | unbindItem = UnbindMessageElement, 309 | showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly, 310 | style = 311 | { 312 | flexGrow = 1, 313 | } 314 | }; 315 | _messageListView.selectionChanged += OnSelectedMessageChanged; 316 | messageListContainer.Add(_messageListView); 317 | 318 | // Message Details View 319 | _messageDetailsElement = new MessageDetailsElement(); 320 | messageDetailsContainer.Add(_messageDetailsElement); 321 | 322 | _createGuiEnd = true; 323 | Refresh(); 324 | } 325 | 326 | private void Update() 327 | { 328 | if ((Messages?.Count ?? 0) != _messageCountCache) 329 | { 330 | Refresh(); 331 | } 332 | 333 | TryClose(); 334 | } 335 | 336 | private void OnDisable() 337 | { 338 | if (Source != null) 339 | { 340 | _sourcedInstanceDict.Remove(Source); 341 | } 342 | } 343 | 344 | private void ShowButton(Rect pos) 345 | { 346 | if (GUI.Button(pos, EditorGUIUtility.IconContent("_Help"), GUI.skin.FindStyle("IconButton"))) 347 | { 348 | Application.OpenURL("https://github.com/SolarianZ/UnityEditorMessages"); 349 | } 350 | } 351 | 352 | #endregion 353 | 354 | 355 | public void SetMessages(IList messages) 356 | { 357 | Messages = messages; 358 | ClearFilters(false); 359 | Refresh(); 360 | } 361 | 362 | public void ClearFilters(bool clearTypeFilter) 363 | { 364 | if (!_createGuiEnd) 365 | { 366 | _selectedTag = TagAll; 367 | _searchPattern = string.Empty; 368 | if (clearTypeFilter) 369 | { 370 | _showInfoMessages = true; 371 | _showWarningMessages = true; 372 | _showErrorMessage = true; 373 | } 374 | 375 | return; 376 | } 377 | 378 | _tagDropdown.value = TagAll; 379 | _searchField.value = string.Empty; 380 | if (clearTypeFilter) 381 | { 382 | _infoMessageToggle.value = true; 383 | _warningMessageToggle.value = true; 384 | _errorMessageToggle.value = true; 385 | } 386 | } 387 | 388 | public void SetCustomDataHandler(Action handler) 389 | { 390 | _customDataHandler = handler; 391 | } 392 | 393 | public void Refresh() 394 | { 395 | if (!_createGuiEnd) 396 | { 397 | return; 398 | } 399 | 400 | _tagList.Clear(); 401 | HashSet tagSet = Messages.CollectTags(); 402 | if (tagSet != null) 403 | { 404 | foreach (string tag in tagSet) 405 | { 406 | if (string.IsNullOrWhiteSpace(tag) || 407 | string.Equals(tag, TagAll, StringComparison.OrdinalIgnoreCase)) 408 | { 409 | continue; 410 | } 411 | 412 | _tagList.Add(tag); 413 | } 414 | _tagList.Sort(); 415 | } 416 | _tagList.Insert(0, TagAll); 417 | 418 | Messages.CountByType(out int infoCount, out int warningCount, out int errorCount); 419 | _messageCountCache = Messages?.Count ?? 0; 420 | _infoMessageToggle.SetMessageCount(infoCount); 421 | _warningMessageToggle.SetMessageCount(warningCount); 422 | _errorMessageToggle.SetMessageCount(errorCount); 423 | 424 | CalcMaxLineNumberLabelWidth(); 425 | FilterMessages(); 426 | } 427 | 428 | private void FilterMessages() 429 | { 430 | _messageListView.ClearSelection(); 431 | _filteredMessageList.Clear(); 432 | if (Messages == null) 433 | { 434 | RebuildMessageListView(); 435 | return; 436 | } 437 | 438 | _regexErrorImage.tooltip = null; 439 | _regexErrorImage.style.display = DisplayStyle.None; 440 | for (int i = 0; i < Messages.Count; i++) 441 | { 442 | Message message = Messages[i]; 443 | if (!TestMessageType(message) || !TestMessageTag(message) || !TestMessageSearchPattern(message)) 444 | { 445 | continue; 446 | } 447 | 448 | _filteredMessageList.Add(message); 449 | } 450 | 451 | RebuildMessageListView(); 452 | } 453 | 454 | private bool TestMessageType(Message message) 455 | { 456 | switch (message.type) 457 | { 458 | case MessageType.Info: 459 | return _showInfoMessages; 460 | 461 | case MessageType.Warning: 462 | return _showWarningMessages; 463 | 464 | case MessageType.Error: 465 | return _showErrorMessage; 466 | 467 | default: 468 | throw new ArgumentOutOfRangeException(nameof(message.type), message.type, null); 469 | } 470 | 471 | } 472 | 473 | private bool TestMessageTag(Message message) 474 | { 475 | return string.IsNullOrWhiteSpace(_selectedTag) || 476 | string.Equals(_selectedTag, TagAll, StringComparison.OrdinalIgnoreCase) || 477 | string.Equals(_selectedTag, message.tag, StringComparison.OrdinalIgnoreCase); 478 | } 479 | 480 | private bool TestMessageSearchPattern(Message message) 481 | { 482 | bool noSearchPattern = string.IsNullOrEmpty(_searchPattern); 483 | if (noSearchPattern) 484 | { 485 | return true; 486 | } 487 | 488 | if (string.IsNullOrEmpty(message.message)) 489 | { 490 | return false; 491 | } 492 | 493 | if (_useRegex) 494 | { 495 | try 496 | { 497 | return Regex.IsMatch(message.message, _searchPattern, RegexOptions.IgnoreCase); 498 | } 499 | catch (Exception ex) 500 | { 501 | _regexErrorImage.tooltip = ex.Message; 502 | _regexErrorImage.style.display = DisplayStyle.Flex; 503 | return false; 504 | } 505 | } 506 | 507 | return message.message.Contains(_searchPattern, StringComparison.OrdinalIgnoreCase); 508 | } 509 | 510 | private void ClearMessages() 511 | { 512 | Messages?.Clear(); 513 | Refresh(); 514 | } 515 | 516 | private void TryClose() 517 | { 518 | if (_sourcelessInstance == this) 519 | { 520 | return; 521 | } 522 | 523 | if (Source == null) 524 | { 525 | Close(); 526 | return; 527 | } 528 | 529 | if (Source is UObject unitySource && !unitySource) 530 | { 531 | Close(); 532 | } 533 | } 534 | 535 | 536 | #region List View 537 | 538 | private int _maxLineNumberWidth; 539 | 540 | 541 | private void RebuildMessageListView() 542 | { 543 | _messageListView.Rebuild(); 544 | if (_messageListView.itemsSource.Count == 0) 545 | { 546 | _messageListView.Q