com.wallstop-studios.dxmessaging 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +8 -8
  3. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +171 -13
  4. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +391 -96
  5. package/Editor/CustomEditors/MessageAwareComponentInspectorView.cs +279 -0
  6. package/Editor/CustomEditors/MessageAwareComponentInspectorView.cs.meta +11 -0
  7. package/Editor/DxMessagingEditorPalette.cs +66 -0
  8. package/Editor/DxMessagingEditorPalette.cs.meta +11 -0
  9. package/Editor/DxMessagingEditorTheme.cs +200 -0
  10. package/Editor/DxMessagingEditorTheme.cs.meta +11 -0
  11. package/Editor/Icons/dxmessaging-icon-256.png +0 -0
  12. package/Editor/Icons/dxmessaging-icon-256.png.meta +156 -0
  13. package/Editor/Icons/dxmessaging-icon-32.png +0 -0
  14. package/Editor/Icons/dxmessaging-icon-32.png.meta +156 -0
  15. package/Editor/Icons/dxmessaging-icon-48.png +0 -0
  16. package/Editor/Icons/dxmessaging-icon-48.png.meta +156 -0
  17. package/Editor/Icons.meta +8 -0
  18. package/Editor/Settings/DxMessagingSettingsProvider.cs +368 -33
  19. package/Editor/Testing/MessagingComponentEditorHarness.cs +11 -4
  20. package/Editor/Theme/DxMessagingTheme.uss +255 -0
  21. package/Editor/Theme/DxMessagingTheme.uss.meta +11 -0
  22. package/Editor/Theme/DxTokens.uss +90 -0
  23. package/Editor/Theme/DxTokens.uss.meta +11 -0
  24. package/Editor/Theme.meta +8 -0
  25. package/Editor/Windows/DxMessagingFlowGraphWindow.cs +5513 -0
  26. package/Editor/Windows/DxMessagingFlowGraphWindow.cs.meta +11 -0
  27. package/Editor/Windows/DxMessagingMessageMonitorWindow.cs +2288 -0
  28. package/Editor/Windows/DxMessagingMessageMonitorWindow.cs.meta +11 -0
  29. package/Editor/Windows.meta +8 -0
  30. package/README.md +84 -35
  31. package/Runtime/AssemblyInfo.cs +1 -0
  32. package/Runtime/Core/Diagnostics/MessageEmissionData.cs +29 -1
  33. package/Runtime/Core/Extensions/MessageExtensions.cs +4 -5
  34. package/Runtime/Core/Helper/MessageCache.cs +56 -23
  35. package/Runtime/Core/Internal/FlatDispatch.cs +32 -0
  36. package/Runtime/Core/Internal/TypedSlots.cs +3 -7
  37. package/Runtime/Core/MessageBus/MessageBus.cs +577 -142
  38. package/Runtime/Core/MessageBus/RegistrationLog.cs +25 -12
  39. package/Runtime/Core/MessageHandler.cs +774 -786
  40. package/Runtime/Core/MessageRegistrationHandle.cs +19 -5
  41. package/Runtime/Core/MessageRegistrationToken.cs +1080 -520
  42. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.cs +173 -0
  43. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.cs.meta +11 -0
  44. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.unity +424 -0
  45. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.unity.meta +7 -0
  46. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingReceiver.cs +185 -0
  47. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingReceiver.cs.meta +11 -0
  48. package/Samples~/Diagnostics Tooling Exerciser/Messages.cs +46 -0
  49. package/Samples~/Diagnostics Tooling Exerciser/Messages.cs.meta +11 -0
  50. package/Samples~/Diagnostics Tooling Exerciser/README.md +53 -0
  51. package/Samples~/Diagnostics Tooling Exerciser/README.md.meta +7 -0
  52. package/Samples~/Diagnostics Tooling Exerciser/WallstopStudios.DxMessaging.DiagnosticsToolingExerciser.Sample.asmdef +13 -0
  53. package/Samples~/Diagnostics Tooling Exerciser/WallstopStudios.DxMessaging.DiagnosticsToolingExerciser.Sample.asmdef.meta +7 -0
  54. package/Samples~/Diagnostics Tooling Exerciser.meta +8 -0
  55. package/Samples~/Mini Combat/README.md +1 -1
  56. package/Samples~/Mini Combat/Walkthrough.md +3 -3
  57. package/docs/images/DxMessaging-banner.svg +79 -0
  58. package/docs/images/DxMessaging-banner.svg.meta +53 -0
  59. package/docs/images.meta +8 -0
  60. package/docs.meta +8 -0
  61. package/package.json +13 -4
