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().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()) // 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().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().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."; } } } /// This regex won't match \r\n, only \n. private static readonly Regex _OnlyUnixLineBreaks = new("(? -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); } } }