From 6298316c947ada78c9082ec2b4478cd187f9c6ce Mon Sep 17 00:00:00 2001 From: F K <54195004+fredjk-gh@users.noreply.github.com> Date: Sun, 26 Feb 2023 09:50:59 -0500 Subject: [PATCH] Implement Tab separated export format (#103) Tab separated output facilitates much more efficient opening of exported content in a spreadsheet for further data analysis, furthermore, it vastly reduces file size at the cost of human-readability. However, for the spreadsheet junkies among us, this is a welcome option. To complement this, there is now an "Export Options" expander in the Core settings panel. Here one can select their preferred export style (default remains Fixed width) and now one can pre-select the export folder. With this change, clicking export *will not* prompt for an export folder if one is already set. It will simply immediately export data. Enjoy! --- ObservatoryCore/Properties/Core.Designer.cs | 14 +- ObservatoryCore/Properties/Core.settings | 3 + .../UI/ViewModels/CoreViewModel.cs | 200 +++++++++++------- ObservatoryCore/UI/Views/BasicUIView.axaml.cs | 107 +++++++++- 4 files changed, 243 insertions(+), 81 deletions(-) diff --git a/ObservatoryCore/Properties/Core.Designer.cs b/ObservatoryCore/Properties/Core.Designer.cs index 96eba62..bca9546 100644 --- a/ObservatoryCore/Properties/Core.Designer.cs +++ b/ObservatoryCore/Properties/Core.Designer.cs @@ -12,7 +12,7 @@ namespace Observatory.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.1.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.2.0.0")] internal sealed partial class Core : global::System.Configuration.ApplicationSettingsBase { private static Core defaultInstance = ((Core)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Core()))); @@ -274,5 +274,17 @@ namespace Observatory.Properties { this["StartReadAll"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("Fixed width")] + public string ExportStyle { + get { + return ((string)(this["ExportStyle"])); + } + set { + this["ExportStyle"] = value; + } + } } } diff --git a/ObservatoryCore/Properties/Core.settings b/ObservatoryCore/Properties/Core.settings index 37938c1..77b31db 100644 --- a/ObservatoryCore/Properties/Core.settings +++ b/ObservatoryCore/Properties/Core.settings @@ -65,5 +65,8 @@ False + + Fixed width + \ No newline at end of file diff --git a/ObservatoryCore/UI/ViewModels/CoreViewModel.cs b/ObservatoryCore/UI/ViewModels/CoreViewModel.cs index 219814d..9dd4a87 100644 --- a/ObservatoryCore/UI/ViewModels/CoreViewModel.cs +++ b/ObservatoryCore/UI/ViewModels/CoreViewModel.cs @@ -112,95 +112,67 @@ namespace Observatory.UI.ViewModels try { var exportFolder = Properties.Core.Default.ExportFolder; - if (string.IsNullOrEmpty(exportFolder)) { exportFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + + OpenFolderDialog openFolderDialog = new() + { + Directory = exportFolder + }; + + var application = (IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current.ApplicationLifetime; + + var selectedFolder = await openFolderDialog.ShowAsync(application.MainWindow); + + if (!string.IsNullOrEmpty(selectedFolder)) + { + Properties.Core.Default.ExportFolder = selectedFolder; + Properties.Core.Default.Save(); + exportFolder = selectedFolder; + } } - OpenFolderDialog openFolderDialog = new() + var exportStyle = Properties.Core.Default.ExportStyle; + if (string.IsNullOrEmpty(exportStyle)) { - Directory = exportFolder - }; - - var application = (IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current.ApplicationLifetime; - - var selectedFolder = await openFolderDialog.ShowAsync(application.MainWindow); - - if (!string.IsNullOrEmpty(selectedFolder)) - { - Properties.Core.Default.ExportFolder = selectedFolder; + exportStyle = "Fixed width"; + Properties.Core.Default.ExportStyle = exportStyle; Properties.Core.Default.Save(); - exportFolder = selectedFolder; + } - foreach (var tab in tabs.Where(t => t.Name != "Core")) + foreach (var tab in tabs.Where(t => t.Name != "Core")) + { + var ui = (BasicUIViewModel)tab.UI; + List selectedData; + bool specificallySelected = ui.SelectedItems?.Count > 1; + + if (specificallySelected) { - var ui = (BasicUIViewModel)tab.UI; - List selectedData; - bool specificallySelected = ui.SelectedItems?.Count > 1; + selectedData = new(); - if (specificallySelected) - { - selectedData = new(); - - foreach (var item in ui.SelectedItems) - selectedData.Add(item); - } - else - { - selectedData = ui.BasicUIGrid.ToList(); - } - - var columns = selectedData[0].GetType().GetProperties(); - Dictionary colSize = new(); - Dictionary> colContent = new(); - - foreach (var column in columns) - { - colSize.Add(column.Name, 0); - colContent.Add(column.Name, new()); - } - - foreach (var line in selectedData) - { - var lineType = line.GetType(); // some plugins have different line types, so don't move this out of loop - foreach (var column in colContent) - { - var cellValue = lineType.GetProperty(column.Key)?.GetValue(line)?.ToString() ?? string.Empty; - column.Value.Add(cellValue); - if (colSize[column.Key] < cellValue.Length) - colSize[column.Key] = cellValue.Length; - } - } - - System.Text.StringBuilder exportData = new(); - - - foreach (var colTitle in colContent.Keys) - { - if (colSize[colTitle] < colTitle.Length) - colSize[colTitle] = colTitle.Length; - - exportData.Append(colTitle.PadRight(colSize[colTitle]) + " "); - } - exportData.AppendLine(); - - for (int i = 0; i < colContent.First().Value.Count; i++) - { - foreach (var column in colContent) - { - if (column.Value[i].Length > 0 && !char.IsNumber(column.Value[i][0]) && column.Value[i].Count(char.IsLetter) / (float)column.Value[i].Length > 0.25) - exportData.Append(column.Value[i].PadRight(colSize[column.Key]) + " "); - else - exportData.Append(column.Value[i].PadLeft(colSize[column.Key]) + " "); - } - exportData.AppendLine(); - } - - string exportPath = $"{exportFolder}{System.IO.Path.DirectorySeparatorChar}Observatory Export - {DateTime.UtcNow:yyyyMMdd-HHmmss} - {tab.Name}.txt"; - - System.IO.File.WriteAllText(exportPath, exportData.ToString()); + foreach (var item in ui.SelectedItems) + selectedData.Add(item); } + else + { + selectedData = ui.BasicUIGrid.ToList(); + } + + System.Text.StringBuilder exportData; + switch (exportStyle) + { + case "Tab separated": + exportData = ExportTabSeparated(selectedData); + break; + default: // Fixed width. + exportData = ExportFixedWidth(selectedData); + break; + } + + string exportPath = $"{exportFolder}{System.IO.Path.DirectorySeparatorChar}Observatory Export - {DateTime.UtcNow:yyyyMMdd-HHmmss} - {tab.Name}.txt"; + + System.IO.File.WriteAllText(exportPath, exportData.ToString()); } } catch (Exception e) @@ -210,6 +182,78 @@ namespace Observatory.UI.ViewModels new List<(string, string)> { ("An error occurred while exporting; output may be missing or incomplete." + Environment.NewLine + "Please check the error log (found in your Documents folder) for more details and visit our discord to report it.", e.Message) }); } + + static System.Text.StringBuilder ExportTabSeparated(List selectedData) + { + System.Text.StringBuilder exportData = new(); + + var columnNames = selectedData[0].GetType().GetProperties().Select(c => c.Name).ToList(); + exportData.AppendJoin('\t', columnNames).AppendLine(); + + var lastColumn = columnNames.Last(); + foreach (var line in selectedData) + { + var lineType = line.GetType(); // some plugins have different line types, so don't move this out of loop + foreach (var columnName in columnNames) + { + var cellValue = lineType.GetProperty(columnName)?.GetValue(line)?.ToString() ?? string.Empty; + exportData.Append(cellValue).Append('\t'); + } + exportData.AppendLine(); + } + return exportData; + } + + static System.Text.StringBuilder ExportFixedWidth(List selectedData) + { + Dictionary colSize = new(); + Dictionary> colContent = new(); + + var columns = selectedData[0].GetType().GetProperties(); + foreach (var column in columns) + { + colSize.Add(column.Name, column.Name.Length); + colContent.Add(column.Name, new()); + } + + foreach (var line in selectedData) + { + var lineType = line.GetType(); // some plugins have different line types, so don't move this out of loop + foreach (var column in colContent) + { + var cellValue = lineType.GetProperty(column.Key)?.GetValue(line)?.ToString() ?? string.Empty; + column.Value.Add(cellValue); + if (colSize[column.Key] < cellValue.Length) + colSize[column.Key] = cellValue.Length; + } + } + + System.Text.StringBuilder exportData = new(); + + + foreach (var colTitle in colContent.Keys) + { + if (colSize[colTitle] < colTitle.Length) + colSize[colTitle] = colTitle.Length; + + exportData.Append(colTitle.PadRight(colSize[colTitle]) + " "); + } + exportData.AppendLine(); + + for (int i = 0; i < colContent.First().Value.Count; i++) + { + foreach (var column in colContent) + { + if (column.Value[i].Length > 0 && !char.IsNumber(column.Value[i][0]) && column.Value[i].Count(char.IsLetter) / (float)column.Value[i].Length > 0.25) + exportData.Append(column.Value[i].PadRight(colSize[column.Key]) + " "); + else + exportData.Append(column.Value[i].PadLeft(colSize[column.Key]) + " "); + } + exportData.AppendLine(); + } + + return exportData; + } } public void ClearGrid() diff --git a/ObservatoryCore/UI/Views/BasicUIView.axaml.cs b/ObservatoryCore/UI/Views/BasicUIView.axaml.cs index ab6059d..8e96d93 100644 --- a/ObservatoryCore/UI/Views/BasicUIView.axaml.cs +++ b/ObservatoryCore/UI/Views/BasicUIView.axaml.cs @@ -647,11 +647,114 @@ namespace Observatory.UI.Views voiceExpander.Content = voiceGrid; gridManager.AddSetting(voiceExpander); - - } #endregion + #region Export options + + Expander exportExpander = new() + { + Header = "Export Options", + DataContext = Properties.Core.Default, + Margin = new Thickness(0, 0) + }; + + Grid exportGrid = new() { Margin = new Thickness(10, 10) }; + SettingGridManager exportGridManager = new(exportGrid); + + exportGrid.ColumnDefinitions = new() + { + new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Star) }, + new ColumnDefinition() { Width = new GridLength(3, GridUnitType.Star) }, + new ColumnDefinition() { Width = new GridLength(3, GridUnitType.Star) } + }; + + TextBlock exportStyleLabel = new() + { + Text = "Export style: ", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + ComboBox exportStyleDropDown = new() + { + MinWidth = 200 + }; + + exportStyleDropDown.Items = new List() { + "Fixed width", + "Tab separated", + }; + + if (Properties.Core.Default.ExportStyle.Length > 0) + { + exportStyleDropDown.SelectedItem = Properties.Core.Default.ExportStyle; + } + + exportStyleDropDown.SelectionChanged += (object sender, SelectionChangedEventArgs e) => + { + var comboBox = (ComboBox)sender; + Properties.Core.Default.ExportStyle = comboBox.SelectedItem.ToString(); + Properties.Core.Default.Save(); + }; + + TextBlock exportPathLabel = new() + { + Text = "Export Path: ", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + + TextBox exportPath = new() + { + Text = Properties.Core.Default.ExportFolder + }; + + Button exportBrowse = new() + { + Content = "Browse", + Height = 30, + Width = 100, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center + }; + + exportBrowse.Click += (object source, RoutedEventArgs e) => + { + OpenFolderDialog openFolderDialog = new() + { + Directory = exportPath.Text + }; + var browseTask = openFolderDialog.ShowAsync((Window)((Button)source).GetVisualRoot()); + browseTask.ContinueWith((task) => + { + string path = task.Result; + if (path != string.Empty) + { + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { exportPath.Text = path; }); + Properties.Core.Default.ExportFolder = path; + Properties.Core.Default.Save(); + } + }); + }; + + exportPath.LostFocus += (object sender, RoutedEventArgs e) => + { + if (System.IO.Directory.Exists(exportPath.Text)) + { + Properties.Core.Default.ExportFolder = exportPath.Text; + Properties.Core.Default.Save(); + } + }; + + exportGridManager.AddSettingWithLabel(exportStyleLabel, exportStyleDropDown); + exportGridManager.AddSettingWithLabel(exportPathLabel, exportPath); + exportGridManager.AddSetting(exportBrowse); + + exportExpander.Content = exportGrid; + + gridManager.AddSetting(exportExpander); + #endregion + #region System Context Priming setting TextBlock primeSystemContextLabel = new() { Text = "Try re-load current system information when starting monitor" };