From 205458044df00ee17225b63ddbbce17f768baf0b Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Tue, 19 Dec 2023 17:35:30 -0800 Subject: [PATCH 1/2] Added IUIDropDownButton and improve Json > Table UI --- src/app/dev/DevToys.Api/Core/IFileStorage.cs | 1 + .../Tool/GUI/Components/IUIDropDownButton.cs | 153 +++++++++++++ .../GUI/Components/IUIDropDownMenuItem.cs | 165 ++++++++++++++ .../Tool/GUI/Components/IUIElement.cs | 25 +-- .../Tool/GUI/Components/IUIGridCell.cs | 26 +-- .../Tool/GUI/Components/IUINumberInput.cs | 5 + src/app/dev/DevToys.Api/ViewModelBase.cs | 29 +++ .../DropDownButton/DropDownButton.razor.cs | 26 ++- .../Feedback/FontIcon/FontIcon.razor | 21 +- .../UIDropDownButtonPresenter.razor | 28 +++ .../UIDropDownButtonPresenter.razor.cs | 63 ++++++ .../UIElements/UIElementPresenter.razor | 4 + .../UIElements/UITextInputWrapper.razor.cs | 5 +- .../DevToys.Tools/Helpers/JsonTableHelper.cs | 34 +-- .../JsonTable/JsonTableConverter.Designer.cs | 18 ++ .../JsonTable/JsonTableConverter.resx | 6 + .../JsonTable/JsonTableConverterGuiTool.cs | 210 ++++++++++-------- 17 files changed, 649 insertions(+), 170 deletions(-) create mode 100644 src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownButton.cs create mode 100644 src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownMenuItem.cs create mode 100644 src/app/dev/DevToys.Api/ViewModelBase.cs create mode 100644 src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor create mode 100644 src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor.cs diff --git a/src/app/dev/DevToys.Api/Core/IFileStorage.cs b/src/app/dev/DevToys.Api/Core/IFileStorage.cs index 3584562da0..b03225627f 100644 --- a/src/app/dev/DevToys.Api/Core/IFileStorage.cs +++ b/src/app/dev/DevToys.Api/Core/IFileStorage.cs @@ -12,6 +12,7 @@ public interface IFileStorage /// string AppCacheDirectory { get; } + // TODO: There's a problem with this API. It's not possible to know what file type the user selected. The file type can impact the data written into the file (i.e PNG vs JPEG). /// /// Prompt the user to select a location to save a file, and decide of the file name. /// diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownButton.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownButton.cs new file mode 100644 index 0000000000..3c2de30c5f --- /dev/null +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownButton.cs @@ -0,0 +1,153 @@ +namespace DevToys.Api; + +/// +/// A component that represents a drop down button where the user can click on a menu item. +/// +public interface IUIDropDownButton : IUIElement +{ + /// + /// Gets the list of items displayed in the drop down menu. + /// + IUIDropDownMenuItem[]? MenuItems { get; } + + /// + /// Gets the text to display in the drop down button. + /// + string? Text { get; } + + /// + /// Gets the name of the font containing the icon. + /// + string? IconFontName { get; } + + /// + /// Gets the glyph corresponding to the icon in the . + /// + char IconGlyph { get; } + + /// + /// Raised when is changed. + /// + event EventHandler? MenuItemsChanged; + + /// + /// Raised when is changed. + /// + event EventHandler? TextChanged; + + /// + /// Raised when is changed. + /// + event EventHandler? IconFontNameChanged; + + /// + /// Raised when is changed. + /// + event EventHandler? IconGlyphChanged; +} + +[DebuggerDisplay($"Id = {{{nameof(Id)}}}, Text = {{{nameof(Text)}}}")] +internal sealed class UIDropDownButton : UIElement, IUIDropDownButton +{ + private IUIDropDownMenuItem[]? _menuItems; + private string? _text; + private string? _iconFontName; + private char _iconGlyph; + + internal UIDropDownButton(string? id) + : base(id) + { + } + + public IUIDropDownMenuItem[]? MenuItems + { + get => _menuItems; + internal set => SetPropertyValue(ref _menuItems, value, MenuItemsChanged); + } + + public string? Text + { + get => _text; + internal set => SetPropertyValue(ref _text, value, TextChanged); + } + + public string? IconFontName + { + get => _iconFontName; + internal set + { + Guard.IsNotNullOrWhiteSpace(value); + SetPropertyValue(ref _iconFontName, value, IconFontNameChanged); + } + } + + public char IconGlyph + { + get => _iconGlyph; + internal set => SetPropertyValue(ref _iconGlyph, value, IconGlyphChanged); + } + + public event EventHandler? MenuItemsChanged; + public event EventHandler? TextChanged; + public event EventHandler? IconFontNameChanged; + public event EventHandler? IconGlyphChanged; +} + +public static partial class GUI +{ + /// + /// Create a component that represents a drop down button where the user can click on a menu item. + /// + public static IUIDropDownButton DropDownButton() + { + return DropDownButton(null); + } + + /// + /// Create a component that represents a drop down button where the user can click on a menu item. + /// + /// An optional unique identifier for this UI element. + public static IUIDropDownButton DropDownButton(string? id) + { + return new UIDropDownButton(id); + } + + /// + /// Create a component that represents a drop down button where the user can click on a menu item. + /// + /// An optional unique identifier for this UI element. + /// The text to display in the drop down button. + public static IUIDropDownButton DropDownButton(string? id, string text) + { + return new UIDropDownButton(id).Text(text); + } + + /// + /// Sets the of the drop down button. + /// + public static IUIDropDownButton Text(this IUIDropDownButton element, string? text) + { + ((UIDropDownButton)element).Text = text; + return element; + } + + /// + /// Sets the icon of the drop down button. + /// + public static IUIDropDownButton Icon(this IUIDropDownButton element, string fontName, char glyph) + { + var button = (UIDropDownButton)element; + button.IconFontName = fontName; + button.IconGlyph = glyph; + return button; + } + + /// + /// Sets the in the drop down button. + /// + public static IUIDropDownButton WithMenuItems(this IUIDropDownButton element, params IUIDropDownMenuItem[] menuItems) + { + ((UIDropDownButton)element).MenuItems = menuItems; + return element; + } +} diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownMenuItem.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownMenuItem.cs new file mode 100644 index 0000000000..87f9ec43b1 --- /dev/null +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIDropDownMenuItem.cs @@ -0,0 +1,165 @@ +namespace DevToys.Api; + +/// +/// A component that represents a menu item, which reacts when clicking on it. +/// +public interface IUIDropDownMenuItem +{ + /// + /// Gets whether this menu item should be enabled or disabled. Default is true. + /// + bool IsEnabled { get; } + + /// + /// Gets the text to display in the menu item. + /// + string? Text { get; } + + /// + /// Gets the name of the font containing the icon. + /// + string? IconFontName { get; } + + /// + /// Gets the glyph corresponding to the icon in the . + /// + char IconGlyph { get; } + + /// + /// Gets the action to run when the user clicks the menu item. + /// + Func? OnClickAction { get; } + + /// + /// Raised when is changed. + /// + event EventHandler? IsEnabledChanged; + + /// + /// Raised when is changed. + /// + event EventHandler? TextChanged; + + /// + /// Raised when is changed. + /// + event EventHandler? IconFontNameChanged; + + /// + /// Raised when is changed. + /// + event EventHandler? IconGlyphChanged; +} + +[DebuggerDisplay($"Text = {{{nameof(Text)}}}")] +internal sealed class UIDropDownMenuItem : ViewModelBase, IUIDropDownMenuItem +{ + private bool _isEnabled = true; + private string? _text; + private string? _iconFontName; + private char _iconGlyph; + + public bool IsEnabled + { + get => _isEnabled; + internal set + { + if (_isEnabled != value) + { + SetPropertyValue(ref _isEnabled, value, IsEnabledChanged); + } + } + } + + public string? Text + { + get => _text; + internal set => SetPropertyValue(ref _text, value, TextChanged); + } + + public string? IconFontName + { + get => _iconFontName; + internal set + { + Guard.IsNotNullOrWhiteSpace(value); + SetPropertyValue(ref _iconFontName, value, IconFontNameChanged); + } + } + + public char IconGlyph + { + get => _iconGlyph; + internal set => SetPropertyValue(ref _iconGlyph, value, IconGlyphChanged); + } + + public Func? OnClickAction { get; internal set; } + + public event EventHandler? IsEnabledChanged; + public event EventHandler? TextChanged; + public event EventHandler? IconFontNameChanged; + public event EventHandler? IconGlyphChanged; +} + +public static partial class GUI +{ + /// + /// Create a component that represents a menu item, which reacts when clicking on it. + /// + public static IUIDropDownMenuItem DropDownMenuItem() + { + return new UIDropDownMenuItem(); + } + + /// + /// Create a component that represents a menu item, which reacts when clicking on it. + /// + /// The text to display in the menu item. + public static IUIDropDownMenuItem DropDownMenuItem(string text) + { + return DropDownMenuItem().Text(text); + } + + /// + /// Sets the of the menu item. + /// + public static IUIDropDownMenuItem Text(this IUIDropDownMenuItem element, string? text) + { + ((UIDropDownMenuItem)element).Text = text; + return element; + } + + /// + /// Sets the action to run when clicking on the menu item. + /// + public static IUIDropDownMenuItem OnClick(this IUIDropDownMenuItem element, Func? actionOnClick) + { + ((UIDropDownMenuItem)element).OnClickAction = actionOnClick; + return element; + } + + /// + /// Sets the action to run when clicking on the menu item. + /// + public static IUIDropDownMenuItem OnClick(this IUIDropDownMenuItem element, Action? actionOnClick) + { + ((UIDropDownMenuItem)element).OnClickAction + = () => + { + actionOnClick?.Invoke(); + return ValueTask.CompletedTask; + }; + return element; + } + + /// + /// Sets the icon of the menu item. + /// + public static IUIDropDownMenuItem Icon(this IUIDropDownMenuItem element, string fontName, char glyph) + { + var menuItem = (UIDropDownMenuItem)element; + menuItem.IconFontName = fontName; + menuItem.IconGlyph = glyph; + return menuItem; + } +} diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIElement.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIElement.cs index fe001d5a3d..523ffc028d 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIElement.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIElement.cs @@ -55,7 +55,7 @@ public interface IUIElement } [DebuggerDisplay($"Id = {{{nameof(Id)}}}, IsVisible = {{{nameof(IsVisible)}}}, IsEnabled = {{{nameof(IsEnabled)}}}")] -internal abstract class UIElement : IUIElement, INotifyPropertyChanged +internal abstract class UIElement : ViewModelBase, IUIElement { private bool _isVisible = true; private bool _isEnabled = true; @@ -100,29 +100,6 @@ public UIVerticalAlignment VerticalAlignment public event EventHandler? HorizontalAlignmentChanged; public event EventHandler? VerticalAlignmentChanged; - - public event PropertyChangedEventHandler? PropertyChanged; - - protected bool SetPropertyValue( - ref T field, - T value, - EventHandler? propertyChangedEventHandler, - [CallerMemberName] string? propertyName = null) - { - if (!EqualityComparer.Default.Equals(field, value)) - { - field = value; - propertyChangedEventHandler?.Invoke(this, EventArgs.Empty); - OnPropertyChanged(propertyName); - return true; - } - return false; - } - - protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new(propertyName)); - } } public static partial class GUI diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIGridCell.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIGridCell.cs index 60813399f6..66f0abc25b 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIGridCell.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIGridCell.cs @@ -62,7 +62,7 @@ public interface IUIGridCell } [DebuggerDisplay($"Row = {{{nameof(Row)}}}, Column = {{{nameof(Column)}}}, RowSpan = {{{nameof(RowSpan)}}}, ColumnSpan = {{{nameof(ColumnSpan)}}}")] -internal sealed class UIGridCell : IUIGridCell, INotifyPropertyChanged +internal sealed class UIGridCell : ViewModelBase, IUIGridCell { private int _row; private int _column; @@ -113,27 +113,6 @@ public IUIElement? Child public event EventHandler? RowSpanChanged; public event EventHandler? ColumnSpanChanged; public event EventHandler? ChildChanged; - - public event PropertyChangedEventHandler? PropertyChanged; - - private void SetPropertyValue( - ref T field, - T value, - EventHandler? propertyChangedEventHandler, - [CallerMemberName] string? propertyName = null) - { - if (!EqualityComparer.Default.Equals(field, value)) - { - field = value; - propertyChangedEventHandler?.Invoke(this, EventArgs.Empty); - OnPropertyChanged(propertyName); - } - } - - private void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new(propertyName)); - } } public static partial class GUI @@ -153,6 +132,7 @@ public static IUIGridCell Cell() /// The column alignment of the cell. /// A value that indicates the total number of rows that the cell content spans within the parent grid. /// A value that indicates the total number of columns that the cell content spans within the parent grid. + /// The child element to display in the cell. public static IUIGridCell Cell(int row, int column, int rowSpan, int columnSpan, IUIElement? child = null) { return new UIGridCell() @@ -168,6 +148,7 @@ public static IUIGridCell Cell(int row, int column, int rowSpan, int columnSpan, /// /// The row where the cell should be placed. /// The column where the cell should be placed. + /// The child element to display in the cell. public static IUIGridCell Cell(TRow row, TColumn column, IUIElement? child = null) where TRow : Enum where TColumn : Enum @@ -188,6 +169,7 @@ public static IUIGridCell Cell(TRow row, TColumn column, IUIEleme /// The last row where the cell should appear. /// The first column where the cell should appear. /// The last column where the cell should appear. + /// The child element to display in the cell. public static IUIGridCell Cell( TRow firstRow, TRow lastRow, diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUINumberInput.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUINumberInput.cs index 0beab1579d..828f16b8b1 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUINumberInput.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUINumberInput.cs @@ -35,6 +35,11 @@ public interface IUINumberInput : IUISingleLineTextInput /// event EventHandler? MaxChanged; + /// + /// Raised when is changed. + /// + event EventHandler? StepChanged; + /// /// Raised when is changed. /// diff --git a/src/app/dev/DevToys.Api/ViewModelBase.cs b/src/app/dev/DevToys.Api/ViewModelBase.cs new file mode 100644 index 0000000000..a1ff4957e5 --- /dev/null +++ b/src/app/dev/DevToys.Api/ViewModelBase.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace DevToys.Api; + +internal abstract class ViewModelBase : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool SetPropertyValue( + ref T field, + T value, + EventHandler? propertyChangedEventHandler, + [CallerMemberName] string? propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + propertyChangedEventHandler?.Invoke(this, EventArgs.Empty); + OnPropertyChanged(propertyName); + return true; + } + return false; + } + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new(propertyName)); + } +} diff --git a/src/app/dev/DevToys.Blazor/Components/BasicInput/DropDownButton/DropDownButton.razor.cs b/src/app/dev/DevToys.Blazor/Components/BasicInput/DropDownButton/DropDownButton.razor.cs index ebda24e23c..00e806fe03 100644 --- a/src/app/dev/DevToys.Blazor/Components/BasicInput/DropDownButton/DropDownButton.razor.cs +++ b/src/app/dev/DevToys.Blazor/Components/BasicInput/DropDownButton/DropDownButton.razor.cs @@ -1,9 +1,10 @@ -using DevToys.Blazor.Core.Services; +using System.Collections.Specialized; +using DevToys.Blazor.Core.Services; using Microsoft.AspNetCore.Components.Web; namespace DevToys.Blazor.Components; -public partial class DropDownButton : StyledComponentBase where TItem : DropDownListItem +public partial class DropDownButton : StyledComponentBase, IDisposable where TItem : DropDownListItem { private bool _isOpen; private ListBox? _listBox; @@ -46,10 +47,26 @@ public partial class DropDownButton : StyledComponentBase where TItem : D [Parameter] public ICollection? Items { get; set; } + public void Dispose() + { + if (Items is INotifyCollectionChanged notifyCollection) + { + notifyCollection.CollectionChanged -= OnItemsChanged; + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + if (Items is INotifyCollectionChanged notifyCollection) + { + notifyCollection.CollectionChanged += OnItemsChanged; + } + } + if (_isOpen && _listBox is not null) { await _listBox.FocusAsync(); @@ -107,4 +124,9 @@ private void OnDropDowlListItemSelected(int itemIndex) _listBox.SelectedItem.OnClick.InvokeAsync(_listBox.SelectedItem).Forget(); } } + + private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + StateHasChanged(); + } } diff --git a/src/app/dev/DevToys.Blazor/Components/Feedback/FontIcon/FontIcon.razor b/src/app/dev/DevToys.Blazor/Components/Feedback/FontIcon/FontIcon.razor index f20159b8f2..9d37c07697 100644 --- a/src/app/dev/DevToys.Blazor/Components/Feedback/FontIcon/FontIcon.razor +++ b/src/app/dev/DevToys.Blazor/Components/Feedback/FontIcon/FontIcon.razor @@ -16,14 +16,17 @@ PaddingBottom="@PaddingBottom" IsEnabled="@IsActuallyEnabled" IsVisible="@IsVisible"> - + @if (!string.IsNullOrWhiteSpace(FontFamily)) + { + + } \ No newline at end of file diff --git a/src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor b/src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor new file mode 100644 index 0000000000..78a69a8c29 --- /dev/null +++ b/src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor @@ -0,0 +1,28 @@ +@namespace DevToys.Blazor.Components.UIElements +@using DevToys.Api; +@inherits ComponentBase + + + @if (!string.IsNullOrWhiteSpace(UIDropDownButton.IconFontName) && UIDropDownButton.IconGlyph != '\0') + { + + + @if (!string.IsNullOrEmpty(UIDropDownButton?.Text)) + { + + } + + } + else + { + + } + \ No newline at end of file diff --git a/src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor.cs b/src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor.cs new file mode 100644 index 0000000000..130a259567 --- /dev/null +++ b/src/app/dev/DevToys.Blazor/Components/UIElements/UIDropDownButtonPresenter.razor.cs @@ -0,0 +1,63 @@ +using System.Collections.ObjectModel; + +namespace DevToys.Blazor.Components.UIElements; + +public partial class UIDropDownButtonPresenter : ComponentBase, IDisposable +{ + [Parameter] + public IUIDropDownButton UIDropDownButton { get; set; } = default!; + + internal ObservableCollection MenuItemComponents { get; } = new(); + + public void Dispose() + { + UIDropDownButton.MenuItemsChanged -= UIDropDownButton_MenuItemsChanged; + GC.SuppressFinalize(this); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + + UIDropDownButton.MenuItemsChanged += UIDropDownButton_MenuItemsChanged; + UIDropDownButton_MenuItemsChanged(this, EventArgs.Empty); + } + + private void UIDropDownButton_MenuItemsChanged(object? sender, EventArgs e) + { + MenuItemComponents.Clear(); + + if (UIDropDownButton.MenuItems is not null) + { + EventCallback onClickEventCallback = EventCallback.Factory.Create(this, MenuItem_Click); + + foreach (IUIDropDownMenuItem item in UIDropDownButton.MenuItems) + { + MenuItemComponents.Add(new UIDropDownMenuItemDropDownListItem + { + UIDropDownMenuItem = item, + IconFontFamily = item.IconFontName ?? string.Empty, + IconGlyph = item.IconGlyph, + Text = item.Text, + IsEnabled = item.IsEnabled, + OnClick = onClickEventCallback, + }); + } + } + + StateHasChanged(); + } + + private void MenuItem_Click(DropDownListItem menuItem) + { + if (menuItem is UIDropDownMenuItemDropDownListItem dropDownMenuItem) + { + dropDownMenuItem.UIDropDownMenuItem.OnClickAction?.Invoke(); + } + } + + public class UIDropDownMenuItemDropDownListItem : DropDownListItem + { + internal IUIDropDownMenuItem UIDropDownMenuItem { get; set; } = default!; + } +} diff --git a/src/app/dev/DevToys.Blazor/Components/UIElements/UIElementPresenter.razor b/src/app/dev/DevToys.Blazor/Components/UIElements/UIElementPresenter.razor index ef79011d90..9c9e8adcf4 100644 --- a/src/app/dev/DevToys.Blazor/Components/UIElements/UIElementPresenter.razor +++ b/src/app/dev/DevToys.Blazor/Components/UIElements/UIElementPresenter.razor @@ -42,6 +42,10 @@ break; + case IUIDropDownButton dropDownButton: + + break; + case IUIDiffTextInput diffTextInput: break; diff --git a/src/app/dev/DevToys.Blazor/Components/UIElements/UITextInputWrapper.razor.cs b/src/app/dev/DevToys.Blazor/Components/UIElements/UITextInputWrapper.razor.cs index 7874d03817..2a9a8e934f 100644 --- a/src/app/dev/DevToys.Blazor/Components/UIElements/UITextInputWrapper.razor.cs +++ b/src/app/dev/DevToys.Blazor/Components/UIElements/UITextInputWrapper.razor.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.Messaging; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.Messaging; using DevToys.Core.Models; using DevToys.Core.Settings; using DevToys.Core.Tools; @@ -377,10 +378,10 @@ await InvokeAsync(() => { ToolsDetectedBySmartDetection = true; + EventCallback onClickEventCallback = EventCallback.Factory.Create(this, SmartDetectionMenuItem_Click); for (int i = 0; i < detectedTools.Count; i++) { SmartDetectedTool detectedTool = detectedTools[i]; - EventCallback onClickEventCallback = EventCallback.Factory.Create(this, SmartDetectionMenuItem_Click); var menuItem = new SmartDetectionDropDownListItem { IconGlyph = detectedTool.ToolInstance.IconGlyph, diff --git a/src/app/dev/DevToys.Tools/Helpers/JsonTableHelper.cs b/src/app/dev/DevToys.Tools/Helpers/JsonTableHelper.cs index c57d60e100..460fae8e62 100644 --- a/src/app/dev/DevToys.Tools/Helpers/JsonTableHelper.cs +++ b/src/app/dev/DevToys.Tools/Helpers/JsonTableHelper.cs @@ -7,16 +7,8 @@ namespace DevToys.Tools.Helpers; internal static class JsonTableHelper { - internal static ConvertResult ConvertFromJson(string? text, CopyFormat format, CancellationToken cancellationToken) + internal static ConvertResult ConvertFromJson(string? text, CopyFormat? format, CancellationToken cancellationToken) { - char separator = format switch - { - CopyFormat.TSV => '\t', - CopyFormat.CSV => ',', - CopyFormat.FSV => ';', - _ => throw new NotSupportedException($"Unhandled {nameof(CopyFormat)}: {format}"), - }; - JObject[]? array = ParseJsonArray(text); if (array == null) { @@ -38,8 +30,20 @@ internal static ConvertResult ConvertFromJson(string? text, CopyFormat format, C var table = new DataGridContents(properties, new()); - var clipboard = new StringBuilder(); - clipboard.AppendLine(string.Join(separator, properties)); + StringBuilder? clipboard = null; + char separator = '\0'; + if (format.HasValue) + { + separator = format switch + { + CopyFormat.TSV => '\t', + CopyFormat.CSV => ',', + CopyFormat.FSV => ';', + _ => throw new NotSupportedException($"Unhandled {nameof(CopyFormat)}: {format}"), + }; + clipboard = new StringBuilder(); + clipboard.AppendLine(string.Join(separator, properties)); + } foreach (JObject obj in flattened) { @@ -50,10 +54,14 @@ internal static ConvertResult ConvertFromJson(string? text, CopyFormat format, C .ToArray(); table.Rows.Add(values); - clipboard.AppendLine(string.Join(separator, values)); + + if (format.HasValue) + { + clipboard!.AppendLine(string.Join(separator, values)); + } } - return new(table, clipboard.ToString(), ConvertResultError.None); + return new(table, clipboard?.ToString() ?? string.Empty, ConvertResultError.None); } internal record ConvertResult(DataGridContents? Data, string Text, ConvertResultError Error); diff --git a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.Designer.cs b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.Designer.cs index 02407e677e..f4b1ac1e1d 100644 --- a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.Designer.cs +++ b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.Designer.cs @@ -96,6 +96,15 @@ internal static string Configuration { } } + /// + /// Looks up a localized string similar to Copy as. + /// + internal static string CopyAs { + get { + return ResourceManager.GetString("CopyAs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Comma separated values (CSV). /// @@ -195,6 +204,15 @@ internal static string OutputFormatDescription { } } + /// + /// Looks up a localized string similar to Save as. + /// + internal static string SaveAs { + get { + return ResourceManager.GetString("SaveAs", resourceCulture); + } + } + /// /// Looks up a localized string similar to JSON,CSV,TSV,French,Excel,Tabular,Data. /// diff --git a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.resx b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.resx index 2e44ed8c88..9d32b9373c 100644 --- a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.resx +++ b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverter.resx @@ -175,4 +175,10 @@ Select which format to output CLI parameter description + + Copy as + + + Save as + \ No newline at end of file diff --git a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs index 9da24bc834..3e057004ed 100644 --- a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs +++ b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs @@ -1,4 +1,6 @@ using System.Data; +using System.IO; +using System.Threading; using DevToys.Tools.Tools.Converters.JsonTable; using Microsoft.Extensions.Logging; using static DevToys.Tools.Helpers.JsonTableHelper; @@ -8,8 +10,8 @@ namespace DevToys.Tools.Tools.Converters.JsonYaml; [Export(typeof(IGuiTool))] [Name("JsonTableConverter")] [ToolDisplayInformation( - IconFontName = "DevToys-Tools-Icons", - IconGlyph = '\u0109', + IconFontName = "FluentSystemIcons", + IconGlyph = '\uF0D8', GroupName = PredefinedCommonToolGroupNames.Converters, ResourceManagerAssemblyIdentifier = nameof(DevToysToolsResourceManagerAssemblyIdentifier), ResourceManagerBaseName = "DevToys.Tools.Tools.Converters.JsonTable.JsonTableConverter", @@ -21,110 +23,65 @@ namespace DevToys.Tools.Tools.Converters.JsonYaml; [AcceptedDataTypeName(PredefinedCommonDataTypeNames.JsonArray)] internal sealed partial class JsonTableConverterGuiTool : IGuiTool, IDisposable { - /// - /// Whether the tool should copy to clipboard as CSV or TSV. - /// - /// - /// Default to tab-separated-values because copy-to-Excel is the most likely use-case. - /// - private static readonly SettingDefinition copyFormat - = new(name: $"{nameof(JsonTableConverterGuiTool)}.{nameof(copyFormat)}", defaultValue: CopyFormat.TSV); - - private enum GridColumn - { - Content - } - - private enum GridRow - { - Header, - Content, - } - private readonly ILogger _logger; - private readonly ISettingsProvider _settingsProvider; private readonly IClipboard _clipboard; + private readonly IFileStorage _fileStorage; private readonly IUIMultiLineTextInput _inputTextArea = MultilineTextInput("json-input-text-area"); private readonly IUIDataGrid _outputDataGrid = DataGrid("output-data-grid"); - private readonly IUIButton _copyButton = Button("copy-data-grid"); + private readonly IUIStack _copyOrSaveStack = Stack("copy-or-save-data-grid"); private readonly DisposableSemaphore _semaphore = new(); // one convert at a time private CancellationTokenSource? _cancellationTokenSource; [ImportingConstructor] - public JsonTableConverterGuiTool(ISettingsProvider settingsProvider, IClipboard clipboard) + public JsonTableConverterGuiTool(IClipboard clipboard, IFileStorage fileStorage) { _logger = this.Log(); - _settingsProvider = settingsProvider; _clipboard = clipboard; + _fileStorage = fileStorage; } internal Task? WorkTask { get; private set; } public UIToolView View - { - get - { - _copyButton - .Icon("FluentSystemIcons", '\uF32B') - .OnClick(OnCopyDataGrid) - .Disable(); // disable until valid JSON is input - - return new( - isScrollable: false, - Grid() - .ColumnLargeSpacing() - .RowLargeSpacing() - .Rows( - (GridRow.Header, Auto), - (GridRow.Content, new UIGridLength(1, UIGridUnitType.Fraction)) - ) - .Columns( - (GridColumn.Content, new UIGridLength(1, UIGridUnitType.Fraction)) - ) - .Cells( - Cell( - GridRow.Header, - GridColumn.Content, - Stack().Vertical().WithChildren( - Label() - .Text(JsonTableConverter.Configuration), - - Setting("clipboard-copy-format-setting") - .Icon("FluentSystemIcons", '\uF7ED') - .Title(JsonTableConverter.ClipboardFormatTitle) - .Description(JsonTableConverter.ClipboardFormatDescription) - .Handle( - _settingsProvider, - copyFormat, - null, - Item(JsonTableConverter.CopyFormatTSV, CopyFormat.TSV), - Item(JsonTableConverter.CopyFormatCSV, CopyFormat.CSV), - Item(JsonTableConverter.CopyFormatFSV, CopyFormat.FSV) - ) - ) - ), - Cell( - GridRow.Content, - GridColumn.Content, - SplitGrid() - .Vertical() - .WithLeftPaneChild( - _inputTextArea - .Title(JsonTableConverter.Input) - .Language("json") - .OnTextChanged(OnInputTextChanged)) - .WithRightPaneChild( - _outputDataGrid - .Title(JsonTableConverter.Output) - .CommandBarExtraContent(_copyButton) - .Extendable()) - ) - ) - ); - } - } + => new( + isScrollable: false, + SplitGrid() + .Vertical() + .WithLeftPaneChild( + + _inputTextArea + .Title(JsonTableConverter.Input) + .Language("json") + .OnTextChanged(OnInputTextChanged)) + + .WithRightPaneChild( + + _outputDataGrid + .Title(JsonTableConverter.Output) + .Extendable() + .CommandBarExtraContent( + _copyOrSaveStack + .Horizontal() + .Disable() // disable until valid JSON is input + .WithChildren( + + DropDownButton() + .Icon("FluentSystemIcons", '\uF32B') + .Text(JsonTableConverter.CopyAs) + .WithMenuItems( + DropDownMenuItem(JsonTableConverter.CopyFormatCSV).OnClick(OnCopyCSV), + DropDownMenuItem(JsonTableConverter.CopyFormatTSV).OnClick(OnCopyTSV), + DropDownMenuItem(JsonTableConverter.CopyFormatFSV).OnClick(OnCopyFSV)), + + DropDownButton() + .Icon("FluentSystemIcons", '\uF67F') + .Text(JsonTableConverter.SaveAs) + .WithMenuItems( + DropDownMenuItem(JsonTableConverter.CopyFormatCSV).OnClick(OnSaveCSV), + DropDownMenuItem(JsonTableConverter.CopyFormatTSV).OnClick(OnSaveTSV), + DropDownMenuItem(JsonTableConverter.CopyFormatFSV).OnClick(OnSaveFSV)))))); // Smart detection handler. public void OnDataReceived(string dataTypeName, object? parsedData) @@ -145,29 +102,34 @@ public void Dispose() private void OnInputTextChanged(string text) { - StartConvert(ConvertTarget.DataGrid); + StartConvert(ConvertTarget.DataGrid, null); } - private void StartConvert(ConvertTarget target) + private void StartConvert(ConvertTarget target, CopyFormat? copyFormat) { _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = new CancellationTokenSource(); - WorkTask = ConvertAsync(target, _cancellationTokenSource.Token); + WorkTask = ConvertAsync(target, copyFormat, _cancellationTokenSource.Token); } - private async Task ConvertAsync(ConvertTarget target, CancellationToken cancellationToken) + private async Task ConvertAsync(ConvertTarget target, CopyFormat? copyFormat, CancellationToken cancellationToken) { using (await _semaphore.WaitAsync(cancellationToken)) { await TaskSchedulerAwaiter.SwitchOffMainThreadAsync(cancellationToken); - ConvertResult conversionResult = ConvertFromJson(_inputTextArea.Text, _settingsProvider.GetSetting(copyFormat), cancellationToken); + if (target == ConvertTarget.Clipboard || target == ConvertTarget.Save) + { + Guard.IsNotNull(copyFormat); + } + + ConvertResult conversionResult = ConvertFromJson(_inputTextArea.Text, copyFormat, cancellationToken); if (conversionResult.Error == ConvertResultError.None && conversionResult.Data != null) { - _copyButton.Enable(); + _copyOrSaveStack.Enable(); switch (target) { case ConvertTarget.DataGrid: @@ -177,17 +139,39 @@ private async Task ConvertAsync(ConvertTarget target, CancellationToken cancella case ConvertTarget.Clipboard: await _clipboard.SetClipboardTextAsync(conversionResult.Text); break; + + case ConvertTarget.Save: + Stream? fileStream = null; + try + { + fileStream = await _fileStorage.PickSaveFileAsync("txt", "csv"); + if (fileStream is not null) + { + using var writer = new StreamWriter(fileStream); + writer.Write(conversionResult.Text); + } + } + finally + { + fileStream?.Dispose(); + } + break; } } else { - _copyButton.Disable(); + _copyOrSaveStack.Disable(); SetDataGridError(); } } } - private enum ConvertTarget { DataGrid, Clipboard } + private enum ConvertTarget + { + DataGrid, + Clipboard, + Save + } private void SetDataGridData(DataGridContents table) { @@ -202,9 +186,39 @@ private void SetDataGridError() _outputDataGrid.WithRows(); } - private ValueTask OnCopyDataGrid() + private ValueTask OnCopyCSV() + { + StartConvert(ConvertTarget.Clipboard, CopyFormat.CSV); + return ValueTask.CompletedTask; + } + + private ValueTask OnCopyTSV() + { + StartConvert(ConvertTarget.Clipboard, CopyFormat.TSV); + return ValueTask.CompletedTask; + } + + private ValueTask OnCopyFSV() + { + StartConvert(ConvertTarget.Clipboard, CopyFormat.FSV); + return ValueTask.CompletedTask; + } + + private ValueTask OnSaveCSV() + { + StartConvert(ConvertTarget.Save, CopyFormat.CSV); + return ValueTask.CompletedTask; + } + + private ValueTask OnSaveTSV() + { + StartConvert(ConvertTarget.Save, CopyFormat.TSV); + return ValueTask.CompletedTask; + } + + private ValueTask OnSaveFSV() { - StartConvert(ConvertTarget.Clipboard); + StartConvert(ConvertTarget.Save, CopyFormat.FSV); return ValueTask.CompletedTask; } } From be1c6d76392a5002f11d936b893bedea0a2a7a51 Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Thu, 21 Dec 2023 16:15:21 -0800 Subject: [PATCH 2/2] Addressed feedback --- .../Converters/JsonTable/JsonTableConverterGuiTool.cs | 8 +------- .../Text/MarkdownPreview/MarkdownPreviewGuiTool.cs | 11 +++++++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs index 3e057004ed..e6f04bc792 100644 --- a/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs +++ b/src/app/dev/DevToys.Tools/Tools/Converters/JsonTable/JsonTableConverterGuiTool.cs @@ -141,20 +141,14 @@ private async Task ConvertAsync(ConvertTarget target, CopyFormat? copyFormat, Ca break; case ConvertTarget.Save: - Stream? fileStream = null; - try { - fileStream = await _fileStorage.PickSaveFileAsync("txt", "csv"); + using Stream? fileStream = await _fileStorage.PickSaveFileAsync("txt", "csv"); if (fileStream is not null) { using var writer = new StreamWriter(fileStream); writer.Write(conversionResult.Text); } } - finally - { - fileStream?.Dispose(); - } break; } } diff --git a/src/app/dev/DevToys.Tools/Tools/Text/MarkdownPreview/MarkdownPreviewGuiTool.cs b/src/app/dev/DevToys.Tools/Tools/Text/MarkdownPreview/MarkdownPreviewGuiTool.cs index 0c35660d2c..aad28e5c1b 100644 --- a/src/app/dev/DevToys.Tools/Tools/Text/MarkdownPreview/MarkdownPreviewGuiTool.cs +++ b/src/app/dev/DevToys.Tools/Tools/Text/MarkdownPreview/MarkdownPreviewGuiTool.cs @@ -29,7 +29,7 @@ internal sealed class MarkdownPreviewGuiTool : IGuiTool, IDisposable private static readonly SettingDefinition theme = new( name: $"{nameof(MarkdownPreviewGuiTool)}.{nameof(theme)}", - defaultValue: MarkdownPreviewTheme.Dark); + defaultValue: MarkdownPreviewTheme.Light); private enum GridRows { @@ -52,10 +52,17 @@ private enum GridColumns private CancellationTokenSource? _cancellationTokenSource; [ImportingConstructor] - public MarkdownPreviewGuiTool(ISettingsProvider settingsProvider) + public MarkdownPreviewGuiTool(ISettingsProvider settingsProvider, IThemeListener themeListener) { _logger = this.Log(); _settingsProvider = settingsProvider; + + // Override the theme with the one defined in the app settings. + _settingsProvider.SetSetting( + theme, + themeListener.ActualAppTheme == ApplicationTheme.Dark + ? MarkdownPreviewTheme.Dark + : MarkdownPreviewTheme.Light); } // For unit tests.