496 lines
17 KiB
C#
496 lines
17 KiB
C#
using Microsoft.Win32;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Json2CSharpCodeGenerator.Lib.WinForms;
|
|
|
|
public partial class MainForm : Form
|
|
{
|
|
private bool _PreventReentrancy = false;
|
|
|
|
public MainForm()
|
|
{
|
|
// `IconTitleFont` is what WinForms *should* be using by default.
|
|
// Need to set `Font` first, before `InitializeComponent();` to ensure font inheritance by controls in the form.
|
|
Font = SystemFonts.IconTitleFont;
|
|
|
|
InitializeComponent();
|
|
|
|
ResetFonts();
|
|
|
|
// Also: https://docs.microsoft.com/en-us/dotnet/desktop/winforms/how-to-respond-to-font-scheme-changes-in-a-windows-forms-application?view=netframeworkdesktop-4.8
|
|
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
|
|
|
|
//
|
|
|
|
openButton.Click += OpenButton_Click;
|
|
|
|
optAttribJP.CheckedChanged += OnAttributesModeCheckedChanged;
|
|
optAttribJpn.CheckedChanged += OnAttributesModeCheckedChanged;
|
|
//optAttribNone .CheckedChanged += OnAttributesModeCheckedChanged;
|
|
|
|
optMemberFields.CheckedChanged += OnMemberModeCheckedChanged;
|
|
optMemberProps.CheckedChanged += OnMemberModeCheckedChanged;
|
|
|
|
optTypesMutablePoco.CheckedChanged += OnOutputTypeModeCheckedChanged;
|
|
optTypesImmutablePoco.CheckedChanged += OnOutputTypeModeCheckedChanged;
|
|
optTypesRecords.CheckedChanged += OnOutputTypeModeCheckedChanged;
|
|
|
|
optsPascalCase.CheckedChanged += OnOptionsChanged;
|
|
|
|
wrapText.CheckedChanged += WrapText_CheckedChanged;
|
|
|
|
copyOutput.Click += CopyOutput_Click;
|
|
copyOutput.Enabled = false;
|
|
|
|
jsonInputTextbox.TextChanged += JsonInputTextbox_TextChanged;
|
|
jsonInputTextbox.DragDrop += JsonInputTextbox_DragDrop;
|
|
jsonInputTextbox.DragOver += JsonInputTextbox_DragOver;
|
|
//jsonInputTextbox.paste // annoyingly, it isn't (easily) feasible to hook/detect TextBox paste events, even globally... grrr.
|
|
|
|
// Invoke event-handlers to set initial toolstrip text:
|
|
optsAttributeMode.Tag = optsAttributeMode.Text + ": {0}";
|
|
optMembersMode.Tag = optMembersMode.Text + ": {0}";
|
|
optTypesMode.Tag = optTypesMode.Text + ": {0}";
|
|
|
|
OnAttributesModeCheckedChanged(optAttribJP, EventArgs.Empty);
|
|
OnMemberModeCheckedChanged(optMemberProps, EventArgs.Empty);
|
|
OnOutputTypeModeCheckedChanged(optTypesMutablePoco, EventArgs.Empty);
|
|
}
|
|
|
|
private void WrapText_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
ToolStripButton tsb = (ToolStripButton)sender;
|
|
|
|
// For some reason, toggling WordWrap causes a text selection in `jsonInputTextbox`. So, doing this:
|
|
try
|
|
{
|
|
jsonInputTextbox.HideSelection = true;
|
|
// ayayayay: https://stackoverflow.com/questions/1140250/how-to-remove-the-focus-from-a-textbox-in-winforms
|
|
ActiveControl = toolStrip;
|
|
|
|
#if WINFORMS_TEXTBOX_GET_SCROLL_POSITION_WORKS_ARGH // It's non-trivial: https://stackoverflow.com/questions/4494162/change-scrollbar-position-in-textbox
|
|
//int idx1 = jsonInputTextbox.GetFirstCharIndexOfCurrentLine(); // but what is the "current line"?
|
|
int firstLineCharIndex = -1;
|
|
if( jsonInputTextbox.Height > 10 )
|
|
{
|
|
// https://stackoverflow.com/questions/10175400/maintain-textbox-scroll-position-while-adding-line
|
|
jsonInputTextbox.GetCharIndexFromPosition( new Point( 3, 3 ) );
|
|
}
|
|
#endif
|
|
|
|
jsonInputTextbox.WordWrap = tsb.Checked;
|
|
csharpOutputTextbox.WordWrap = tsb.Checked;
|
|
|
|
#if WINFORMS_TEXTBOX_GET_SCROLL_POSITION_WORKS_ARGH
|
|
if( firstLineCharIndex > 0 ) // Greater than zero, not -1, because `GetCharIndexFromPosition` returns a meaningless zero sometimes.
|
|
{
|
|
jsonInputTextbox.SelectionStart = firstLineCharIndex;
|
|
jsonInputTextbox.ScrollToCaret();
|
|
}
|
|
#endif
|
|
}
|
|
finally
|
|
{
|
|
jsonInputTextbox.HideSelection = false;
|
|
}
|
|
}
|
|
|
|
#region WinForms Taxes
|
|
|
|
private static Font GetMonospaceFont(float emFontSizePoints)
|
|
{
|
|
// See if Consolas or Lucida Sans Typewriter is available before falling-back:
|
|
string[] preferredFonts = new[] { "Consolas", "Lucida Sans Typewriter" };
|
|
foreach (string fontName in preferredFonts)
|
|
{
|
|
if (TestFont(fontName, emFontSizePoints))
|
|
{
|
|
return new Font(fontName, emFontSizePoints, FontStyle.Regular);
|
|
}
|
|
}
|
|
|
|
// Fallback:
|
|
return new Font(FontFamily.GenericMonospace, emSize: emFontSizePoints);
|
|
}
|
|
|
|
private static bool TestFont(string fontName, float emFontSizePoints)
|
|
{
|
|
try
|
|
{
|
|
using Font test = new(fontName, emFontSizePoints, FontStyle.Regular);
|
|
return test.Name == fontName;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
|
|
{
|
|
switch (e.Category)
|
|
{
|
|
case UserPreferenceCategory.Accessibility:
|
|
case UserPreferenceCategory.Window:
|
|
case UserPreferenceCategory.VisualStyle:
|
|
case UserPreferenceCategory.Menu:
|
|
ResetFonts();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ResetFonts()
|
|
{
|
|
Font = SystemFonts.IconTitleFont;
|
|
|
|
Font monospaceFont = GetMonospaceFont(emFontSizePoints: SystemFonts.IconTitleFont.SizeInPoints);
|
|
jsonInputTextbox.Font = monospaceFont;
|
|
csharpOutputTextbox.Font = monospaceFont;
|
|
}
|
|
|
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
|
{
|
|
base.OnFormClosing(e);
|
|
|
|
if (!e.Cancel)
|
|
{
|
|
SystemEvents.UserPreferenceChanged -= new UserPreferenceChangedEventHandler(SystemEvents_UserPreferenceChanged);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods to ensure only a single checkbox-style menu item is checked at-a-time, and that the ToolStripDropDownButton's text indicates the currently selected option:
|
|
|
|
private void OnAttributesModeCheckedChanged(object sender, EventArgs e)
|
|
{
|
|
EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText((ToolStripMenuItem)sender, defaultItem: optAttribJP, parent: optsAttributeMode);
|
|
|
|
GenerateCSharp();
|
|
}
|
|
|
|
private void OnMemberModeCheckedChanged(object sender, EventArgs e)
|
|
{
|
|
EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText((ToolStripMenuItem)sender, defaultItem: optMemberProps, parent: optMembersMode);
|
|
|
|
GenerateCSharp();
|
|
}
|
|
|
|
private void OnOutputTypeModeCheckedChanged(object sender, EventArgs e)
|
|
{
|
|
EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText((ToolStripMenuItem)sender, defaultItem: optTypesMutablePoco, parent: optTypesMode);
|
|
|
|
GenerateCSharp();
|
|
}
|
|
|
|
private void EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText(ToolStripMenuItem subject, ToolStripMenuItem defaultItem, ToolStripDropDownButton parent)
|
|
{
|
|
if (_PreventReentrancy)
|
|
return;
|
|
try
|
|
{
|
|
_PreventReentrancy = true;
|
|
|
|
ToolStripMenuItem singleCheckedItem;
|
|
if (subject.Checked)
|
|
{
|
|
singleCheckedItem = subject;
|
|
UncheckOthers(subject, parent);
|
|
}
|
|
else
|
|
{
|
|
EnsureAtLeast1IsCheckedAfterItemWasUnchecked(subject, defaultItem, parent);
|
|
singleCheckedItem = parent.DropDownItems.Cast<ToolStripMenuItem>().Single(item => item.Checked);
|
|
}
|
|
|
|
string parentTextFormat = (string)parent.Tag;
|
|
parent.Text = string.Format(format: parentTextFormat, arg0: singleCheckedItem.Text);
|
|
}
|
|
finally
|
|
{
|
|
_PreventReentrancy = false;
|
|
}
|
|
}
|
|
|
|
private static void UncheckOthers(ToolStripMenuItem sender, ToolStripDropDownButton parent)
|
|
{
|
|
foreach (ToolStripMenuItem menuItem in parent.DropDownItems.Cast<ToolStripMenuItem>()) // I really hate old-style IEnumerable, *grumble*
|
|
{
|
|
if (!ReferenceEquals(menuItem, sender))
|
|
{
|
|
menuItem.Checked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void EnsureAtLeast1IsCheckedAfterItemWasUnchecked(ToolStripMenuItem subject, ToolStripMenuItem defaultItem, ToolStripDropDownButton parent)
|
|
{
|
|
int countChecked = parent.DropDownItems.Cast<ToolStripMenuItem>().Count(item => item.Checked);
|
|
|
|
if (countChecked == 1)
|
|
{
|
|
// Is exactly 1 checked already? If so, then NOOP.
|
|
}
|
|
else if (countChecked > 1)
|
|
{
|
|
// If more than 1 are checked, then check only the default:
|
|
defaultItem.Checked = true;
|
|
UncheckOthers(sender: defaultItem, parent);
|
|
}
|
|
else
|
|
{
|
|
// If none are checked, then *if* the unchecked item is NOT the default item, then check the default item:
|
|
if (!ReferenceEquals(subject, defaultItem))
|
|
{
|
|
defaultItem.Checked = true;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, check the first non-default item:
|
|
ToolStripMenuItem nextBestItem = parent.DropDownItems.Cast<ToolStripMenuItem>().First(item => item != defaultItem);
|
|
nextBestItem.Checked = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void OnOptionsChanged(object sender, EventArgs e) => GenerateCSharp();
|
|
|
|
#region Drag and Drop
|
|
|
|
private void JsonInputTextbox_DragOver(object sender, DragEventArgs e)
|
|
{
|
|
bool acceptable =
|
|
e.Data.GetDataPresent(DataFormats.FileDrop) ||
|
|
// e.Data.GetDataPresent( DataFormats.Text ) ||
|
|
// e.Data.GetDataPresent( DataFormats.OemText ) ||
|
|
e.Data.GetDataPresent(DataFormats.UnicodeText, autoConvert: true)// ||
|
|
// e.Data.GetDataPresent( DataFormats.Html ) ||
|
|
// e.Data.GetDataPresent( DataFormats.StringFormat ) ||
|
|
// e.Data.GetDataPresent( DataFormats.Rtf )
|
|
;
|
|
|
|
if (acceptable)
|
|
{
|
|
e.Effect = DragDropEffects.Copy;
|
|
}
|
|
else
|
|
{
|
|
e.Effect = DragDropEffects.None;
|
|
}
|
|
}
|
|
|
|
private void JsonInputTextbox_DragDrop(object sender, DragEventArgs e)
|
|
{
|
|
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
|
{
|
|
string[] fileNames = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
if (fileNames.Length >= 1)
|
|
{
|
|
// hmm, maybe allow multiple files by concatenating them all into a horrible JSON array? :D
|
|
TryLoadJsonFile(fileNames[0]);
|
|
}
|
|
}
|
|
else if (e.Data.GetDataPresent(DataFormats.UnicodeText, autoConvert: true))
|
|
{
|
|
statusStrip.Text = "";
|
|
|
|
string text = (string)e.Data.GetData(DataFormats.UnicodeText, autoConvert: true);
|
|
if (text != null)
|
|
{
|
|
jsonInputTextbox.Text = text; // This will invoke `GenerateCSharp()`.
|
|
|
|
statusStrip.Text = "Loaded JSON from drag and drop data.";
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>This regex won't match <c>\r\n</c>, only <c>\n</c>.</summary>
|
|
private static readonly Regex _OnlyUnixLineBreaks = new("(?<!\r)\n", RegexOptions.Compiled); // Don't use `[^\r]?\n` because it *will* match `\r\n`, and don't use `[^\r]\n` because it won't match a leading `$\n` in a file.
|
|
|
|
private static string RepairLineBreaks(string text)
|
|
{
|
|
if (_OnlyUnixLineBreaks.IsMatch(text))
|
|
{
|
|
return _OnlyUnixLineBreaks.Replace(text, replacement: "\r\n");
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Open JSON file
|
|
|
|
private void OpenButton_Click(object sender, EventArgs e)
|
|
{
|
|
if (ofd.ShowDialog(owner: this) == DialogResult.OK)
|
|
{
|
|
TryLoadJsonFile(ofd.FileName);
|
|
}
|
|
}
|
|
|
|
private void TryLoadJsonFile(string filePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath))
|
|
{
|
|
csharpOutputTextbox.Text = "Error: an empty file path was specified.";
|
|
}
|
|
// else if ( filePath.IndexOfAny( Path.GetInvalidFileNameChars() ) > -1 )
|
|
// {
|
|
// const String fmt = "Invalid file path: \"{0}\"";
|
|
// csharpOutputTextbox.Text = String.Format( CultureInfo.CurrentCulture, fmt, filePath );
|
|
// }
|
|
else
|
|
{
|
|
FileInfo jsonFileInfo;
|
|
try
|
|
{
|
|
jsonFileInfo = new FileInfo(filePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
const string fmt = "Invalid file path: \"{0}\"\r\n{1}";
|
|
csharpOutputTextbox.Text = string.Format(CultureInfo.CurrentCulture, fmt, filePath, ex.ToString());
|
|
return;
|
|
}
|
|
|
|
TryLoadJsonFile(jsonFileInfo);
|
|
}
|
|
}
|
|
|
|
private void TryLoadJsonFile(FileInfo jsonFile)
|
|
{
|
|
if (jsonFile is null)
|
|
return;
|
|
|
|
statusStrip.Text = "";
|
|
|
|
try
|
|
{
|
|
jsonFile.Refresh();
|
|
if (jsonFile.Exists)
|
|
{
|
|
string jsonText = File.ReadAllText(jsonFile.FullName);
|
|
jsonInputTextbox.Text = jsonText; // This will invoke `GenerateCSharp()`.
|
|
|
|
statusStrip.Text = "Loaded \"" + jsonFile.FullName + "\" successfully.";
|
|
}
|
|
else
|
|
{
|
|
csharpOutputTextbox.Text = string.Format(CultureInfo.CurrentCulture, "Error: File \"{0}\" does not exist.", jsonFile.FullName);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
const string fmt = "Error loading file: \"{0}\"\r\n{1}";
|
|
|
|
csharpOutputTextbox.Text = string.Format(CultureInfo.CurrentCulture, fmt, jsonFile.FullName, ex.ToString());
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void ConfigureGenerator(IJsonClassGeneratorConfig config)
|
|
{
|
|
config.UsePascalCase = optsPascalCase.Checked;
|
|
config.UseThisKeyWord = false;
|
|
//
|
|
|
|
if (optAttribJP.Checked)
|
|
{
|
|
config.AttributeLibrary = JsonLibrary.NewtonsoftJson;
|
|
}
|
|
else// implicit: ( optAttribJpn.Checked )
|
|
{
|
|
config.AttributeLibrary = JsonLibrary.SystemTextJson;
|
|
}
|
|
|
|
//
|
|
|
|
if (optMemberProps.Checked)
|
|
{
|
|
config.MutableClasses.Members = OutputMembers.AsProperties;
|
|
}
|
|
else// implicit: ( optMemberFields.Checked )
|
|
{
|
|
config.MutableClasses.Members = OutputMembers.AsPublicFields;
|
|
}
|
|
|
|
//
|
|
|
|
if (optTypesImmutablePoco.Checked)
|
|
{
|
|
config.OutputType = OutputTypes.ImmutableClass;
|
|
}
|
|
else if (optTypesMutablePoco.Checked)
|
|
{
|
|
config.OutputType = OutputTypes.MutableClass;
|
|
}
|
|
else// implicit: ( optTypesRecords.Checked )
|
|
{
|
|
config.OutputType = OutputTypes.ImmutableRecord;
|
|
}
|
|
}
|
|
|
|
private void JsonInputTextbox_TextChanged(object sender, EventArgs e)
|
|
{
|
|
if (_PreventReentrancy)
|
|
return;
|
|
_PreventReentrancy = true;
|
|
try
|
|
{
|
|
jsonInputTextbox.Text = RepairLineBreaks(jsonInputTextbox.Text);
|
|
|
|
GenerateCSharp();
|
|
}
|
|
finally
|
|
{
|
|
_PreventReentrancy = false;
|
|
}
|
|
}
|
|
|
|
private void GenerateCSharp()
|
|
{
|
|
copyOutput.Enabled = false;
|
|
|
|
string jsonText = jsonInputTextbox.Text;
|
|
if (string.IsNullOrWhiteSpace(jsonText))
|
|
{
|
|
csharpOutputTextbox.Text = string.Empty;
|
|
return;
|
|
}
|
|
|
|
JsonClassGenerator generator = new();
|
|
ConfigureGenerator(generator);
|
|
|
|
try
|
|
{
|
|
StringBuilder sb = generator.GenerateClasses(jsonText, errorMessage: out string errorMessage);
|
|
if (!string.IsNullOrWhiteSpace(errorMessage))
|
|
{
|
|
csharpOutputTextbox.Text = "Error:\r\n" + errorMessage;
|
|
}
|
|
else
|
|
{
|
|
csharpOutputTextbox.Text = sb.ToString();
|
|
copyOutput.Enabled = true;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
csharpOutputTextbox.Text = "Error:\r\n" + ex.ToString();
|
|
}
|
|
}
|
|
|
|
private void CopyOutput_Click(object sender, EventArgs e)
|
|
{
|
|
if (csharpOutputTextbox.Text?.Length > 0)
|
|
{
|
|
Clipboard.SetText(csharpOutputTextbox.Text, TextDataFormat.UnicodeText);
|
|
}
|
|
}
|
|
} |