@@ -0,0 +1,2288 @@
1
+ #if UNITY_EDITOR
2
+ namespace DxMessaging.Editor.Windows
3
+ {
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.Linq;
7
+ using System.Text;
8
+ using Core;
9
+ using Core.Diagnostics;
10
+ using Core.MessageBus;
11
+ using Core.Messages;
12
+ using DxMessaging.Editor;
13
+ using DxMessaging.Editor.Testing;
14
+ using DxMessaging.Unity;
15
+ using UnityEditor;
16
+ using UnityEditor.SceneManagement;
17
+ using UnityEngine;
18
+ using UnityEngine.UIElements;
19
+
20
+ public sealed class DxMessagingMessageMonitorWindow : EditorWindow
21
+ {
22
+ internal const string RootClassName = "dxmessaging-monitor";
23
+ internal const string ToolbarClassName = "dxmessaging-monitor-toolbar";
24
+ internal const string RowClassName = "dxmessaging-monitor-row";
25
+ internal const string StatusLabelName = "dxmessaging-monitor-status";
26
+ internal const string FilterFieldName = "dxmessaging-monitor-filter";
27
+ internal const string ActiveFilterSummaryName = "dxmessaging-monitor-active-filter";
28
+ internal const string ActiveFilterSummaryLabelName =
29
+ "dxmessaging-monitor-active-filter-label";
30
+ internal const string ActiveFilterTokenScrollViewName =
31
+ "dxmessaging-monitor-active-filter-token-scroll";
32
+ internal const string ActiveFilterTokenClassName =
33
+ "dxmessaging-monitor-active-filter-token";
34
+ internal const string ActiveFilterClearButtonName =
35
+ "dxmessaging-monitor-active-filter-clear";
36
+ internal const string RefreshButtonName = "dxmessaging-monitor-refresh";
37
+ internal const string ExportButtonName = "dxmessaging-monitor-export";
38
+ internal const string ContentContainerName = "dxmessaging-monitor-content";
39
+ internal const string MessageSectionName = "dxmessaging-monitor-message-section";
40
+ internal const string EmptyStateLabelName = "dxmessaging-monitor-empty";
41
+ internal const string EmptyStateTitleLabelName = "dxmessaging-monitor-empty-title";
42
+ internal const string MessageTypeLabelName = "dxmessaging-monitor-message-type";
43
+ internal const string RouteKindLabelName = "dxmessaging-monitor-route-kind";
44
+ internal const string ContextLabelName = "dxmessaging-monitor-context";
45
+ internal const string StackTraceLabelName = "dxmessaging-monitor-stack";
46
+ internal const string DetailsPaneName = "dxmessaging-monitor-details";
47
+ internal const string DetailsTypeLabelName = "dxmessaging-monitor-details-type";
48
+ internal const string DetailsContextLabelName = "dxmessaging-monitor-details-context";
49
+ internal const string DetailsStackTraceLabelName = "dxmessaging-monitor-details-stack";
50
+ internal const string VisibleMessageTypeLanesName =
51
+ "dxmessaging-monitor-message-type-lanes";
52
+ internal const string VisibleMessageTypeLaneScrollViewName =
53
+ "dxmessaging-monitor-message-type-lane-scroll";
54
+ internal const string VisibleMessageTypeLaneRowClassName =
55
+ "dxmessaging-monitor-message-type-lane-row";
56
+ internal const string VisibleMessageTypeLanesSummaryLabelName =
57
+ "dxmessaging-monitor-message-type-lanes-summary";
58
+ internal const string VisibleMessageTypeLaneTypeLabelName =
59
+ "dxmessaging-monitor-message-type-lane-type";
60
+ internal const string VisibleMessageTypeLaneSummaryLabelName =
61
+ "dxmessaging-monitor-message-type-lane-summary";
62
+ internal const string VisibleMessageTypeLaneContextsLabelName =
63
+ "dxmessaging-monitor-message-type-lane-contexts";
64
+ internal const string VisibleMessageTypeLaneFilterButtonName =
65
+ "dxmessaging-monitor-message-type-lane-filter";
66
+ internal const string VisibleContextLanesName = "dxmessaging-monitor-context-lanes";
67
+ internal const string VisibleContextLaneScrollViewName =
68
+ "dxmessaging-monitor-context-lane-scroll";
69
+ internal const string VisibleContextLaneRowClassName =
70
+ "dxmessaging-monitor-context-lane-row";
71
+ internal const string VisibleContextLanesSummaryLabelName =
72
+ "dxmessaging-monitor-context-lanes-summary";
73
+ internal const string VisibleContextLaneContextLabelName =
74
+ "dxmessaging-monitor-context-lane-context";
75
+ internal const string VisibleContextLaneSummaryLabelName =
76
+ "dxmessaging-monitor-context-lane-summary";
77
+ internal const string VisibleContextLaneMessagesLabelName =
78
+ "dxmessaging-monitor-context-lane-messages";
79
+ internal const string VisibleContextLaneFilterButtonName =
80
+ "dxmessaging-monitor-context-lane-filter";
81
+ internal const string ComponentPanelName = "dxmessaging-monitor-components";
82
+ internal const string ComponentScrollViewName = "dxmessaging-monitor-component-scroll";
83
+ internal const string ComponentRowClassName = "dxmessaging-monitor-component-row";
84
+ internal const string ComponentNameLabelName = "dxmessaging-monitor-component-name";
85
+ internal const string ComponentSummaryLabelName = "dxmessaging-monitor-component-summary";
86
+ internal const string ComponentProviderLabelName = "dxmessaging-monitor-component-provider";
87
+ internal const string ComponentWarningLabelName = "dxmessaging-monitor-component-warning";
88
+ internal const string ComponentEmptyStateLabelName = "dxmessaging-monitor-component-empty";
89
+
90
+ private const string Title = "Message Monitor";
91
+
92
+ private string _filterText = string.Empty;
93
+ private int _selectedEntryIndex;
94
+ private MessageMonitorSnapshot _currentSnapshot = MessageMonitorSnapshot.Unavailable(
95
+ "No message monitor snapshot has been captured yet."
96
+ );
97
+ private IReadOnlyList<ComponentMonitorEntry> _currentComponents =
98
+ Array.Empty<ComponentMonitorEntry>();
99
+
100
+ [MenuItem("Tools/Wallstop Studios/DxMessaging/Message Monitor")]
101
+ public static void Open()
102
+ {
103
+ DxMessagingMessageMonitorWindow window = GetWindow<DxMessagingMessageMonitorWindow>();
104
+ window.titleContent = new GUIContent(Title, DxMessagingEditorTheme.LoadIcon());
105
+ window.minSize = new Vector2(420, 320);
106
+ window.Refresh();
107
+ }
108
+
109
+ private void CreateGUI()
110
+ {
111
+ titleContent = new GUIContent(Title, DxMessagingEditorTheme.LoadIcon());
112
+ Refresh();
113
+ }
114
+
115
+ private void Refresh()
116
+ {
117
+ MessageMonitorSnapshot snapshot = MessageHandler.MessageBus is MessageBus messageBus
118
+ ? CaptureSnapshot(messageBus)
119
+ : MessageMonitorSnapshot.Unavailable(
120
+ "The active global bus is not the default DxMessaging MessageBus."
121
+ );
122
+ IReadOnlyList<ComponentMonitorEntry> components = CaptureComponentSnapshots();
123
+ _currentSnapshot = snapshot;
124
+ _currentComponents = components;
125
+ BuildMonitorUi(
126
+ rootVisualElement,
127
+ snapshot,
128
+ new MessageMonitorViewState(_filterText, _selectedEntryIndex),
129
+ HandleFilterChanged,
130
+ HandleSelectedEntryChanged,
131
+ Refresh,
132
+ exportText => EditorGUIUtility.systemCopyBuffer = exportText,
133
+ components
134
+ );
135
+ }
136
+
137
+ private void HandleFilterChanged(string filterText)
138
+ {
139
+ string normalizedFilterText = filterText ?? string.Empty;
140
+ if (string.Equals(_filterText, normalizedFilterText, StringComparison.Ordinal))
141
+ {
142
+ return;
143
+ }
144
+
145
+ _filterText = normalizedFilterText;
146
+ _selectedEntryIndex = 0;
147
+ RefreshCurrentSnapshotContent();
148
+ }
149
+
150
+ private void HandleSelectedEntryChanged(int selectedEntryIndex)
151
+ {
152
+ int normalizedSelectedEntryIndex = Math.Max(0, selectedEntryIndex);
153
+ if (_selectedEntryIndex == normalizedSelectedEntryIndex)
154
+ {
155
+ return;
156
+ }
157
+
158
+ _selectedEntryIndex = normalizedSelectedEntryIndex;
159
+ RefreshCurrentSnapshotContent();
160
+ }
161
+
162
+ private void RefreshCurrentSnapshotContent()
163
+ {
164
+ VisualElement messageSection = rootVisualElement.Q<VisualElement>(MessageSectionName);
165
+ Label status = rootVisualElement.Q<Label>(StatusLabelName);
166
+ if (messageSection == null || status == null)
167
+ {
168
+ Refresh();
169
+ return;
170
+ }
171
+
172
+ MessageMonitorViewState viewState = new(_filterText, _selectedEntryIndex);
173
+ IReadOnlyList<MessageMonitorEntry> filteredEntries = FilterEntries(
174
+ _currentSnapshot.Entries,
175
+ viewState.FilterText
176
+ );
177
+ status.text = CreateStatusText(_currentSnapshot, filteredEntries.Count);
178
+ void RequestFilterChange(string filterText)
179
+ {
180
+ TextField filter = rootVisualElement.Q<TextField>(FilterFieldName);
181
+ if (filter != null)
182
+ {
183
+ filter.value = filterText ?? string.Empty;
184
+ return;
185
+ }
186
+
187
+ HandleFilterChanged(filterText);
188
+ }
189
+
190
+ RenderMessageSection(
191
+ messageSection,
192
+ _currentSnapshot,
193
+ filteredEntries,
194
+ viewState,
195
+ HandleSelectedEntryChanged,
196
+ RequestFilterChange
197
+ );
198
+ }
199
+
200
+ internal static MessageMonitorSnapshot CaptureSnapshot(MessageBus messageBus)
201
+ {
202
+ if (messageBus == null)
203
+ {
204
+ throw new ArgumentNullException(nameof(messageBus));
205
+ }
206
+
207
+ IReadOnlyList<MessageMonitorEntry> entries = messageBus
208
+ ._emissionBuffer.Reverse()
209
+ .Select(MessageMonitorEntry.FromEmission)
210
+ .ToArray();
211
+
212
+ return new MessageMonitorSnapshot(
213
+ messageBus.DiagnosticsMode,
214
+ IMessageBus.GlobalMessageBufferSize,
215
+ entries
216
+ );
217
+ }
218
+
219
+ internal static void BuildMonitorUi(VisualElement root, MessageMonitorSnapshot snapshot)
220
+ {
221
+ BuildMonitorUi(root, snapshot, MessageMonitorViewState.Default);
222
+ }
223
+
224
+ internal static void BuildMonitorUi(
225
+ VisualElement root,
226
+ MessageMonitorSnapshot snapshot,
227
+ IReadOnlyList<ComponentMonitorEntry> componentEntries
228
+ )
229
+ {
230
+ BuildMonitorUi(
231
+ root,
232
+ snapshot,
233
+ MessageMonitorViewState.Default,
234
+ componentEntries: componentEntries
235
+ );
236
+ }
237
+
238
+ internal static void BuildMonitorUi(
239
+ VisualElement root,
240
+ MessageMonitorSnapshot snapshot,
241
+ MessageMonitorViewState viewState,
242
+ Action<string> onFilterChanged = null,
243
+ Action<int> onSelectedEntryChanged = null,
244
+ Action onRefresh = null,
245
+ Action<string> onCopyExport = null,
246
+ IReadOnlyList<ComponentMonitorEntry> componentEntries = null
247
+ )
248
+ {
249
+ if (root == null)
250
+ {
251
+ throw new ArgumentNullException(nameof(root));
252
+ }
253
+
254
+ root.Clear();
255
+ componentEntries ??= Array.Empty<ComponentMonitorEntry>();
256
+ DxMessagingEditorTheme.ApplyWindow(root);
257
+ root.AddToClassList(RootClassName);
258
+ root.style.paddingTop = 10;
259
+ root.style.paddingRight = 12;
260
+ root.style.paddingBottom = 12;
261
+ root.style.paddingLeft = 12;
262
+
263
+ VisualElement toolbar = new();
264
+ toolbar.AddToClassList(ToolbarClassName);
265
+ toolbar.AddToClassList(DxMessagingEditorTheme.ToolbarClassName);
266
+ toolbar.style.flexDirection = FlexDirection.Row;
267
+ toolbar.style.justifyContent = Justify.SpaceBetween;
268
+ toolbar.style.alignItems = Align.Center;
269
+ toolbar.style.marginBottom = 10;
270
+
271
+ Label title = new(Title);
272
+ title.style.fontSize = 16;
273
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
274
+ toolbar.Add(title);
275
+
276
+ IReadOnlyList<MessageMonitorEntry> filteredEntries = FilterEntries(
277
+ snapshot.Entries,
278
+ viewState.FilterText
279
+ );
280
+
281
+ Label status = new(CreateStatusText(snapshot, filteredEntries.Count))
282
+ {
283
+ name = StatusLabelName,
284
+ };
285
+ status.style.unityTextAlign = TextAnchor.MiddleRight;
286
+ toolbar.Add(status);
287
+ root.Add(toolbar);
288
+
289
+ VisualElement content = new() { name = ContentContainerName };
290
+ content.style.flexGrow = 1;
291
+
292
+ if (!snapshot.Available)
293
+ {
294
+ root.Add(content);
295
+ AddEmptyState(content, "Monitor unavailable", snapshot.UnavailableReason);
296
+ content.Add(CreateComponentPanel(componentEntries));
297
+ return;
298
+ }
299
+
300
+ Action<string> applyFilterText = null;
301
+ void RequestFilterChange(string filterText)
302
+ {
303
+ applyFilterText?.Invoke(filterText);
304
+ }
305
+
306
+ void RefreshLocalContent(string filterText)
307
+ {
308
+ IReadOnlyList<MessageMonitorEntry> nextFilteredEntries = FilterEntries(
309
+ snapshot.Entries,
310
+ filterText
311
+ );
312
+ status.text = CreateStatusText(snapshot, nextFilteredEntries.Count);
313
+ RenderMonitorContent(
314
+ content,
315
+ snapshot,
316
+ nextFilteredEntries,
317
+ componentEntries,
318
+ new MessageMonitorViewState(filterText),
319
+ onSelectedEntryChanged,
320
+ RequestFilterChange
321
+ );
322
+ }
323
+
324
+ root.Add(
325
+ CreateControlRow(
326
+ snapshot,
327
+ viewState,
328
+ onFilterChanged,
329
+ onRefresh,
330
+ onCopyExport,
331
+ onFilterChanged == null ? RefreshLocalContent : null,
332
+ out applyFilterText
333
+ )
334
+ );
335
+
336
+ root.Add(content);
337
+ RenderMonitorContent(
338
+ content,
339
+ snapshot,
340
+ filteredEntries,
341
+ componentEntries,
342
+ viewState,
343
+ onSelectedEntryChanged,
344
+ RequestFilterChange
345
+ );
346
+ }
347
+
348
+ internal static string CreateExportText(MessageMonitorSnapshot snapshot, string filterText)
349
+ {
350
+ return CreateExportText(snapshot, FilterEntries(snapshot.Entries, filterText));
351
+ }
352
+
353
+ internal static IReadOnlyList<ComponentMonitorEntry> CaptureComponentSnapshots()
354
+ {
355
+ MessagingComponent[] components = FindMessagingComponentsInLoadedScenes();
356
+ return CaptureComponentSnapshots(components.Where(IsSceneComponent));
357
+ }
358
+
359
+ internal static IReadOnlyList<ComponentMonitorEntry> CaptureComponentSnapshots(
360
+ IEnumerable<MessagingComponent> components
361
+ )
362
+ {
363
+ if (components == null)
364
+ {
365
+ throw new ArgumentNullException(nameof(components));
366
+ }
367
+
368
+ MessagingComponent[] orderedComponents = components
369
+ .Where(component => component != null)
370
+ .OrderBy(component => GetHierarchyPath(component.transform), StringComparer.Ordinal)
371
+ .ThenBy(component => InstanceId.StableId(component))
372
+ .ToArray();
373
+
374
+ List<ComponentMonitorEntry> entries = new(orderedComponents.Length);
375
+ foreach (MessagingComponent component in orderedComponents)
376
+ {
377
+ try
378
+ {
379
+ entries.Add(CreateComponentMonitorEntry(component));
380
+ }
381
+ catch (Exception exception)
382
+ {
383
+ entries.Add(CreateFailedComponentMonitorEntry(component, exception));
384
+ }
385
+ }
386
+
387
+ return entries;
388
+ }
389
+
390
+ internal static string CreateExportText(
391
+ MessageMonitorSnapshot snapshot,
392
+ IReadOnlyList<MessageMonitorEntry> entries
393
+ )
394
+ {
395
+ if (entries == null)
396
+ {
397
+ throw new ArgumentNullException(nameof(entries));
398
+ }
399
+
400
+ IReadOnlyList<MessageMonitorEntry> exportEntries = snapshot.DiagnosticsEnabled
401
+ ? entries
402
+ : Array.Empty<MessageMonitorEntry>();
403
+
404
+ StringBuilder builder = new();
405
+ builder.AppendLine("{");
406
+ builder
407
+ .Append(" \"diagnosticsEnabled\": ")
408
+ .Append(snapshot.DiagnosticsEnabled ? "true" : "false")
409
+ .AppendLine(",");
410
+ builder.Append(" \"capacity\": ").Append(snapshot.Capacity).AppendLine(",");
411
+ builder.Append(" \"entryCount\": ").Append(exportEntries.Count).AppendLine(",");
412
+ builder.AppendLine(" \"entries\": [");
413
+ for (int i = 0; i < exportEntries.Count; i++)
414
+ {
415
+ MessageMonitorEntry entry = exportEntries[i];
416
+ builder.AppendLine(" {");
417
+ AppendJsonProperty(
418
+ builder,
419
+ "messageType",
420
+ entry.MessageTypeName,
421
+ trailingComma: true
422
+ );
423
+ AppendJsonProperty(builder, "context", entry.ContextText, trailingComma: true);
424
+ AppendJsonProperty(builder, "stackTrace", entry.StackTrace, trailingComma: false);
425
+ builder.Append(" }");
426
+ if (i < exportEntries.Count - 1)
427
+ {
428
+ builder.Append(",");
429
+ }
430
+ builder.AppendLine();
431
+ }
432
+ builder.AppendLine(" ]");
433
+ builder.Append("}");
434
+ return builder.ToString();
435
+ }
436
+
437
+ private static void RenderMonitorContent(
438
+ VisualElement content,
439
+ MessageMonitorSnapshot snapshot,
440
+ IReadOnlyList<MessageMonitorEntry> filteredEntries,
441
+ IReadOnlyList<ComponentMonitorEntry> componentEntries,
442
+ MessageMonitorViewState viewState,
443
+ Action<int> onSelectedEntryChanged,
444
+ Action<string> onFilterRequested
445
+ )
446
+ {
447
+ if (content == null)
448
+ {
449
+ throw new ArgumentNullException(nameof(content));
450
+ }
451
+ if (filteredEntries == null)
452
+ {
453
+ throw new ArgumentNullException(nameof(filteredEntries));
454
+ }
455
+ if (componentEntries == null)
456
+ {
457
+ throw new ArgumentNullException(nameof(componentEntries));
458
+ }
459
+
460
+ content.Clear();
461
+ VisualElement messageSection = new() { name = MessageSectionName };
462
+ messageSection.style.flexGrow = 1;
463
+ content.Add(messageSection);
464
+ RenderMessageSection(
465
+ messageSection,
466
+ snapshot,
467
+ filteredEntries,
468
+ viewState,
469
+ onSelectedEntryChanged,
470
+ onFilterRequested
471
+ );
472
+ content.Add(CreateComponentPanel(componentEntries));
473
+ }
474
+
475
+ private static void RenderMessageSection(
476
+ VisualElement messageSection,
477
+ MessageMonitorSnapshot snapshot,
478
+ IReadOnlyList<MessageMonitorEntry> filteredEntries,
479
+ MessageMonitorViewState viewState,
480
+ Action<int> onSelectedEntryChanged,
481
+ Action<string> onFilterRequested
482
+ )
483
+ {
484
+ if (messageSection == null)
485
+ {
486
+ throw new ArgumentNullException(nameof(messageSection));
487
+ }
488
+
489
+ messageSection.Clear();
490
+
491
+ if (!snapshot.DiagnosticsEnabled)
492
+ {
493
+ AddEmptyState(
494
+ messageSection,
495
+ "Diagnostics are Off",
496
+ "Enable diagnostics to collect message history."
497
+ );
498
+ return;
499
+ }
500
+
501
+ if (snapshot.Entries.Count == 0)
502
+ {
503
+ AddEmptyState(
504
+ messageSection,
505
+ "No messages yet",
506
+ "Diagnostics are On. No messages have been recorded yet."
507
+ );
508
+ return;
509
+ }
510
+
511
+ if (filteredEntries.Count == 0)
512
+ {
513
+ AddEmptyState(
514
+ messageSection,
515
+ "No matches",
516
+ "No messages match the current filter."
517
+ );
518
+ return;
519
+ }
520
+
521
+ messageSection.Add(CreateVisibleMessageTypeLanes(filteredEntries, onFilterRequested));
522
+ messageSection.Add(CreateVisibleContextLanes(filteredEntries, onFilterRequested));
523
+
524
+ ScrollView list = new(ScrollViewMode.Vertical);
525
+ list.style.flexGrow = 1;
526
+ int selectedEntryIndex = ClampSelectedIndex(
527
+ viewState.SelectedEntryIndex,
528
+ filteredEntries.Count
529
+ );
530
+ for (int i = 0; i < filteredEntries.Count; i++)
531
+ {
532
+ list.Add(
533
+ CreateRow(
534
+ filteredEntries[i],
535
+ i,
536
+ i == selectedEntryIndex,
537
+ onSelectedEntryChanged
538
+ )
539
+ );
540
+ }
541
+ messageSection.Add(list);
542
+ messageSection.Add(CreateDetailsPane(filteredEntries[selectedEntryIndex]));
543
+ }
544
+
545
+ private static IReadOnlyList<MessageMonitorEntry> FilterEntries(
546
+ IReadOnlyList<MessageMonitorEntry> entries,
547
+ string filterText
548
+ )
549
+ {
550
+ if (entries == null)
551
+ {
552
+ throw new ArgumentNullException(nameof(entries));
553
+ }
554
+
555
+ if (string.IsNullOrWhiteSpace(filterText))
556
+ {
557
+ return entries;
558
+ }
559
+
560
+ return entries.Where(entry => entry.Matches(filterText)).ToArray();
561
+ }
562
+
563
+ private static string CreateStatusText(MessageMonitorSnapshot snapshot, int visibleCount)
564
+ {
565
+ if (!snapshot.Available)
566
+ {
567
+ return "Unavailable";
568
+ }
569
+ string enabled = snapshot.DiagnosticsEnabled ? "On" : "Off";
570
+ if (
571
+ snapshot.DiagnosticsEnabled
572
+ && visibleCount >= 0
573
+ && visibleCount != snapshot.Entries.Count
574
+ )
575
+ {
576
+ return $"Diagnostics {enabled} | {visibleCount}/{snapshot.Entries.Count} shown | {snapshot.Entries.Count}/{snapshot.Capacity}";
577
+ }
578
+ return $"Diagnostics {enabled} | {snapshot.Entries.Count}/{snapshot.Capacity}";
579
+ }
580
+
581
+ private static VisualElement CreateVisibleMessageTypeLanes(
582
+ IReadOnlyList<MessageMonitorEntry> entries,
583
+ Action<string> onFilterRequested
584
+ )
585
+ {
586
+ MessageMonitorTypeLane[] lanes = BuildVisibleMessageTypeLanes(entries);
587
+ VisualElement lanesRoot = new() { name = VisibleMessageTypeLanesName };
588
+ DxMessagingEditorTheme.ApplyCompleteBorder(
589
+ lanesRoot,
590
+ DxMessagingEditorPalette.BorderPanel
591
+ );
592
+ lanesRoot.style.marginBottom = 8;
593
+ lanesRoot.style.paddingTop = 8;
594
+ lanesRoot.style.paddingRight = 8;
595
+ lanesRoot.style.paddingBottom = 8;
596
+ lanesRoot.style.paddingLeft = 8;
597
+
598
+ Label title = new("Visible Message Type Lanes");
599
+ title.AddToClassList(DxMessagingEditorTheme.CardLabelClassName);
600
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
601
+ lanesRoot.Add(title);
602
+
603
+ Label summary = new(CreateVisibleMessageTypeLanesSummaryText(lanes))
604
+ {
605
+ name = VisibleMessageTypeLanesSummaryLabelName,
606
+ };
607
+ summary.style.marginTop = 2;
608
+ summary.style.whiteSpace = WhiteSpace.Normal;
609
+ lanesRoot.Add(summary);
610
+
611
+ ScrollView laneRows = new(ScrollViewMode.Vertical)
612
+ {
613
+ name = VisibleMessageTypeLaneScrollViewName,
614
+ };
615
+ laneRows.style.maxHeight = 160;
616
+ laneRows.style.marginTop = 2;
617
+ lanesRoot.Add(laneRows);
618
+
619
+ int totalEntries = lanes.Sum(lane => lane.EntryCount);
620
+ foreach (MessageMonitorTypeLane lane in lanes)
621
+ {
622
+ laneRows.Add(
623
+ CreateVisibleMessageTypeLaneRow(lane, totalEntries, onFilterRequested)
624
+ );
625
+ }
626
+
627
+ return lanesRoot;
628
+ }
629
+
630
+ private static VisualElement CreateVisibleContextLanes(
631
+ IReadOnlyList<MessageMonitorEntry> entries,
632
+ Action<string> onFilterRequested
633
+ )
634
+ {
635
+ MessageMonitorContextLane[] lanes = BuildVisibleContextLanes(entries);
636
+ VisualElement lanesRoot = new() { name = VisibleContextLanesName };
637
+ DxMessagingEditorTheme.ApplyCompleteBorder(
638
+ lanesRoot,
639
+ DxMessagingEditorPalette.BorderPanel
640
+ );
641
+ lanesRoot.style.marginBottom = 8;
642
+ lanesRoot.style.paddingTop = 8;
643
+ lanesRoot.style.paddingRight = 8;
644
+ lanesRoot.style.paddingBottom = 8;
645
+ lanesRoot.style.paddingLeft = 8;
646
+
647
+ Label title = new("Visible Context Lanes");
648
+ title.AddToClassList(DxMessagingEditorTheme.CardLabelClassName);
649
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
650
+ lanesRoot.Add(title);
651
+
652
+ Label summary = new(CreateVisibleContextLanesSummaryText(lanes))
653
+ {
654
+ name = VisibleContextLanesSummaryLabelName,
655
+ };
656
+ summary.style.marginTop = 2;
657
+ summary.style.whiteSpace = WhiteSpace.Normal;
658
+ lanesRoot.Add(summary);
659
+
660
+ ScrollView laneRows = new(ScrollViewMode.Vertical)
661
+ {
662
+ name = VisibleContextLaneScrollViewName,
663
+ };
664
+ laneRows.style.maxHeight = 160;
665
+ laneRows.style.marginTop = 2;
666
+ lanesRoot.Add(laneRows);
667
+
668
+ int totalEntries = lanes.Sum(lane => lane.EntryCount);
669
+ foreach (MessageMonitorContextLane lane in lanes)
670
+ {
671
+ laneRows.Add(CreateVisibleContextLaneRow(lane, totalEntries, onFilterRequested));
672
+ }
673
+
674
+ return lanesRoot;
675
+ }
676
+
677
+ private static VisualElement CreateVisibleMessageTypeLaneRow(
678
+ MessageMonitorTypeLane lane,
679
+ int totalEntries,
680
+ Action<string> onFilterRequested
681
+ )
682
+ {
683
+ VisualElement row = new();
684
+ row.AddToClassList(VisibleMessageTypeLaneRowClassName);
685
+ row.AddToClassList(DxMessagingEditorTheme.CardClassName);
686
+ row.style.flexDirection = FlexDirection.Row;
687
+ row.style.alignItems = Align.Center;
688
+ DxMessagingEditorTheme.ApplyCompleteBorder(row, DxMessagingEditorPalette.Amber);
689
+ row.style.marginTop = 6;
690
+ row.style.paddingTop = 7;
691
+ row.style.paddingRight = 8;
692
+ row.style.paddingBottom = 7;
693
+ row.style.paddingLeft = 10;
694
+
695
+ Label type = new(lane.MessageTypeName) { name = VisibleMessageTypeLaneTypeLabelName };
696
+ type.style.flexBasis = 0;
697
+ type.style.flexGrow = 1;
698
+ type.style.unityFontStyleAndWeight = FontStyle.Bold;
699
+ type.style.whiteSpace = WhiteSpace.Normal;
700
+ row.Add(type);
701
+
702
+ Label summary = new(
703
+ $"Entries: {lane.EntryCount} | Contexts: {lane.ContextCount} | Share: {CreateEntryShareText(lane.EntryCount, totalEntries)}"
704
+ )
705
+ {
706
+ name = VisibleMessageTypeLaneSummaryLabelName,
707
+ };
708
+ summary.style.flexBasis = 0;
709
+ summary.style.flexGrow = 2;
710
+ summary.style.marginLeft = 8;
711
+ summary.style.whiteSpace = WhiteSpace.Normal;
712
+ row.Add(summary);
713
+
714
+ Label contexts = new($"Contexts: {lane.ContextsText}")
715
+ {
716
+ name = VisibleMessageTypeLaneContextsLabelName,
717
+ };
718
+ contexts.style.flexBasis = 0;
719
+ contexts.style.flexGrow = 3;
720
+ contexts.style.marginLeft = 8;
721
+ contexts.style.whiteSpace = WhiteSpace.Normal;
722
+ row.Add(contexts);
723
+
724
+ row.Add(
725
+ CreateLaneFilterButton(
726
+ VisibleMessageTypeLaneFilterButtonName,
727
+ CreateMessageTypeLaneFilterText(lane.MessageTypeName),
728
+ onFilterRequested
729
+ )
730
+ );
731
+
732
+ return row;
733
+ }
734
+
735
+ private static VisualElement CreateVisibleContextLaneRow(
736
+ MessageMonitorContextLane lane,
737
+ int totalEntries,
738
+ Action<string> onFilterRequested
739
+ )
740
+ {
741
+ VisualElement row = new();
742
+ row.AddToClassList(VisibleContextLaneRowClassName);
743
+ row.AddToClassList(DxMessagingEditorTheme.CardClassName);
744
+ row.style.flexDirection = FlexDirection.Row;
745
+ row.style.alignItems = Align.Center;
746
+ DxMessagingEditorTheme.ApplyCompleteBorder(row, DxMessagingEditorPalette.AmberSoft);
747
+ row.style.marginTop = 6;
748
+ row.style.paddingTop = 7;
749
+ row.style.paddingRight = 8;
750
+ row.style.paddingBottom = 7;
751
+ row.style.paddingLeft = 10;
752
+
753
+ Label context = new(lane.ContextText) { name = VisibleContextLaneContextLabelName };
754
+ context.style.flexBasis = 0;
755
+ context.style.flexGrow = 1;
756
+ context.style.unityFontStyleAndWeight = FontStyle.Bold;
757
+ context.style.whiteSpace = WhiteSpace.Normal;
758
+ row.Add(context);
759
+
760
+ Label summary = new(
761
+ $"Entries: {lane.EntryCount} | Message types: {lane.MessageTypeCount} | Share: {CreateEntryShareText(lane.EntryCount, totalEntries)}"
762
+ )
763
+ {
764
+ name = VisibleContextLaneSummaryLabelName,
765
+ };
766
+ summary.style.flexBasis = 0;
767
+ summary.style.flexGrow = 2;
768
+ summary.style.marginLeft = 8;
769
+ summary.style.whiteSpace = WhiteSpace.Normal;
770
+ row.Add(summary);
771
+
772
+ Label messages = new($"Messages: {lane.MessageTypesText}")
773
+ {
774
+ name = VisibleContextLaneMessagesLabelName,
775
+ };
776
+ messages.style.flexBasis = 0;
777
+ messages.style.flexGrow = 3;
778
+ messages.style.marginLeft = 8;
779
+ messages.style.whiteSpace = WhiteSpace.Normal;
780
+ row.Add(messages);
781
+
782
+ row.Add(
783
+ CreateLaneFilterButton(
784
+ VisibleContextLaneFilterButtonName,
785
+ CreateContextLaneFilterText(lane.ContextText),
786
+ onFilterRequested
787
+ )
788
+ );
789
+
790
+ return row;
791
+ }
792
+
793
+ private static Button CreateLaneFilterButton(
794
+ string name,
795
+ string filterText,
796
+ Action<string> onFilterRequested
797
+ )
798
+ {
799
+ Button button = new() { name = name, text = "Filter" };
800
+ button.AddToClassList(DxMessagingEditorTheme.ButtonGhostClassName);
801
+ button.tooltip = $"Filter to {filterText}";
802
+ button.SetEnabled(onFilterRequested != null && !string.IsNullOrWhiteSpace(filterText));
803
+ button.RegisterCallback<ClickEvent>(_ => onFilterRequested?.Invoke(filterText));
804
+ button.style.marginLeft = 8;
805
+ button.style.flexShrink = 0;
806
+ return button;
807
+ }
808
+
809
+ private static string CreateMessageTypeLaneFilterText(string messageTypeName)
810
+ {
811
+ return $"type:{NormalizeMessageTypeName(messageTypeName)}";
812
+ }
813
+
814
+ private static string CreateContextLaneFilterText(string contextText)
815
+ {
816
+ return $"context:{QuoteFilterValue(NormalizeContextText(contextText))}";
817
+ }
818
+
819
+ private static string QuoteFilterValue(string value)
820
+ {
821
+ return $"\"{(value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
822
+ }
823
+
824
+ private static string CreateVisibleMessageTypeLanesSummaryText(
825
+ IReadOnlyList<MessageMonitorTypeLane> lanes
826
+ )
827
+ {
828
+ int totalEntries = lanes.Sum(lane => lane.EntryCount);
829
+ if (lanes.Count == 0 || totalEntries <= 0 || lanes[0].EntryCount <= 0)
830
+ {
831
+ return $"{FormatCount(lanes.Count, "message type lane")} | Entries: {totalEntries} | Busiest message type: none";
832
+ }
833
+
834
+ MessageMonitorTypeLane busiestLane = lanes[0];
835
+ return $"{FormatCount(lanes.Count, "message type lane")} | Entries: {totalEntries} | Busiest message type: {busiestLane.MessageTypeName} | Share: {CreateEntryShareText(busiestLane.EntryCount, totalEntries)}";
836
+ }
837
+
838
+ private static string CreateVisibleContextLanesSummaryText(
839
+ IReadOnlyList<MessageMonitorContextLane> lanes
840
+ )
841
+ {
842
+ int totalEntries = lanes.Sum(lane => lane.EntryCount);
843
+ if (lanes.Count == 0 || totalEntries <= 0 || lanes[0].EntryCount <= 0)
844
+ {
845
+ return $"{FormatCount(lanes.Count, "context lane")} | Entries: {totalEntries} | Busiest context: none";
846
+ }
847
+
848
+ MessageMonitorContextLane busiestLane = lanes[0];
849
+ return $"{FormatCount(lanes.Count, "context lane")} | Entries: {totalEntries} | Busiest context: {busiestLane.ContextText} | Share: {CreateEntryShareText(busiestLane.EntryCount, totalEntries)}";
850
+ }
851
+
852
+ private static MessageMonitorTypeLane[] BuildVisibleMessageTypeLanes(
853
+ IReadOnlyList<MessageMonitorEntry> entries
854
+ )
855
+ {
856
+ var laneGroups = entries
857
+ .GroupBy(entry => NormalizeMessageTypeIdentity(entry))
858
+ .Select(group =>
859
+ {
860
+ MessageMonitorEntry[] groupEntries = group.ToArray();
861
+ MessageMonitorEntry firstEntry = groupEntries[0];
862
+ string[] contexts = groupEntries
863
+ .Select(entry => entry.ContextText)
864
+ .Where(context => !string.IsNullOrWhiteSpace(context))
865
+ .Distinct(StringComparer.Ordinal)
866
+ .OrderBy(context => context, StringComparer.Ordinal)
867
+ .ToArray();
868
+
869
+ return new
870
+ {
871
+ MessageTypeName = NormalizeMessageTypeName(firstEntry.MessageTypeName),
872
+ MessageTypeDisplayPath = NormalizeMessageTypeName(
873
+ firstEntry.MessageTypeDisplayPath
874
+ ),
875
+ EntryCount = groupEntries.Length,
876
+ Contexts = contexts,
877
+ };
878
+ })
879
+ .ToArray();
880
+
881
+ HashSet<string> duplicateDisplayNames = new(
882
+ laneGroups
883
+ .GroupBy(group => group.MessageTypeName, StringComparer.Ordinal)
884
+ .Where(group => group.Count() > 1)
885
+ .Select(group => group.Key),
886
+ StringComparer.Ordinal
887
+ );
888
+
889
+ return laneGroups
890
+ .Select(group => new MessageMonitorTypeLane(
891
+ duplicateDisplayNames.Contains(group.MessageTypeName)
892
+ ? group.MessageTypeDisplayPath
893
+ : group.MessageTypeName,
894
+ group.EntryCount,
895
+ group.Contexts
896
+ ))
897
+ .OrderByDescending(lane => lane.EntryCount)
898
+ .ThenBy(lane => lane.MessageTypeName, StringComparer.Ordinal)
899
+ .ToArray();
900
+ }
901
+
902
+ private static MessageMonitorContextLane[] BuildVisibleContextLanes(
903
+ IReadOnlyList<MessageMonitorEntry> entries
904
+ )
905
+ {
906
+ HashSet<string> duplicateDisplayNames = CreateDuplicateMessageTypeDisplayNames(entries);
907
+
908
+ return entries
909
+ .GroupBy(entry => NormalizeContextText(entry.ContextText), StringComparer.Ordinal)
910
+ .Select(group =>
911
+ {
912
+ MessageMonitorEntry[] groupEntries = group.ToArray();
913
+ string[] messageTypes = CreateVisibleMessageTypeDisplayNames(
914
+ groupEntries,
915
+ duplicateDisplayNames
916
+ );
917
+
918
+ return new MessageMonitorContextLane(
919
+ group.Key,
920
+ groupEntries.Length,
921
+ messageTypes
922
+ );
923
+ })
924
+ .OrderByDescending(lane => lane.EntryCount)
925
+ .ThenBy(lane => lane.ContextText, StringComparer.Ordinal)
926
+ .ToArray();
927
+ }
928
+
929
+ private static string[] CreateVisibleMessageTypeDisplayNames(
930
+ IReadOnlyList<MessageMonitorEntry> entries,
931
+ HashSet<string> duplicateDisplayNames
932
+ )
933
+ {
934
+ var typeGroups = entries
935
+ .GroupBy(entry => NormalizeMessageTypeIdentity(entry))
936
+ .Select(group =>
937
+ {
938
+ MessageMonitorEntry firstEntry = group.First();
939
+ return new
940
+ {
941
+ MessageTypeName = NormalizeMessageTypeName(firstEntry.MessageTypeName),
942
+ MessageTypeDisplayPath = NormalizeMessageTypeName(
943
+ firstEntry.MessageTypeDisplayPath
944
+ ),
945
+ };
946
+ })
947
+ .ToArray();
948
+
949
+ return typeGroups
950
+ .Select(group =>
951
+ duplicateDisplayNames.Contains(group.MessageTypeName)
952
+ ? group.MessageTypeDisplayPath
953
+ : group.MessageTypeName
954
+ )
955
+ .OrderBy(messageType => messageType, StringComparer.Ordinal)
956
+ .ToArray();
957
+ }
958
+
959
+ private static HashSet<string> CreateDuplicateMessageTypeDisplayNames(
960
+ IReadOnlyList<MessageMonitorEntry> entries
961
+ )
962
+ {
963
+ string[] typeNames = entries
964
+ .GroupBy(entry => NormalizeMessageTypeIdentity(entry))
965
+ .Select(group => NormalizeMessageTypeName(group.First().MessageTypeName))
966
+ .ToArray();
967
+
968
+ return new HashSet<string>(
969
+ typeNames
970
+ .GroupBy(typeName => typeName, StringComparer.Ordinal)
971
+ .Where(group => group.Count() > 1)
972
+ .Select(group => group.Key),
973
+ StringComparer.Ordinal
974
+ );
975
+ }
976
+
977
+ private static string NormalizeMessageTypeName(string messageTypeName)
978
+ {
979
+ return string.IsNullOrWhiteSpace(messageTypeName)
980
+ ? "<unknown>"
981
+ : messageTypeName.Trim();
982
+ }
983
+
984
+ private static string NormalizeMessageTypeIdentity(MessageMonitorEntry entry)
985
+ {
986
+ return string.IsNullOrWhiteSpace(entry.MessageTypeIdentity)
987
+ ? NormalizeMessageTypeName(entry.MessageTypeName)
988
+ : entry.MessageTypeIdentity.Trim();
989
+ }
990
+
991
+ private static string NormalizeContextText(string contextText)
992
+ {
993
+ return string.IsNullOrWhiteSpace(contextText) ? "Context: none" : contextText.Trim();
994
+ }
995
+
996
+ private static string CreateEntryShareText(int count, int total)
997
+ {
998
+ if (total <= 0)
999
+ {
1000
+ return $"{count}/{total} (n/a)";
1001
+ }
1002
+
1003
+ int percent = (int)
1004
+ Math.Round((double)count / total * 100, MidpointRounding.AwayFromZero);
1005
+ return $"{count}/{total} ({percent}%)";
1006
+ }
1007
+
1008
+ private static string FormatCount(int count, string noun)
1009
+ {
1010
+ return count == 1 ? $"1 {noun}" : $"{count} {noun}s";
1011
+ }
1012
+
1013
+ private static VisualElement CreateControlRow(
1014
+ MessageMonitorSnapshot snapshot,
1015
+ MessageMonitorViewState viewState,
1016
+ Action<string> onFilterChanged,
1017
+ Action onRefresh,
1018
+ Action<string> onCopyExport,
1019
+ Action<string> onLocalFilterChanged,
1020
+ out Action<string> applyFilterText
1021
+ )
1022
+ {
1023
+ VisualElement controls = new();
1024
+ controls.style.marginBottom = 10;
1025
+
1026
+ VisualElement filterRow = new();
1027
+ filterRow.style.flexDirection = FlexDirection.Row;
1028
+ filterRow.style.alignItems = Align.Center;
1029
+ controls.Add(filterRow);
1030
+
1031
+ TextField filter = new("Filter") { name = FilterFieldName };
1032
+ filter.AddToClassList(DxMessagingEditorTheme.SearchClassName);
1033
+ filter.SetValueWithoutNotify(viewState.FilterText);
1034
+ filter.tooltip =
1035
+ "Use plain text, or field filters such as type:, message:, context:, and stack:.";
1036
+ filter.style.flexGrow = 1;
1037
+ filter.style.marginRight = 8;
1038
+ Button export = null;
1039
+ VisualElement activeFilter = null;
1040
+ void ApplyFilterState(string filterText)
1041
+ {
1042
+ string normalizedFilterText = filterText ?? string.Empty;
1043
+ onFilterChanged?.Invoke(normalizedFilterText);
1044
+ onLocalFilterChanged?.Invoke(normalizedFilterText);
1045
+ SetExportButtonEnabled(export, snapshot, normalizedFilterText, onCopyExport);
1046
+ UpdateActiveFilterSummary(
1047
+ activeFilter,
1048
+ normalizedFilterText,
1049
+ () => ClearFilter(filter, activeFilter, snapshot, onCopyExport, export)
1050
+ );
1051
+ }
1052
+
1053
+ applyFilterText = filterText =>
1054
+ {
1055
+ string normalizedFilterText = filterText ?? string.Empty;
1056
+ if (filter.panel != null)
1057
+ {
1058
+ if (
1059
+ !string.Equals(filter.value, normalizedFilterText, StringComparison.Ordinal)
1060
+ )
1061
+ {
1062
+ filter.value = normalizedFilterText;
1063
+ return;
1064
+ }
1065
+
1066
+ ApplyFilterState(normalizedFilterText);
1067
+ return;
1068
+ }
1069
+
1070
+ filter.SetValueWithoutNotify(normalizedFilterText);
1071
+ ApplyFilterState(normalizedFilterText);
1072
+ };
1073
+ filter.RegisterValueChangedCallback(evt =>
1074
+ {
1075
+ ApplyFilterState(evt.newValue);
1076
+ });
1077
+ filterRow.Add(filter);
1078
+
1079
+ Button refresh = new(() => onRefresh?.Invoke())
1080
+ {
1081
+ name = RefreshButtonName,
1082
+ text = "Refresh",
1083
+ };
1084
+ refresh.AddToClassList(DxMessagingEditorTheme.ToolButtonClassName);
1085
+ refresh.SetEnabled(onRefresh != null);
1086
+ refresh.style.marginRight = 6;
1087
+ filterRow.Add(refresh);
1088
+
1089
+ export = new(() => onCopyExport?.Invoke(CreateExportText(snapshot, filter.value)))
1090
+ {
1091
+ name = ExportButtonName,
1092
+ text = "Copy JSON",
1093
+ };
1094
+ export.AddToClassList(DxMessagingEditorTheme.ToolButtonClassName);
1095
+ SetExportButtonEnabled(export, snapshot, viewState.FilterText, onCopyExport);
1096
+ filterRow.Add(export);
1097
+
1098
+ activeFilter = CreateActiveFilterSummary(
1099
+ viewState.FilterText,
1100
+ () => ClearFilter(filter, activeFilter, snapshot, onCopyExport, export)
1101
+ );
1102
+ controls.Add(activeFilter);
1103
+
1104
+ return controls;
1105
+ }
1106
+
1107
+ private static VisualElement CreateActiveFilterSummary(string filterText, Action onClear)
1108
+ {
1109
+ VisualElement summary = new() { name = ActiveFilterSummaryName };
1110
+ summary.AddToClassList(DxMessagingEditorTheme.CardClassName);
1111
+ summary.style.flexDirection = FlexDirection.Row;
1112
+ summary.style.alignItems = Align.Center;
1113
+ summary.style.marginTop = 6;
1114
+ summary.style.paddingTop = 5;
1115
+ summary.style.paddingRight = 6;
1116
+ summary.style.paddingBottom = 5;
1117
+ summary.style.paddingLeft = 8;
1118
+ DxMessagingEditorTheme.ApplyCompleteBorder(summary, DxMessagingEditorPalette.Amber);
1119
+
1120
+ UpdateActiveFilterSummary(summary, filterText, onClear);
1121
+ return summary;
1122
+ }
1123
+
1124
+ private static void UpdateActiveFilterSummary(
1125
+ VisualElement summary,
1126
+ string filterText,
1127
+ Action onClear
1128
+ )
1129
+ {
1130
+ if (summary == null)
1131
+ {
1132
+ return;
1133
+ }
1134
+
1135
+ summary.Clear();
1136
+ if (string.IsNullOrWhiteSpace(filterText))
1137
+ {
1138
+ summary.style.display = DisplayStyle.None;
1139
+ return;
1140
+ }
1141
+
1142
+ summary.style.display = DisplayStyle.Flex;
1143
+ bool typedFilter = MessageMonitorFilterQuery.TryCreateDisplayTokens(
1144
+ filterText,
1145
+ out string[] displayTokens
1146
+ );
1147
+ if (!typedFilter)
1148
+ {
1149
+ displayTokens = new[] { filterText.Trim() };
1150
+ }
1151
+
1152
+ Label label = new(typedFilter ? "Active typed filters" : "Active text filter")
1153
+ {
1154
+ name = ActiveFilterSummaryLabelName,
1155
+ };
1156
+ label.style.unityFontStyleAndWeight = FontStyle.Bold;
1157
+ label.style.marginRight = 8;
1158
+ label.style.flexShrink = 0;
1159
+ summary.Add(label);
1160
+
1161
+ Button clear = new() { name = ActiveFilterClearButtonName, text = "Clear" };
1162
+ clear.AddToClassList(DxMessagingEditorTheme.ButtonGhostClassName);
1163
+ clear.RegisterCallback<ClickEvent>(_ => onClear?.Invoke());
1164
+ clear.style.marginRight = 8;
1165
+ clear.style.flexShrink = 0;
1166
+ summary.Add(clear);
1167
+
1168
+ ScrollView tokenScroll = new(ScrollViewMode.Vertical)
1169
+ {
1170
+ name = ActiveFilterTokenScrollViewName,
1171
+ };
1172
+ tokenScroll.style.flexGrow = 1;
1173
+ tokenScroll.style.flexShrink = 1;
1174
+ tokenScroll.style.maxHeight = 72;
1175
+ tokenScroll.contentContainer.style.flexDirection = FlexDirection.Row;
1176
+ tokenScroll.contentContainer.style.flexWrap = Wrap.Wrap;
1177
+ summary.Add(tokenScroll);
1178
+
1179
+ foreach (string token in displayTokens)
1180
+ {
1181
+ Label tokenLabel = new(token);
1182
+ tokenLabel.AddToClassList(ActiveFilterTokenClassName);
1183
+ tokenLabel.style.marginTop = 2;
1184
+ tokenLabel.style.marginRight = 6;
1185
+ tokenLabel.style.marginBottom = 2;
1186
+ tokenLabel.style.paddingTop = 2;
1187
+ tokenLabel.style.paddingRight = 5;
1188
+ tokenLabel.style.paddingBottom = 2;
1189
+ tokenLabel.style.paddingLeft = 5;
1190
+ DxMessagingEditorTheme.ApplyCompleteBorder(
1191
+ tokenLabel,
1192
+ DxMessagingEditorPalette.Border
1193
+ );
1194
+ tokenLabel.style.whiteSpace = WhiteSpace.Normal;
1195
+ tokenScroll.Add(tokenLabel);
1196
+ }
1197
+ }
1198
+
1199
+ private static void ClearFilter(
1200
+ TextField filter,
1201
+ VisualElement activeFilter,
1202
+ MessageMonitorSnapshot snapshot,
1203
+ Action<string> onCopyExport,
1204
+ Button export
1205
+ )
1206
+ {
1207
+ if (filter == null || string.IsNullOrEmpty(filter.value))
1208
+ {
1209
+ return;
1210
+ }
1211
+
1212
+ if (filter.panel != null)
1213
+ {
1214
+ filter.value = string.Empty;
1215
+ return;
1216
+ }
1217
+
1218
+ filter.SetValueWithoutNotify(string.Empty);
1219
+ SetExportButtonEnabled(export, snapshot, string.Empty, onCopyExport);
1220
+ UpdateActiveFilterSummary(activeFilter, string.Empty, null);
1221
+ }
1222
+
1223
+ private static void SetExportButtonEnabled(
1224
+ Button export,
1225
+ MessageMonitorSnapshot snapshot,
1226
+ string filterText,
1227
+ Action<string> onCopyExport
1228
+ )
1229
+ {
1230
+ if (export == null)
1231
+ {
1232
+ return;
1233
+ }
1234
+
1235
+ export.SetEnabled(
1236
+ onCopyExport != null
1237
+ && snapshot.DiagnosticsEnabled
1238
+ && FilterEntries(snapshot.Entries, filterText).Count > 0
1239
+ );
1240
+ }
1241
+
1242
+ private static bool IsSceneComponent(MessagingComponent component)
1243
+ {
1244
+ return component != null
1245
+ && component.gameObject != null
1246
+ && component.gameObject.scene.IsValid()
1247
+ && !EditorSceneManager.IsPreviewSceneObject(component.gameObject)
1248
+ && !EditorUtility.IsPersistent(component);
1249
+ }
1250
+
1251
+ private static MessagingComponent[] FindMessagingComponentsInLoadedScenes()
1252
+ {
1253
+ #if UNITY_2023_1_OR_NEWER
1254
+ // FindObjectsByType's two-argument (FindObjectsInactive, FindObjectsSortMode)
1255
+ // overload exists across all 2023.1+ editors, including every 6000.x. The
1256
+ // one-argument FindObjectsByType(FindObjectsInactive) convenience overload only
1257
+ // exists on some 6000.x patch releases (e.g. 6000.4, not 6000.3), so always pass
1258
+ // both arguments to stay portable across the whole 6000.x range.
1259
+ return UnityEngine.Object.FindObjectsByType<MessagingComponent>(
1260
+ FindObjectsInactive.Include,
1261
+ FindObjectsSortMode.None
1262
+ );
1263
+ #else
1264
+ return UnityEngine.Object.FindObjectsOfType<MessagingComponent>(includeInactive: true);
1265
+ #endif
1266
+ }
1267
+
1268
+ private static ComponentMonitorEntry CreateComponentMonitorEntry(
1269
+ MessagingComponent component
1270
+ )
1271
+ {
1272
+ MessagingComponentInspectorState state = MessagingComponentEditorHarness.Capture(
1273
+ component,
1274
+ resolveSerializedProviderBus: false
1275
+ );
1276
+ int listenerCount = state.Listeners.Count;
1277
+ int enabledListenerCount = state.Listeners.Count(listener => listener.TokenEnabled);
1278
+ int diagnosticsListenerCount = state.Listeners.Count(listener =>
1279
+ listener.DiagnosticsEnabled
1280
+ );
1281
+ int registrationCount = state.Listeners.Sum(listener => listener.Registrations.Count);
1282
+ int callCount = state.Listeners.Sum(listener =>
1283
+ listener.Registrations.Sum(registration => registration.CallCount)
1284
+ );
1285
+ int localEmissionCount = state.Listeners.Sum(listener =>
1286
+ listener.EmissionHistory.Count
1287
+ );
1288
+
1289
+ return new ComponentMonitorEntry(
1290
+ GetHierarchyPath(component.transform),
1291
+ component.GetType().Name,
1292
+ component.gameObject.activeInHierarchy,
1293
+ listenerCount,
1294
+ enabledListenerCount,
1295
+ diagnosticsListenerCount,
1296
+ registrationCount,
1297
+ callCount,
1298
+ localEmissionCount,
1299
+ CreateProviderStatusText(state.ProviderDiagnostics),
1300
+ CreateProviderWarningText(state.ProviderDiagnostics)
1301
+ );
1302
+ }
1303
+
1304
+ private static ComponentMonitorEntry CreateFailedComponentMonitorEntry(
1305
+ MessagingComponent component,
1306
+ Exception exception
1307
+ )
1308
+ {
1309
+ return new ComponentMonitorEntry(
1310
+ GetHierarchyPath(component != null ? component.transform : null),
1311
+ component != null ? component.GetType().Name : "<missing>",
1312
+ component != null && component.gameObject.activeInHierarchy,
1313
+ listenerCount: 0,
1314
+ enabledListenerCount: 0,
1315
+ diagnosticsListenerCount: 0,
1316
+ registrationCount: 0,
1317
+ callCount: 0,
1318
+ localEmissionCount: 0,
1319
+ providerStatusText: "Provider: unavailable",
1320
+ warningText: $"Diagnostics capture failed: {exception}"
1321
+ );
1322
+ }
1323
+
1324
+ private static string CreateProviderStatusText(ProviderDiagnosticsView providerDiagnostics)
1325
+ {
1326
+ List<string> states = new();
1327
+ if (providerDiagnostics.HasMessageBusOverride)
1328
+ {
1329
+ states.Add("bus override");
1330
+ }
1331
+ if (providerDiagnostics.HasRuntimeProvider)
1332
+ {
1333
+ states.Add("runtime provider");
1334
+ }
1335
+ if (providerDiagnostics.HasSerializedProvider)
1336
+ {
1337
+ states.Add("serialized provider");
1338
+ }
1339
+ if (states.Count == 0)
1340
+ {
1341
+ states.Add("global bus");
1342
+ }
1343
+ if (providerDiagnostics.AutoConfigureSerializedProviderOnAwake)
1344
+ {
1345
+ states.Add("auto-configure");
1346
+ }
1347
+
1348
+ return "Provider: " + string.Join(", ", states);
1349
+ }
1350
+
1351
+ private static string CreateProviderWarningText(ProviderDiagnosticsView providerDiagnostics)
1352
+ {
1353
+ List<string> warnings = new();
1354
+ if (providerDiagnostics.SerializedProviderMissingWarning)
1355
+ {
1356
+ warnings.Add("Serialized provider missing");
1357
+ }
1358
+ if (providerDiagnostics.SerializedProviderNullBusWarning)
1359
+ {
1360
+ warnings.Add("Serialized provider resolves no bus");
1361
+ }
1362
+
1363
+ return string.Join("; ", warnings);
1364
+ }
1365
+
1366
+ private static string GetHierarchyPath(Transform transform)
1367
+ {
1368
+ if (transform == null)
1369
+ {
1370
+ return "<missing>";
1371
+ }
1372
+
1373
+ Stack<string> segments = new();
1374
+ Transform current = transform;
1375
+ while (current != null)
1376
+ {
1377
+ segments.Push(current.name);
1378
+ current = current.parent;
1379
+ }
1380
+
1381
+ return string.Join("/", segments);
1382
+ }
1383
+
1384
+ private static void AddEmptyState(VisualElement root, string title, string body)
1385
+ {
1386
+ VisualElement empty = DxMessagingEditorTheme.CreateEmptyState(
1387
+ title,
1388
+ body,
1389
+ bodyName: EmptyStateLabelName,
1390
+ titleName: EmptyStateTitleLabelName
1391
+ );
1392
+ empty.style.marginTop = 8;
1393
+ root.Add(empty);
1394
+ }
1395
+
1396
+ private static VisualElement CreateRow(
1397
+ MessageMonitorEntry entry,
1398
+ int entryIndex,
1399
+ bool selected,
1400
+ Action<int> onSelectedEntryChanged
1401
+ )
1402
+ {
1403
+ VisualElement row = new();
1404
+ row.AddToClassList(RowClassName);
1405
+ row.AddToClassList(DxMessagingEditorTheme.CardClassName);
1406
+ Color routeColor = DxMessagingEditorPalette.RouteKindColor(entry.RouteKind);
1407
+ DxMessagingEditorTheme.ApplyCompleteBorder(row, routeColor);
1408
+ row.style.marginBottom = 8;
1409
+ row.style.paddingTop = 8;
1410
+ row.style.paddingRight = 8;
1411
+ row.style.paddingBottom = 8;
1412
+ row.style.paddingLeft = 10;
1413
+ if (selected)
1414
+ {
1415
+ row.style.backgroundColor = DxMessagingEditorPalette.SelectedWash;
1416
+ }
1417
+ if (onSelectedEntryChanged != null)
1418
+ {
1419
+ row.RegisterCallback<ClickEvent>(_ => onSelectedEntryChanged.Invoke(entryIndex));
1420
+ }
1421
+
1422
+ Label type = new(entry.MessageTypeName) { name = MessageTypeLabelName };
1423
+ type.style.unityFontStyleAndWeight = FontStyle.Bold;
1424
+ row.Add(type);
1425
+
1426
+ string routeKind = DxMessagingEditorPalette.NormalizeRouteKind(entry.RouteKind);
1427
+ if (!string.IsNullOrWhiteSpace(routeKind))
1428
+ {
1429
+ Label kind = new(routeKind) { name = RouteKindLabelName };
1430
+ DxMessagingEditorTheme.AddRouteKindTypeBadgeClasses(kind, routeKind);
1431
+ kind.style.marginTop = 2;
1432
+ kind.style.unityFontStyleAndWeight = FontStyle.Bold;
1433
+ row.Add(kind);
1434
+ }
1435
+
1436
+ Label context = new(entry.ContextText) { name = ContextLabelName };
1437
+ context.style.marginTop = 2;
1438
+ row.Add(context);
1439
+
1440
+ if (!string.IsNullOrWhiteSpace(entry.StackTrace))
1441
+ {
1442
+ Label stack = new(entry.StackTrace) { name = StackTraceLabelName };
1443
+ stack.style.marginTop = 6;
1444
+ stack.style.whiteSpace = WhiteSpace.Normal;
1445
+ row.Add(stack);
1446
+ }
1447
+
1448
+ return row;
1449
+ }
1450
+
1451
+ private static VisualElement CreateComponentPanel(
1452
+ IReadOnlyList<ComponentMonitorEntry> componentEntries
1453
+ )
1454
+ {
1455
+ VisualElement panel = new() { name = ComponentPanelName };
1456
+ DxMessagingEditorTheme.ApplyCompleteBorder(panel, DxMessagingEditorPalette.BorderPanel);
1457
+ panel.style.marginTop = 10;
1458
+ panel.style.paddingTop = 8;
1459
+ panel.style.paddingRight = 8;
1460
+ panel.style.paddingBottom = 8;
1461
+ panel.style.paddingLeft = 8;
1462
+
1463
+ Label title = new($"Component Diagnostics ({componentEntries.Count})");
1464
+ title.AddToClassList(DxMessagingEditorTheme.CardLabelClassName);
1465
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
1466
+ panel.Add(title);
1467
+
1468
+ if (componentEntries.Count == 0)
1469
+ {
1470
+ Label empty = new("No MessagingComponent instances are loaded in open scenes.")
1471
+ {
1472
+ name = ComponentEmptyStateLabelName,
1473
+ };
1474
+ empty.AddToClassList(DxMessagingEditorTheme.EmptyBodyClassName);
1475
+ empty.style.whiteSpace = WhiteSpace.Normal;
1476
+ empty.style.marginTop = 6;
1477
+ panel.Add(empty);
1478
+ return panel;
1479
+ }
1480
+
1481
+ ScrollView componentScroll = new(ScrollViewMode.Vertical)
1482
+ {
1483
+ name = ComponentScrollViewName,
1484
+ };
1485
+ componentScroll.style.maxHeight = 180;
1486
+ componentScroll.style.marginTop = 2;
1487
+ panel.Add(componentScroll);
1488
+
1489
+ foreach (ComponentMonitorEntry componentEntry in componentEntries)
1490
+ {
1491
+ componentScroll.Add(CreateComponentRow(componentEntry));
1492
+ }
1493
+
1494
+ return panel;
1495
+ }
1496
+
1497
+ private static VisualElement CreateComponentRow(ComponentMonitorEntry componentEntry)
1498
+ {
1499
+ VisualElement row = new();
1500
+ row.AddToClassList(ComponentRowClassName);
1501
+ row.AddToClassList(DxMessagingEditorTheme.CardClassName);
1502
+ DxMessagingEditorTheme.ApplyCompleteBorder(row, DxMessagingEditorPalette.Amber);
1503
+ row.style.marginTop = 8;
1504
+ row.style.paddingTop = 8;
1505
+ row.style.paddingRight = 8;
1506
+ row.style.paddingBottom = 8;
1507
+ row.style.paddingLeft = 10;
1508
+
1509
+ string activeText = componentEntry.ActiveInHierarchy ? "active" : "inactive";
1510
+ Label name = new(
1511
+ $"{componentEntry.HierarchyPath} ({componentEntry.ComponentTypeName}, {activeText})"
1512
+ )
1513
+ {
1514
+ name = ComponentNameLabelName,
1515
+ };
1516
+ name.style.unityFontStyleAndWeight = FontStyle.Bold;
1517
+ row.Add(name);
1518
+
1519
+ Label summary = new(
1520
+ $"Listeners: {componentEntry.ListenerCount} ({componentEntry.EnabledListenerCount} enabled, {componentEntry.DiagnosticsListenerCount} diagnostics) | Registrations: {componentEntry.RegistrationCount} | Calls: {componentEntry.CallCount} | Local messages: {componentEntry.LocalEmissionCount}"
1521
+ )
1522
+ {
1523
+ name = ComponentSummaryLabelName,
1524
+ };
1525
+ summary.style.marginTop = 2;
1526
+ summary.style.whiteSpace = WhiteSpace.Normal;
1527
+ row.Add(summary);
1528
+
1529
+ Label provider = new(componentEntry.ProviderStatusText)
1530
+ {
1531
+ name = ComponentProviderLabelName,
1532
+ };
1533
+ provider.style.marginTop = 2;
1534
+ provider.style.whiteSpace = WhiteSpace.Normal;
1535
+ row.Add(provider);
1536
+
1537
+ if (!string.IsNullOrWhiteSpace(componentEntry.WarningText))
1538
+ {
1539
+ Label warning = new(componentEntry.WarningText)
1540
+ {
1541
+ name = ComponentWarningLabelName,
1542
+ };
1543
+ warning.AddToClassList(DxMessagingEditorTheme.WarningClassName);
1544
+ warning.style.marginTop = 4;
1545
+ warning.style.whiteSpace = WhiteSpace.Normal;
1546
+ row.Add(warning);
1547
+ }
1548
+
1549
+ return row;
1550
+ }
1551
+
1552
+ private static VisualElement CreateDetailsPane(MessageMonitorEntry entry)
1553
+ {
1554
+ VisualElement details = new() { name = DetailsPaneName };
1555
+ details.AddToClassList(DxMessagingEditorTheme.CardClassName);
1556
+ details.style.borderTopWidth = 1;
1557
+ details.style.borderTopColor = DxMessagingEditorPalette.BorderPanel;
1558
+ details.style.marginTop = 8;
1559
+ details.style.paddingTop = 8;
1560
+
1561
+ Label title = new("Details");
1562
+ title.AddToClassList(DxMessagingEditorTheme.CardLabelClassName);
1563
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
1564
+ details.Add(title);
1565
+
1566
+ Label type = new($"Message: {entry.MessageTypeName}") { name = DetailsTypeLabelName };
1567
+ type.style.marginTop = 4;
1568
+ details.Add(type);
1569
+
1570
+ Label context = new(entry.ContextText) { name = DetailsContextLabelName };
1571
+ context.style.marginTop = 2;
1572
+ details.Add(context);
1573
+
1574
+ string stackText = string.IsNullOrWhiteSpace(entry.StackTrace)
1575
+ ? "Stack trace: not captured"
1576
+ : entry.StackTrace;
1577
+ Label stack = new(stackText) { name = DetailsStackTraceLabelName };
1578
+ stack.style.marginTop = 6;
1579
+ stack.style.whiteSpace = WhiteSpace.Normal;
1580
+ details.Add(stack);
1581
+
1582
+ return details;
1583
+ }
1584
+
1585
+ private static int ClampSelectedIndex(int selectedEntryIndex, int entryCount)
1586
+ {
1587
+ if (entryCount <= 0)
1588
+ {
1589
+ return 0;
1590
+ }
1591
+ if (selectedEntryIndex < 0)
1592
+ {
1593
+ return 0;
1594
+ }
1595
+ return selectedEntryIndex >= entryCount ? entryCount - 1 : selectedEntryIndex;
1596
+ }
1597
+
1598
+ private static void AppendJsonProperty(
1599
+ StringBuilder builder,
1600
+ string name,
1601
+ string value,
1602
+ bool trailingComma
1603
+ )
1604
+ {
1605
+ builder
1606
+ .Append(" \"")
1607
+ .Append(name)
1608
+ .Append("\": \"")
1609
+ .Append(EscapeJson(value))
1610
+ .Append("\"");
1611
+ if (trailingComma)
1612
+ {
1613
+ builder.Append(",");
1614
+ }
1615
+ builder.AppendLine();
1616
+ }
1617
+
1618
+ private static string EscapeJson(string value)
1619
+ {
1620
+ if (string.IsNullOrEmpty(value))
1621
+ {
1622
+ return string.Empty;
1623
+ }
1624
+
1625
+ StringBuilder builder = new(value.Length + 8);
1626
+ foreach (char c in value)
1627
+ {
1628
+ switch (c)
1629
+ {
1630
+ case '\\':
1631
+ builder.Append("\\\\");
1632
+ break;
1633
+ case '"':
1634
+ builder.Append("\\\"");
1635
+ break;
1636
+ case '\n':
1637
+ builder.Append("\\n");
1638
+ break;
1639
+ case '\r':
1640
+ builder.Append("\\r");
1641
+ break;
1642
+ case '\t':
1643
+ builder.Append("\\t");
1644
+ break;
1645
+ default:
1646
+ if (char.IsControl(c))
1647
+ {
1648
+ builder.Append("\\u").Append(((int)c).ToString("x4"));
1649
+ }
1650
+ else
1651
+ {
1652
+ builder.Append(c);
1653
+ }
1654
+ break;
1655
+ }
1656
+ }
1657
+ return builder.ToString();
1658
+ }
1659
+ }
1660
+
1661
+ internal readonly struct MessageMonitorViewState
1662
+ {
1663
+ internal static MessageMonitorViewState Default { get; } = new();
1664
+
1665
+ internal MessageMonitorViewState(string filterText = "", int selectedEntryIndex = 0)
1666
+ {
1667
+ FilterText = filterText ?? string.Empty;
1668
+ SelectedEntryIndex = selectedEntryIndex;
1669
+ }
1670
+
1671
+ internal string FilterText { get; }
1672
+
1673
+ internal int SelectedEntryIndex { get; }
1674
+ }
1675
+
1676
+ internal readonly struct ComponentMonitorEntry
1677
+ {
1678
+ internal ComponentMonitorEntry(
1679
+ string hierarchyPath,
1680
+ string componentTypeName,
1681
+ bool activeInHierarchy,
1682
+ int listenerCount,
1683
+ int enabledListenerCount,
1684
+ int diagnosticsListenerCount,
1685
+ int registrationCount,
1686
+ int callCount,
1687
+ int localEmissionCount,
1688
+ string providerStatusText,
1689
+ string warningText
1690
+ )
1691
+ {
1692
+ HierarchyPath = hierarchyPath ?? string.Empty;
1693
+ ComponentTypeName = componentTypeName ?? string.Empty;
1694
+ ActiveInHierarchy = activeInHierarchy;
1695
+ ListenerCount = listenerCount;
1696
+ EnabledListenerCount = enabledListenerCount;
1697
+ DiagnosticsListenerCount = diagnosticsListenerCount;
1698
+ RegistrationCount = registrationCount;
1699
+ CallCount = callCount;
1700
+ LocalEmissionCount = localEmissionCount;
1701
+ ProviderStatusText = providerStatusText ?? string.Empty;
1702
+ WarningText = warningText ?? string.Empty;
1703
+ }
1704
+
1705
+ internal string HierarchyPath { get; }
1706
+
1707
+ internal string ComponentTypeName { get; }
1708
+
1709
+ internal bool ActiveInHierarchy { get; }
1710
+
1711
+ internal int ListenerCount { get; }
1712
+
1713
+ internal int EnabledListenerCount { get; }
1714
+
1715
+ internal int DiagnosticsListenerCount { get; }
1716
+
1717
+ internal int RegistrationCount { get; }
1718
+
1719
+ internal int CallCount { get; }
1720
+
1721
+ internal int LocalEmissionCount { get; }
1722
+
1723
+ internal string ProviderStatusText { get; }
1724
+
1725
+ internal string WarningText { get; }
1726
+ }
1727
+
1728
+ internal readonly struct MessageMonitorTypeLane
1729
+ {
1730
+ internal MessageMonitorTypeLane(
1731
+ string messageTypeName,
1732
+ int entryCount,
1733
+ IReadOnlyList<string> contexts
1734
+ )
1735
+ {
1736
+ MessageTypeName = string.IsNullOrWhiteSpace(messageTypeName)
1737
+ ? "<unknown>"
1738
+ : messageTypeName.Trim();
1739
+ EntryCount = entryCount;
1740
+ Contexts = contexts ?? Array.Empty<string>();
1741
+ }
1742
+
1743
+ internal string MessageTypeName { get; }
1744
+
1745
+ internal int EntryCount { get; }
1746
+
1747
+ internal IReadOnlyList<string> Contexts { get; }
1748
+
1749
+ internal int ContextCount => Contexts.Count;
1750
+
1751
+ internal string ContextsText => Contexts.Count == 0 ? "none" : string.Join(", ", Contexts);
1752
+ }
1753
+
1754
+ internal readonly struct MessageMonitorContextLane
1755
+ {
1756
+ internal MessageMonitorContextLane(
1757
+ string contextText,
1758
+ int entryCount,
1759
+ IReadOnlyList<string> messageTypes
1760
+ )
1761
+ {
1762
+ ContextText = string.IsNullOrWhiteSpace(contextText)
1763
+ ? "Context: none"
1764
+ : contextText.Trim();
1765
+ EntryCount = entryCount;
1766
+ MessageTypes = messageTypes ?? Array.Empty<string>();
1767
+ }
1768
+
1769
+ internal string ContextText { get; }
1770
+
1771
+ internal int EntryCount { get; }
1772
+
1773
+ internal IReadOnlyList<string> MessageTypes { get; }
1774
+
1775
+ internal int MessageTypeCount => MessageTypes.Count;
1776
+
1777
+ internal string MessageTypesText =>
1778
+ MessageTypes.Count == 0 ? "none" : string.Join(", ", MessageTypes);
1779
+ }
1780
+
1781
+ internal readonly struct MessageMonitorSnapshot
1782
+ {
1783
+ internal MessageMonitorSnapshot(
1784
+ bool diagnosticsEnabled,
1785
+ int capacity,
1786
+ IReadOnlyList<MessageMonitorEntry> entries,
1787
+ bool available = true,
1788
+ string unavailableReason = ""
1789
+ )
1790
+ {
1791
+ DiagnosticsEnabled = diagnosticsEnabled;
1792
+ Capacity = capacity;
1793
+ Entries = entries ?? throw new ArgumentNullException(nameof(entries));
1794
+ Available = available;
1795
+ UnavailableReason = unavailableReason ?? string.Empty;
1796
+ }
1797
+
1798
+ internal bool Available { get; }
1799
+
1800
+ internal bool DiagnosticsEnabled { get; }
1801
+
1802
+ internal int Capacity { get; }
1803
+
1804
+ internal IReadOnlyList<MessageMonitorEntry> Entries { get; }
1805
+
1806
+ internal string UnavailableReason { get; }
1807
+
1808
+ internal static MessageMonitorSnapshot Unavailable(string reason)
1809
+ {
1810
+ return new MessageMonitorSnapshot(
1811
+ diagnosticsEnabled: false,
1812
+ capacity: 0,
1813
+ entries: Array.Empty<MessageMonitorEntry>(),
1814
+ available: false,
1815
+ unavailableReason: reason
1816
+ );
1817
+ }
1818
+ }
1819
+
1820
+ internal readonly struct MessageMonitorEntry
1821
+ {
1822
+ private const string EmptyContextText = "Context: none";
1823
+
1824
+ internal MessageMonitorEntry(
1825
+ string messageTypeName,
1826
+ string contextText,
1827
+ string stackTrace,
1828
+ string messageTypeIdentity = null,
1829
+ string messageTypeDisplayPath = null,
1830
+ string routeKind = null
1831
+ )
1832
+ {
1833
+ MessageTypeName = messageTypeName;
1834
+ MessageTypeIdentity = string.IsNullOrWhiteSpace(messageTypeIdentity)
1835
+ ? messageTypeName
1836
+ : messageTypeIdentity;
1837
+ MessageTypeDisplayPath = string.IsNullOrWhiteSpace(messageTypeDisplayPath)
1838
+ ? messageTypeName
1839
+ : messageTypeDisplayPath;
1840
+ ContextText = contextText;
1841
+ StackTrace = stackTrace;
1842
+ RouteKind = routeKind ?? string.Empty;
1843
+ }
1844
+
1845
+ internal string MessageTypeName { get; }
1846
+
1847
+ internal string MessageTypeIdentity { get; }
1848
+
1849
+ internal string MessageTypeDisplayPath { get; }
1850
+
1851
+ internal string ContextText { get; }
1852
+
1853
+ internal string StackTrace { get; }
1854
+
1855
+ internal string RouteKind { get; }
1856
+
1857
+ internal static MessageMonitorEntry FromEmission(MessageEmissionData emission)
1858
+ {
1859
+ Type messageType = emission.message?.MessageType;
1860
+ string typeName = messageType == null ? "<unknown>" : messageType.Name;
1861
+ string typeIdentity = CreateMessageTypeIdentity(messageType, typeName);
1862
+ string typeDisplayPath = CreateMessageTypeDisplayPath(messageType, typeName);
1863
+ string contextText = FormatContext(emission.context);
1864
+ return new MessageMonitorEntry(
1865
+ typeName,
1866
+ contextText,
1867
+ emission.stackTrace ?? string.Empty,
1868
+ typeIdentity,
1869
+ typeDisplayPath,
1870
+ CreateRouteKind(messageType)
1871
+ );
1872
+ }
1873
+
1874
+ private static string CreateRouteKind(Type messageType)
1875
+ {
1876
+ if (messageType == null)
1877
+ {
1878
+ return string.Empty;
1879
+ }
1880
+ if (typeof(IUntargetedMessage).IsAssignableFrom(messageType))
1881
+ {
1882
+ return DxMessagingEditorPalette.UntargetedKind;
1883
+ }
1884
+ if (typeof(ITargetedMessage).IsAssignableFrom(messageType))
1885
+ {
1886
+ return DxMessagingEditorPalette.TargetedKind;
1887
+ }
1888
+ if (typeof(IBroadcastMessage).IsAssignableFrom(messageType))
1889
+ {
1890
+ return DxMessagingEditorPalette.BroadcastKind;
1891
+ }
1892
+ return string.Empty;
1893
+ }
1894
+
1895
+ internal bool Matches(string filterText)
1896
+ {
1897
+ if (string.IsNullOrWhiteSpace(filterText))
1898
+ {
1899
+ return true;
1900
+ }
1901
+
1902
+ if (
1903
+ !MessageMonitorFilterQuery.TryCreateTerms(
1904
+ filterText,
1905
+ out MessageMonitorFilterTerm[] terms
1906
+ )
1907
+ )
1908
+ {
1909
+ return ContainsAnyField(filterText);
1910
+ }
1911
+
1912
+ return terms.All(MatchesFilterTerm);
1913
+ }
1914
+
1915
+ private bool MatchesFilterTerm(MessageMonitorFilterTerm term)
1916
+ {
1917
+ if (string.IsNullOrWhiteSpace(term.Text))
1918
+ {
1919
+ return true;
1920
+ }
1921
+
1922
+ switch (term.Facet)
1923
+ {
1924
+ case MessageMonitorFilterFacet.MessageType:
1925
+ return Contains(MessageTypeName, term.Text)
1926
+ || Contains(MessageTypeDisplayPath, term.Text);
1927
+ case MessageMonitorFilterFacet.Context:
1928
+ if (term.Exact)
1929
+ {
1930
+ return string.Equals(
1931
+ ContextText,
1932
+ term.Text,
1933
+ StringComparison.OrdinalIgnoreCase
1934
+ );
1935
+ }
1936
+
1937
+ return Contains(ContextText, term.Text);
1938
+ case MessageMonitorFilterFacet.Stack:
1939
+ return Contains(StackTrace, term.Text);
1940
+ default:
1941
+ return ContainsAnyField(term.Text);
1942
+ }
1943
+ }
1944
+
1945
+ private bool ContainsAnyField(string filterText)
1946
+ {
1947
+ return Contains(MessageTypeName, filterText)
1948
+ || Contains(MessageTypeDisplayPath, filterText)
1949
+ || Contains(ContextText, filterText)
1950
+ || Contains(StackTrace, filterText);
1951
+ }
1952
+
1953
+ private static string CreateMessageTypeIdentity(Type messageType, string fallback)
1954
+ {
1955
+ if (messageType == null)
1956
+ {
1957
+ return fallback;
1958
+ }
1959
+
1960
+ return messageType.AssemblyQualifiedName ?? messageType.FullName ?? fallback;
1961
+ }
1962
+
1963
+ private static string CreateMessageTypeDisplayPath(Type messageType, string fallback)
1964
+ {
1965
+ if (messageType == null)
1966
+ {
1967
+ return fallback;
1968
+ }
1969
+
1970
+ return (messageType.FullName ?? fallback).Replace('+', '.');
1971
+ }
1972
+
1973
+ private static bool Contains(string value, string filterText)
1974
+ {
1975
+ return !string.IsNullOrEmpty(value)
1976
+ && value.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) >= 0;
1977
+ }
1978
+
1979
+ private static string FormatContext(InstanceId? context)
1980
+ {
1981
+ if (!context.HasValue)
1982
+ {
1983
+ return EmptyContextText;
1984
+ }
1985
+
1986
+ InstanceId instanceId = context.Value;
1987
+ #if UNITY_2021_3_OR_NEWER
1988
+ UnityEngine.Object unityObject = instanceId.Object;
1989
+ if (unityObject != null)
1990
+ {
1991
+ return $"Context: {unityObject.name} ({instanceId.Id})";
1992
+ }
1993
+ #endif
1994
+ return $"Context: {instanceId.Id}";
1995
+ }
1996
+ }
1997
+
1998
+ internal enum MessageMonitorFilterFacet
1999
+ {
2000
+ Any,
2001
+ MessageType,
2002
+ Context,
2003
+ Stack,
2004
+ }
2005
+
2006
+ internal static class MessageMonitorFilterQuery
2007
+ {
2008
+ internal static bool TryCreateTerms(string filterText, out MessageMonitorFilterTerm[] terms)
2009
+ {
2010
+ string[] tokens = SplitFilterTokens(filterText);
2011
+ if (tokens.Length == 0)
2012
+ {
2013
+ terms = Array.Empty<MessageMonitorFilterTerm>();
2014
+ return false;
2015
+ }
2016
+
2017
+ List<MessageMonitorFilterTerm> parsedTerms = new(tokens.Length);
2018
+ foreach (string token in tokens)
2019
+ {
2020
+ if (!TryCreateFilterTerm(token, out MessageMonitorFilterTerm term, out _))
2021
+ {
2022
+ terms = Array.Empty<MessageMonitorFilterTerm>();
2023
+ return false;
2024
+ }
2025
+
2026
+ parsedTerms.Add(term);
2027
+ }
2028
+
2029
+ terms = parsedTerms.ToArray();
2030
+ return true;
2031
+ }
2032
+
2033
+ internal static bool TryCreateDisplayTokens(string filterText, out string[] displayTokens)
2034
+ {
2035
+ string[] tokens = SplitFilterTokens(filterText);
2036
+ if (tokens.Length == 0)
2037
+ {
2038
+ displayTokens = Array.Empty<string>();
2039
+ return false;
2040
+ }
2041
+
2042
+ List<string> parsedTokens = new(tokens.Length);
2043
+ foreach (string token in tokens)
2044
+ {
2045
+ if (!TryCreateFilterTerm(token, out _, out string displayToken))
2046
+ {
2047
+ displayTokens = Array.Empty<string>();
2048
+ return false;
2049
+ }
2050
+
2051
+ parsedTokens.Add(displayToken);
2052
+ }
2053
+
2054
+ displayTokens = parsedTokens.ToArray();
2055
+ return true;
2056
+ }
2057
+
2058
+ private static string[] SplitFilterTokens(string filterText)
2059
+ {
2060
+ string source = filterText ?? string.Empty;
2061
+ if (source.Length == 0)
2062
+ {
2063
+ return Array.Empty<string>();
2064
+ }
2065
+
2066
+ List<string> tokens = new();
2067
+ StringBuilder current = new();
2068
+ bool quoted = false;
2069
+ bool escaped = false;
2070
+
2071
+ foreach (char character in source)
2072
+ {
2073
+ if (escaped)
2074
+ {
2075
+ current.Append(character);
2076
+ escaped = false;
2077
+ continue;
2078
+ }
2079
+
2080
+ if (quoted && character == '\\')
2081
+ {
2082
+ current.Append(character);
2083
+ escaped = true;
2084
+ continue;
2085
+ }
2086
+
2087
+ if (character == '"')
2088
+ {
2089
+ quoted = !quoted;
2090
+ current.Append(character);
2091
+ continue;
2092
+ }
2093
+
2094
+ if (!quoted && char.IsWhiteSpace(character))
2095
+ {
2096
+ AddCurrentToken(tokens, current);
2097
+ continue;
2098
+ }
2099
+
2100
+ current.Append(character);
2101
+ }
2102
+
2103
+ AddCurrentToken(tokens, current);
2104
+ return tokens.ToArray();
2105
+ }
2106
+
2107
+ private static void AddCurrentToken(List<string> tokens, StringBuilder current)
2108
+ {
2109
+ if (current.Length == 0)
2110
+ {
2111
+ return;
2112
+ }
2113
+
2114
+ tokens.Add(current.ToString());
2115
+ current.Clear();
2116
+ }
2117
+
2118
+ private static bool TryCreateFilterTerm(
2119
+ string token,
2120
+ out MessageMonitorFilterTerm term,
2121
+ out string displayToken
2122
+ )
2123
+ {
2124
+ int separatorIndex = token.IndexOf(':');
2125
+ if (separatorIndex <= 0)
2126
+ {
2127
+ term = default;
2128
+ displayToken = string.Empty;
2129
+ return false;
2130
+ }
2131
+
2132
+ string prefix = token.Substring(0, separatorIndex);
2133
+ string rawValue = token.Substring(separatorIndex + 1);
2134
+ if (
2135
+ !TryCreateFilterValue(rawValue, out string value, out bool exact)
2136
+ || !TryCreateFilterFacet(prefix, out MessageMonitorFilterFacet facet)
2137
+ )
2138
+ {
2139
+ term = default;
2140
+ displayToken = string.Empty;
2141
+ return false;
2142
+ }
2143
+
2144
+ term = new MessageMonitorFilterTerm(facet, value, exact);
2145
+ displayToken =
2146
+ $"{CreateFacetDisplayPrefix(prefix, facet)}:{CreateFilterValueDisplay(value, exact)}";
2147
+ return true;
2148
+ }
2149
+
2150
+ private static bool TryCreateFilterValue(string rawValue, out string value, out bool exact)
2151
+ {
2152
+ string trimmedValue = rawValue?.Trim() ?? string.Empty;
2153
+ value = string.Empty;
2154
+ exact = false;
2155
+ if (string.IsNullOrWhiteSpace(trimmedValue))
2156
+ {
2157
+ return false;
2158
+ }
2159
+
2160
+ bool startsQuoted = trimmedValue[0] == '"';
2161
+ bool endsQuoted = trimmedValue[trimmedValue.Length - 1] == '"';
2162
+ if (startsQuoted || endsQuoted)
2163
+ {
2164
+ if (!startsQuoted || !endsQuoted || trimmedValue.Length < 2)
2165
+ {
2166
+ return false;
2167
+ }
2168
+
2169
+ exact = true;
2170
+ return TryUnescapeQuotedFilterValue(
2171
+ trimmedValue.Substring(1, trimmedValue.Length - 2),
2172
+ out value
2173
+ ) && !string.IsNullOrWhiteSpace(value);
2174
+ }
2175
+
2176
+ value = trimmedValue;
2177
+ return true;
2178
+ }
2179
+
2180
+ private static bool TryUnescapeQuotedFilterValue(string quotedValue, out string value)
2181
+ {
2182
+ StringBuilder builder = new();
2183
+ bool escaped = false;
2184
+
2185
+ foreach (char character in quotedValue ?? string.Empty)
2186
+ {
2187
+ if (escaped)
2188
+ {
2189
+ builder.Append(character);
2190
+ escaped = false;
2191
+ continue;
2192
+ }
2193
+
2194
+ if (character == '\\')
2195
+ {
2196
+ escaped = true;
2197
+ continue;
2198
+ }
2199
+
2200
+ builder.Append(character);
2201
+ }
2202
+
2203
+ value = builder.ToString();
2204
+ return !escaped;
2205
+ }
2206
+
2207
+ private static string CreateFilterValueDisplay(string value, bool exact)
2208
+ {
2209
+ if (!exact && !RequiresQuotedFilterValue(value))
2210
+ {
2211
+ return value;
2212
+ }
2213
+
2214
+ return $"\"{EscapeQuotedFilterValue(value)}\"";
2215
+ }
2216
+
2217
+ private static bool RequiresQuotedFilterValue(string value)
2218
+ {
2219
+ return (value ?? string.Empty).Any(char.IsWhiteSpace);
2220
+ }
2221
+
2222
+ private static string EscapeQuotedFilterValue(string value)
2223
+ {
2224
+ return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
2225
+ }
2226
+
2227
+ private static bool TryCreateFilterFacet(string prefix, out MessageMonitorFilterFacet facet)
2228
+ {
2229
+ switch (prefix?.Trim().ToLowerInvariant())
2230
+ {
2231
+ case "type":
2232
+ case "message":
2233
+ facet = MessageMonitorFilterFacet.MessageType;
2234
+ return true;
2235
+ case "context":
2236
+ facet = MessageMonitorFilterFacet.Context;
2237
+ return true;
2238
+ case "stack":
2239
+ facet = MessageMonitorFilterFacet.Stack;
2240
+ return true;
2241
+ default:
2242
+ facet = MessageMonitorFilterFacet.Any;
2243
+ return false;
2244
+ }
2245
+ }
2246
+
2247
+ private static string CreateFacetDisplayPrefix(
2248
+ string prefix,
2249
+ MessageMonitorFilterFacet facet
2250
+ )
2251
+ {
2252
+ string normalizedPrefix = prefix?.Trim().ToLowerInvariant();
2253
+ if (string.Equals(normalizedPrefix, "message", StringComparison.Ordinal))
2254
+ {
2255
+ return "message";
2256
+ }
2257
+
2258
+ switch (facet)
2259
+ {
2260
+ case MessageMonitorFilterFacet.MessageType:
2261
+ return "type";
2262
+ case MessageMonitorFilterFacet.Context:
2263
+ return "context";
2264
+ case MessageMonitorFilterFacet.Stack:
2265
+ return "stack";
2266
+ default:
2267
+ return normalizedPrefix ?? string.Empty;
2268
+ }
2269
+ }
2270
+ }
2271
+
2272
+ internal readonly struct MessageMonitorFilterTerm
2273
+ {
2274
+ internal MessageMonitorFilterTerm(MessageMonitorFilterFacet facet, string text, bool exact)
2275
+ {
2276
+ Facet = facet;
2277
+ Text = text ?? string.Empty;
2278
+ Exact = exact;
2279
+ }
2280
+
2281
+ internal MessageMonitorFilterFacet Facet { get; }
2282
+
2283
+ internal string Text { get; }
2284
+
2285
+ internal bool Exact { get; }
2286
+ }
2287
+ }
2288
+ #endif