Skip to content

Commit b706d20

Browse files
committed
Refactored JsonExporter to reduce duplicate code and hardcoded strings
Also allow appending to default exported fields on CLI when prefixed with a +
1 parent aac6b39 commit b706d20

4 files changed

Lines changed: 131 additions & 64 deletions

File tree

src/FlaUInspect/App.xaml.cs

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public partial class App {
1313
private void ApplicationStart(object sender, StartupEventArgs e) {
1414
var args = Environment.GetCommandLineArgs();
1515
string? processName = null;
16+
string? processPid = null;
1617
string? exportFile = null;
1718
string? exportOptions = null;
1819
string? errorToFile = null;
@@ -21,6 +22,9 @@ private void ApplicationStart(object sender, StartupEventArgs e) {
2122
if (args[i] == "--process" && i + 1 < args.Length) {
2223
processName = args[++i];
2324
}
25+
if (args[i] == "--pid" && i + 1 < args.Length) {
26+
processPid = args[++i];
27+
}
2428
if (args[i] == "--export_json") {
2529
if (i + 1 < args.Length && !args[i + 1].StartsWith("-")) {
2630
exportFile = args[++i];
@@ -29,40 +33,43 @@ private void ApplicationStart(object sender, StartupEventArgs e) {
2933
}
3034
}
3135
if (args[i] == "--export_json_options" && i + 1 < args.Length) {
32-
exportOptions = args[++i];
36+
exportOptions = args[++i].Trim();
3337
}
3438
if (args[i] == "--error_file" && i + 1 < args.Length) {
3539
errorToFile = args[++i];
3640
}
3741
}
3842

39-
if (processName != null && exportFile != null) {
43+
if ((!String.IsNullOrWhiteSpace(processName) || !String.IsNullOrWhiteSpace(processPid)) && exportFile != null) {
4044
try {
4145
using var automation = new FlaUI.UIA3.UIA3Automation();
42-
var process = System.Diagnostics.Process.GetProcessesByName(processName).FirstOrDefault();
46+
var process = String.IsNullOrWhiteSpace(processPid) ? System.Diagnostics.Process.GetProcessesByName(processName).FirstOrDefault() : System.Diagnostics.Process.GetProcessById(int.Parse(processPid));
4347
if (process == null)
44-
throw new ArgumentException("Process: " + processName + " not found");
48+
throw new ArgumentException($"Process: {processName} {processPid} not found");
4549

4650
var app = FlaUI.Core.Application.Attach(process);
4751
var window = app.GetMainWindow(automation);
4852
if (window != null) {
49-
System.Collections.Generic.HashSet<string>? options = null;
53+
HashSet<string> options = new(Core.JsonExporter.DefaultOptions.Select(a => a.ToString()), StringComparer.OrdinalIgnoreCase);
5054
if (exportOptions != null) {
51-
var parts = exportOptions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
52-
if (parts.Length > 0) {
53-
options = new System.Collections.Generic.HashSet<string>(parts.Select(x => x.Trim()), StringComparer.OrdinalIgnoreCase);
54-
}
55+
if (!exportOptions.StartsWith("+"))
56+
options.Clear();
57+
else
58+
exportOptions = exportOptions.Substring(1);
59+
var parts = exportOptions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
60+
foreach (var part in parts)
61+
options.Add(part);
5562
}
5663

5764
var data = FlaUInspect.Core.JsonExporter.CollectNodeData(window, options);
58-
var json = System.Text.Json.JsonSerializer.Serialize(data, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
65+
var json = FlaUInspect.Core.JsonExporter.SerializeNodeInfo(data);
5966
System.IO.File.WriteAllText(exportFile, json);
6067
}
6168

6269
} catch (Exception ex) {
6370
var msg = $"Error exporting: {ex.Message}";
6471
if (String.IsNullOrWhiteSpace(errorToFile))
65-
MessageBox.Show(msg,"FlaUI CLI Export Failed");
72+
MessageBox.Show(msg, "FlaUI CLI Export Failed");
6673
else
6774
System.IO.File.WriteAllText(errorToFile, msg);
6875
Environment.Exit(1);
@@ -73,33 +80,33 @@ private void ApplicationStart(object sender, StartupEventArgs e) {
7380

7481
AssemblyFileVersionAttribute? versionAttribute = Assembly.GetEntryAssembly()?.GetCustomAttribute(typeof(AssemblyFileVersionAttribute)) as AssemblyFileVersionAttribute;
7582
string applicationVersion = versionAttribute?.Version ?? "N/A";
76-
InternalLogger logger = new ();
83+
InternalLogger logger = new();
84+
7785

78-
7986
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
80-
ChooseVersionWindow dialog = new ();
87+
ChooseVersionWindow dialog = new();
8188
#if AUTOMATION_UIA3
8289
dialog.SelectedAutomationType = AutomationType.UIA3;
8390
#elif AUTOMATION_UIA2
8491
dialog.SelectedAutomationType = AutomationType.UIA2;
8592
#else
86-
if (args.Any(a=>a.Equals("--uia2", StringComparison.OrdinalIgnoreCase)))
87-
dialog.SelectedAutomationType = AutomationType.UIA2;
88-
else if (args.Any(a=>a.Equals("--uia3", StringComparison.OrdinalIgnoreCase)))
89-
dialog.SelectedAutomationType = AutomationType.UIA3;
90-
else if (dialog.ShowDialog() != true)
91-
return;
93+
if (args.Any(a => a.Equals("--uia2", StringComparison.OrdinalIgnoreCase)))
94+
dialog.SelectedAutomationType = AutomationType.UIA2;
95+
else if (args.Any(a => a.Equals("--uia3", StringComparison.OrdinalIgnoreCase)))
96+
dialog.SelectedAutomationType = AutomationType.UIA3;
97+
else if (dialog.ShowDialog() != true)
98+
return;
9299
#endif
93100

94101

95102

96-
MainViewModel mainViewModel = new (dialog.SelectedAutomationType, applicationVersion, logger);
97-
MainWindow mainWindow = new () { DataContext = mainViewModel };
103+
MainViewModel mainViewModel = new(dialog.SelectedAutomationType, applicationVersion, logger);
104+
MainWindow mainWindow = new() { DataContext = mainViewModel };
98105

99-
//Re-enable normal shutdown mode.
100-
Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
101-
Current.MainWindow = mainWindow;
102-
mainWindow.Show();
106+
//Re-enable normal shutdown mode.
107+
Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
108+
Current.MainWindow = mainWindow;
109+
mainWindow.Show();
103110

104111

105112
}
Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,112 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using System.Text.RegularExpressions;
8+
using FlaUI.Core;
49
using FlaUI.Core.AutomationElements;
510
using FlaUInspect.Core.Extensions;
611

712
namespace FlaUInspect.Core;
813

9-
public static class JsonExporter
10-
{
11-
public static Dictionary<string, object> CollectNodeData(AutomationElement element, HashSet<string>? options = null)
14+
public static class JsonExporter {
15+
public static ExportOptions[] DefaultOptions = new[]
1216
{
13-
var dict = new Dictionary<string, object>();
14-
// Default options if none provided
15-
options ??= new HashSet<string> { "ControlType", "ClassName", "Name", "AutomationId" };
16-
17-
if (options.Contains("ControlType")) dict["ControlType"] = element.Properties.ControlType.ValueOrDefault.ToString();
18-
if (options.Contains("ClassName")) dict["ClassName"] = element.Properties.ClassName.ValueOrDefault;
19-
if (options.Contains("Name")) dict["Name"] = element.Properties.Name.ValueOrDefault;
20-
if (options.Contains("AutomationId")) dict["AutomationId"] = element.Properties.AutomationId.ValueOrDefault;
21-
if (options.Contains("HelpText")) dict["HelpText"] = element.Properties.HelpText.ValueOrDefault;
22-
if (options.Contains("BoundingRectangle")) dict["BoundingRectangle"] = element.Properties.BoundingRectangle.ValueOrDefault.ToString();
23-
if (options.Contains("ProcessId")) dict["ProcessId"] = element.Properties.ProcessId.ValueOrDefault;
24-
if (options.Contains("IsEnabled")) dict["IsEnabled"] = element.Properties.IsEnabled.ValueOrDefault;
25-
if (options.Contains("IsOffscreen")) dict["IsOffscreen"] = element.Properties.IsOffscreen.ValueOrDefault;
17+
ExportOptions.ControlType,
18+
ExportOptions.ClassName,
19+
ExportOptions.Name,
20+
ExportOptions.AutomationId
21+
};
22+
public enum ExportOptions {
23+
ControlType,
24+
ClassName,
25+
Name,
26+
AutomationId,
27+
HelpText,
28+
BoundingRectangle,
29+
ProcessId,
30+
IsEnabled,
31+
IsOffscreen,
32+
Value,
33+
SupportedPatterns
34+
}
35+
public class NodeInfo {
36+
public string? ControlType { get; set; }
37+
public string? ClassName { get; set; }
38+
public string? Name { get; set; }
39+
public string? AutomationId { get; set; }
40+
public string? HelpText { get; set; }
41+
public string? BoundingRectangle { get; set; }
42+
public int? ProcessId { get; set; }
43+
public bool? IsEnabled { get; set; }
44+
public bool? IsOffscreen { get; set; }
45+
public string? Value { get; set; }
46+
public string[]? SupportedPatterns { get; set; }
47+
public List<NodeInfo>? Children { get; set; }
48+
}
49+
//"v => node.ControlType = v.ToString()"
50+
private static Regex LambdaToOptionKey = new(@"=>\s*node\.(?<optionKey>\w+)",
51+
RegexOptions.Compiled | RegexOptions.IgnoreCase);
52+
53+
public static void SetPropertyOrNull<T>(HashSet<string> options, AutomationProperty<T> prop, Action<T> OnValue, [CallerArgumentExpression(nameof(OnValue))] string? OptionNameOverride = null) {
54+
try {
55+
var match = LambdaToOptionKey.Match(OptionNameOverride ?? string.Empty);
56+
if (match.Success)
57+
OptionNameOverride = match.Groups["optionKey"].Value;
58+
59+
if (String.IsNullOrWhiteSpace(OptionNameOverride) || !options.Contains(OptionNameOverride))
60+
return;
61+
62+
if (prop.TryGetValue(out var val))
63+
OnValue(val);
64+
} catch { }
65+
}
66+
public static string SerializeNodeInfo(NodeInfo node) {
67+
return JsonSerializer.Serialize(node, new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
68+
69+
}
70+
public static NodeInfo CollectNodeData(AutomationElement element, HashSet<string> options) {
71+
var node = new NodeInfo();
72+
73+
var props = element.Properties;
74+
75+
SetPropertyOrNull(options, props.ControlType, v => node.ControlType = v.ToString());
76+
SetPropertyOrNull(options, props.ClassName, v => node.ClassName = v);
77+
SetPropertyOrNull(options, props.Name, v => node.Name = v);
78+
SetPropertyOrNull(options, props.AutomationId, v => node.AutomationId = v);
79+
SetPropertyOrNull(options, props.HelpText, v => node.HelpText = v);
80+
SetPropertyOrNull(options, props.BoundingRectangle, v => node.BoundingRectangle = v.ToString());
81+
SetPropertyOrNull(options, props.ProcessId, v => node.ProcessId = v);
82+
SetPropertyOrNull(options, props.IsEnabled, v => node.IsEnabled = v);
83+
SetPropertyOrNull(options, props.IsOffscreen, v => node.IsOffscreen = v);
84+
try {
85+
if (options.Contains("Value") && element.Patterns.Value.TryGetPattern(out var valuePattern)) {
86+
SetPropertyOrNull(options, valuePattern.Value, v => node.Value = v);
87+
}
88+
} catch { }
2689

2790
if (options.Contains("SupportedPatterns")) {
2891
try {
2992
var patterns = element.GetSupportedPatterns();
30-
dict["SupportedPatterns"] = patterns.Select(p => p.Name)
93+
node.SupportedPatterns = patterns.Select(p => p.Name)
3194
.Where(n => n != "LegacyIAccessible" && n != "LegacyIAccessiblePattern")
3295
.OrderBy(x => x).ToArray();
3396
} catch { }
3497
}
3598

36-
var children = new List<Dictionary<string, object>>();
99+
var children = new List<NodeInfo>();
37100
try {
38101
foreach (var child in element.FindAllChildren()) {
39102
children.Add(CollectNodeData(child, options));
40103
}
41104
} catch { } // Ignore errors fetching children
42105

43106
if (children.Count > 0) {
44-
dict["Children"] = children;
107+
node.Children = children;
45108
}
46109

47-
return dict;
110+
return node;
48111
}
49112
}

src/FlaUInspect/ViewModels/ElementViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ private MenuItem CreateMenuItem(string header, Action value) {
117117
if (AutomationElement == null) return;
118118
var saveFileDialog = new SaveFileDialog { Filter = "JSON files (*.json)|*.json" };
119119
if (saveFileDialog.ShowDialog() == true) {
120-
var options = ExportOptions.Where(x => x.IsChecked).Select(x => x.Header).ToHashSet();
120+
var options = ExportOptions.Where(x => x.IsChecked).Select(x => x.Header).ToHashSet(StringComparer.OrdinalIgnoreCase);
121121
var rootNode = JsonExporter.CollectNodeData(AutomationElement, options);
122-
var json = JsonSerializer.Serialize(rootNode, new JsonSerializerOptions { WriteIndented = true });
122+
var json = FlaUInspect.Core.JsonExporter.SerializeNodeInfo(rootNode);
123123
File.WriteAllText(saveFileDialog.FileName, json);
124124
}
125125
});
Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1+
using FlaUInspect.Core;
12
using System.Collections.ObjectModel;
23

34
namespace FlaUInspect.ViewModels;
45

5-
public static class ExportConfiguration
6-
{
7-
public static ObservableCollection<ExportOptionItem> Options { get; } = new()
8-
{
9-
new ExportOptionItem { Header = "ControlType", IsChecked = true },
10-
new ExportOptionItem { Header = "ClassName", IsChecked = true },
11-
new ExportOptionItem { Header = "Name", IsChecked = true },
12-
new ExportOptionItem { Header = "AutomationId", IsChecked = true },
13-
new ExportOptionItem { Header = "HelpText", IsChecked = false },
14-
new ExportOptionItem { Header = "BoundingRectangle", IsChecked = false },
15-
new ExportOptionItem { Header = "ProcessId", IsChecked = false },
16-
new ExportOptionItem { Header = "IsEnabled", IsChecked = false },
17-
new ExportOptionItem { Header = "IsOffscreen", IsChecked = false },
18-
new ExportOptionItem { Header = "SupportedPatterns", IsChecked = false }
19-
};
6+
public static class ExportConfiguration {
7+
public static ObservableCollection<ExportOptionItem> Options { get; } = GetOptions();
8+
9+
private static ObservableCollection<ExportOptionItem> GetOptions() {
10+
var ret = new ObservableCollection<ExportOptionItem>();
11+
foreach (var itm in Enum.GetValues<JsonExporter.ExportOptions>()) {
12+
ret.Add(new ExportOptionItem { Header = itm.ToString(), IsChecked = JsonExporter.DefaultOptions.Contains(itm) });
13+
}
14+
return ret;
15+
}
16+
2017
}

0 commit comments

Comments
 (0)