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);
}
}
}