diff --git a/CommonUtil/TextUtil.cs b/CommonUtil/TextUtil.cs index b595661..5930c72 100644 --- a/CommonUtil/TextUtil.cs +++ b/CommonUtil/TextUtil.cs @@ -238,5 +238,27 @@ namespace CommonUtil { } return arr; } + + /// + /// Converts a char[] to a string, inserting line numbers at the start of each line. + /// Assumes lines end with '\n' (with or without a preceding '\r'). + /// + /// Character data to process. + /// String with line numbers. + public static string CharArrayToLineNumberedString(char[] data) { + StringBuilder sb = new StringBuilder(data.Length + data.Length / 40); // guess + int lineStart = 0; + int lineNum = 0; + + for (int i = 0; i < data.Length; i++) { + if (data[i] == '\n') { + sb.AppendFormat("{0,4:D0} ", ++lineNum); + sb.Append(data, lineStart, i - lineStart + 1); + lineStart = i + 1; + } + } + + return sb.ToString(); + } } } diff --git a/SourceGenWPF/AsmGen/WpfGui/GenAndAsm.xaml b/SourceGenWPF/AsmGen/WpfGui/GenAndAsm.xaml new file mode 100644 index 0000000..e33fe2f --- /dev/null +++ b/SourceGenWPF/AsmGen/WpfGui/GenAndAsm.xaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Assembler: + + + + ONE + TWO + + + + + + diff --git a/SourceGenWPF/AsmGen/WpfGui/GenAndAsm.xaml.cs b/SourceGenWPF/AsmGen/WpfGui/GenAndAsm.xaml.cs new file mode 100644 index 0000000..2f137f3 --- /dev/null +++ b/SourceGenWPF/AsmGen/WpfGui/GenAndAsm.xaml.cs @@ -0,0 +1,376 @@ +/* + * Copyright 2019 faddenSoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Windows; +using System.Windows.Controls; + +using CommonUtil; + +namespace SourceGenWPF.AsmGen.WpfGui { + /// + /// Code generation and assembler execution dialog. + /// + public partial class GenAndAsm : Window { + private const int PREVIEW_BUF_SIZE = 64 * 1024; // 64KB should be enough for preview + private static string NO_PREVIEW_FILES = "<" + Res.Strings.NO_FILES_AVAILABLE + ">"; + + /// + /// Holds data for the preview combo box. + /// + private class ComboPath { + public string FileName { get; private set; } + public string PathName { get; private set; } + public ComboPath(string pathName) { + PathName = pathName; + if (string.IsNullOrEmpty(pathName)) { + FileName = NO_PREVIEW_FILES; + } else { + FileName = Path.GetFileName(pathName); + } + } + public override string ToString() { + return FileName; + } + } + + /// + /// Project with data. + /// + private DisasmProject mProject; + + /// + /// Directory where generated files and assembler output will go. + /// + private string mWorkDirectory; + + /// + /// Base file name. For example, if this is "GenFile", we might generate + /// "GenFile_Cc65.S". + /// + private string mBaseFileName; + + /// + /// Currently-selected assembler ID. + /// + private AssemblerInfo.Id mSelectedAssemblerId; + + /// + /// Results from last source generation. + /// + private List mGenerationResults; + + /// + /// Holds an item for the pick-your-assembler combox box. + /// + private class AsmComboItem { + public AssemblerInfo.Id AssemblerId { get; private set; } + public string Name { get; private set; } + public AssemblerVersion AsmVersion { get; private set; } + + public AsmComboItem(AssemblerInfo info, AssemblerVersion version) { + AssemblerId = info.AssemblerId; + Name = info.Name; + AsmVersion = version; + } + // This determines what the combo box shows. + public override string ToString() { + if (AsmVersion == null) { + return Name + " " + Res.Strings.ASM_LATEST_VERSION; + } else { + return Name + " v" + AsmVersion.VersionStr; + } + } + } + + + /// + /// Constructor. + /// + /// Project reference. + /// Full path to the project file. + public GenAndAsm(Window owner, DisasmProject project, string projectPathName) { + InitializeComponent(); + Owner = owner; + + mProject = project; + mWorkDirectory = Path.GetDirectoryName(projectPathName); + mBaseFileName = Path.GetFileNameWithoutExtension(projectPathName); + + workDirectoryTextBox.Text = mWorkDirectory; + } + + private void Window_Loaded(object sender, RoutedEventArgs e) { + // Try to select the previously-used asm format. + string defaultAsm = + AppSettings.Global.GetString(AppSettings.SRCGEN_DEFAULT_ASM, string.Empty); + PopulateAssemblerComboBox(defaultAsm); + + ResetElements(); + } + + /// + /// Populates the assembler combo box. Attempts to match the defaultAsm arg with + /// the entries to configure the initial value. + /// + private void PopulateAssemblerComboBox(string defaultAsm) { + //assemblerComboBox.DisplayMember = "Name"; // show this property + + assemblerComboBox.Items.Clear(); + IEnumerator iter = AssemblerInfo.GetInfoEnumerator(); + bool foundMatch = false; + while (iter.MoveNext()) { + AssemblerInfo info = iter.Current; + AssemblerVersion version = AssemblerVersionCache.GetVersion(info.AssemblerId); + AsmComboItem item = new AsmComboItem(info, version); + assemblerComboBox.Items.Add(item); + if (item.AssemblerId.ToString() == defaultAsm) { + Debug.WriteLine("matched current " + defaultAsm); + assemblerComboBox.SelectedItem = item; + foundMatch = true; + } + } + if (!foundMatch) { + // Need to do this or box will show empty. + assemblerComboBox.SelectedIndex = 0; + } + } + + /// + /// Updates the selected assembler as the combo box selection changes. This is + /// expected to be called during the window load event, to initialize the field. + /// + private void AssemblerComboBox_SelectionChanged(object sender, + SelectionChangedEventArgs e) { + AsmComboItem sel = (AsmComboItem)assemblerComboBox.SelectedItem; + if (sel == null) { + // this happens on Items.Clear() + return; + } + if (mSelectedAssemblerId != sel.AssemblerId) { + // Selection changed, discard window contents. + mSelectedAssemblerId = sel.AssemblerId; + AppSettings.Global.SetString(AppSettings.SRCGEN_DEFAULT_ASM, + mSelectedAssemblerId.ToString()); + ResetElements(); + } + } + + /// + /// Loads the appropriate preview file when the combo box selection changes. + /// + private void PreviewFileComboBox_SelectionChanged(object sender, + SelectionChangedEventArgs e) { + ComboPath cpath = (ComboPath)previewFileComboBox.SelectedItem; + if (cpath == null || string.IsNullOrEmpty(cpath.PathName)) { + // nothing to do + return; + } + + LoadPreviewFile(cpath.PathName); + } + + /// + /// Resets all of the active elements to the initial state, before any source code + /// was generated. + /// + private void ResetElements() { + mGenerationResults = null; + previewFileComboBox.Items.Clear(); + previewFileComboBox.Items.Add(new ComboPath(null)); + previewFileComboBox.SelectedIndex = 0; + + previewTextBox.Text = string.Empty; + + cmdOutputTextBox.Text = string.Empty; + + UpdateAssemblerControls(); + } + + /// + /// Updates the controls in the lower (assembler) half of the dialog. + /// + private void UpdateAssemblerControls() { + bool asmConf = IsAssemblerConfigured(); + //Debug.WriteLine("ID=" + mSelectedAssemblerId + " asmConf=" + asmConf); + asmNotConfiguredText.Visibility = asmConf ? Visibility.Hidden : Visibility.Visible; + if (mGenerationResults == null || !asmConf) { + runAssemblerButton.IsEnabled = false; + } else { + runAssemblerButton.IsEnabled = true; + } + } + + /// + /// Returns true if the selected cross-assembler executable has been configured. + /// + private bool IsAssemblerConfigured() { + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, mSelectedAssemblerId); + return config != null && !string.IsNullOrEmpty(config.ExecutablePath); + } + + private void AssemblerSettingsButton_Click(object sender, RoutedEventArgs e) { + // Pop open the app settings dialog, with the appropriate tab selected. +#if false + mMainCtrl.ShowAppSettings(AppForms.EditAppSettings.Tab.AsmConfig, + mSelectedAssemblerId); +#endif + + // Update the controls based on whether or not the assembler is now available. + UpdateAssemblerControls(); + AsmComboItem item = (AsmComboItem)assemblerComboBox.SelectedItem; + Debug.Assert(item != null); + PopulateAssemblerComboBox(item.AssemblerId.ToString()); + } + + private void GenerateButton_Click(object sender, RoutedEventArgs e) { +#if false + IGenerator gen = AssemblerInfo.GetGenerator(mSelectedAssemblerId); + if (gen == null) { + Debug.WriteLine("Unable to get generator for " + mSelectedAssemblerId); + return; + } + gen.Configure(mProject, mWorkDirectory, mBaseFileName, + AssemblerVersionCache.GetVersion(mSelectedAssemblerId), AppSettings.Global); + + GeneratorProgress dlg = new GeneratorProgress(gen); + dlg.ShowDialog(); + Debug.WriteLine("Dialog returned: " + dlg.DialogResult); + + List pathNames = dlg.Results; + dlg.Dispose(); + + if (pathNames == null) { + // errors already reported + return; + } + + ResetElements(); + mGenerationResults = pathNames; + previewFileComboBox.Items.Clear(); + foreach (string str in pathNames) { + previewFileComboBox.Items.Add(new ComboPath(str)); + } + previewFileComboBox.SelectedIndex = 0; // should trigger update + + UpdateAssemblerControls(); +#else + Debug.WriteLine("GENERATE"); +#endif + } + + private void LoadPreviewFile(string pathName) { + Debug.WriteLine("LOAD " + pathName); + + try { + using (StreamReader sr = new StreamReader(pathName, Encoding.UTF8)) { + char[] bigbuf = new char[PREVIEW_BUF_SIZE]; + int actual = sr.Read(bigbuf, 0, bigbuf.Length); + string str = TextUtil.CharArrayToLineNumberedString(bigbuf); + if (actual < PREVIEW_BUF_SIZE) { + previewTextBox.Text = str; + } else { + previewTextBox.Text = str + "\r\n" + + Res.Strings.ERR_TOO_LARGE_FOR_PREVIEW; + } + } + } catch (Exception ex) { + previewTextBox.Text = ex.ToString(); + } + } + + private void RunAssemblerButton_Click(object sender, RoutedEventArgs e) { +#if false + IAssembler asm = AssemblerInfo.GetAssembler(mSelectedAssemblerId); + if (asm == null) { + Debug.WriteLine("Unable to get assembler for " + mSelectedAssemblerId); + return; + } + + asm.Configure(mGenerationResults, mWorkDirectory); + AssemblerProgress dlg = new AssemblerProgress(asm); + dlg.ShowDialog(); + Debug.WriteLine("Dialog returned: " + dlg.DialogResult); + if (dlg.DialogResult != DialogResult.OK) { + // Cancelled, or failed to even run the assembler. + return; + } + + AssemblerResults results = dlg.Results; + if (results == null) { + Debug.WriteLine("Dialog returned OK, but no assembler results found"); + Debug.Assert(false); + return; + } + + StringBuilder sb = + new StringBuilder(results.Stdout.Length + results.Stderr.Length + 200); + sb.Append(results.CommandLine); + sb.Append("\r\n"); + sb.AppendFormat("ExitCode={0} - ", results.ExitCode); + if (results.ExitCode == 0) { + FileInfo fi = new FileInfo(results.OutputPathName); + if (!fi.Exists) { + MessageBox.Show(this, Properties.Resources.ASM_OUTPUT_NOT_FOUND, + Properties.Resources.ASM_MISMATCH_CAPTION, + MessageBoxButtons.OK, MessageBoxIcon.Error); + sb.Append(Properties.Resources.ASM_MATCH_FAILURE); + } else if (!CommonUtil.FileUtil.CompareBinaryFile(mProject.FileData, + results.OutputPathName, out int offset, out byte fileVal)) { + if (fi.Length != mProject.FileData.Length && + offset == fi.Length || offset == mProject.FileData.Length) { + // The files matched up to the point where one ended. + string msg = string.Format(Properties.Resources.ASM_MISMATCH_LENGTH_FMT, + fi.Length, mProject.FileData.Length); + MessageBox.Show(this, msg, Properties.Resources.ASM_MISMATCH_CAPTION, + MessageBoxButtons.OK, MessageBoxIcon.Error); + sb.Append(msg); + } else { + string msg = string.Format(Properties.Resources.ASM_MISMATCH_DATA_FMT, + offset, fileVal, mProject.FileData[offset]); + MessageBox.Show(this, msg, Properties.Resources.ASM_MISMATCH_CAPTION, + MessageBoxButtons.OK, MessageBoxIcon.Error); + sb.Append(msg); + } + } else { + sb.Append(Properties.Resources.ASM_MATCH_SUCCESS); + } + } + sb.Append("\r\n\r\n"); + + if (results.Stdout != null && results.Stdout.Length > 2) { + sb.Append("----- stdout -----\r\n"); + sb.Append(results.Stdout); + sb.Append("\r\n"); + } + if (results.Stderr != null && results.Stderr.Length > 2) { + sb.Append("----- stderr -----\r\n"); + sb.Append(results.Stderr); + sb.Append("\r\n"); + } + + cmdOutputTextBox.Text = sb.ToString(); + cmdOutputTextBox.BackColor = SystemColors.Window; +#else + Debug.WriteLine("ASSEMBLE"); +#endif + } + } +} diff --git a/SourceGenWPF/MainController.cs b/SourceGenWPF/MainController.cs index 0ac8610..6f29952 100644 --- a/SourceGenWPF/MainController.cs +++ b/SourceGenWPF/MainController.cs @@ -1130,6 +1130,24 @@ namespace SourceGenWPF { return mProject != null; } + public void AssembleProject() { + if (string.IsNullOrEmpty(mProjectPathName)) { + // We need a project pathname so we know where to write the assembler + // source files, and what to call the output files. We could just pop up the + // Save As dialog, but that seems confusing unless we do a custom dialog with + // an explanation, or have some annoying click-through. + // + // This only appears for never-saved projects, not projects with unsaved data. + MessageBox.Show(Res.Strings.SAVE_BEFORE_ASM, Res.Strings.SAVE_BEFORE_ASM_CAPTION, + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + AsmGen.WpfGui.GenAndAsm dlg = + new AsmGen.WpfGui.GenAndAsm(mMainWin, mProject, mProjectPathName); + dlg.ShowDialog(); + } + public void HandleCodeListDoubleClick(int row, int col) { Debug.WriteLine("DCLICK: row=" + row + " col=" + col); diff --git a/SourceGenWPF/Res/Strings.xaml b/SourceGenWPF/Res/Strings.xaml index a0f63c7..31afc2b 100644 --- a/SourceGenWPF/Res/Strings.xaml +++ b/SourceGenWPF/Res/Strings.xaml @@ -19,6 +19,7 @@ limitations under the License. xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:local="clr-namespace:SourceGenWPF.Res"> + [latest version] Default Bad format descriptor at +{0:x6}. Bad format descriptor type @@ -41,6 +42,7 @@ limitations under the License. Project file may be corrupt Unable to load project file Unable to save project file + [File was too large for preview window] All files (*.*)|*.* C# Source Files(*.cs)|*.cs SourceGen projects(*.dis65)|*.dis65 @@ -54,6 +56,7 @@ limitations under the License. Extension scripts: Default settings: Symbol files: + no files available The file doesn't exist. File is empty Unable to load data file @@ -79,5 +82,7 @@ limitations under the License. The RuntimeData directory was not found. It should be in the same directory as the executable. RuntimeData Not Found + Please save your project before assembling. The generated source code will be placed in the same directory as the project file. + Save Project First {1} CPU @ {2} MHz \ No newline at end of file diff --git a/SourceGenWPF/Res/Strings.xaml.cs b/SourceGenWPF/Res/Strings.xaml.cs index fa0e70a..9b816ff 100644 --- a/SourceGenWPF/Res/Strings.xaml.cs +++ b/SourceGenWPF/Res/Strings.xaml.cs @@ -18,6 +18,8 @@ using System.Windows; namespace SourceGenWPF.Res { public static class Strings { + public static string ASM_LATEST_VERSION = + (string)Application.Current.FindResource("str_AsmLatestVersion"); public static string DEFAULT_VALUE = (string)Application.Current.FindResource("str_DefaultValue"); public static string ERR_BAD_FD = @@ -62,6 +64,8 @@ namespace SourceGenWPF.Res { (string)Application.Current.FindResource("str_ErrProjectLoadFail"); public static string ERR_PROJECT_SAVE_FAIL = (string)Application.Current.FindResource("str_ErrProjectSaveFail"); + public static string ERR_TOO_LARGE_FOR_PREVIEW = + (string)Application.Current.FindResource("str_ErrTooLargeForPreview"); public static string FILE_FILTER_ALL = (string)Application.Current.FindResource("str_FileFilterAll"); public static string FILE_FILTER_CS = @@ -88,6 +92,8 @@ namespace SourceGenWPF.Res { (string)Application.Current.FindResource("str_InitialParameters"); public static string INITIAL_SYMBOL_FILES = (string)Application.Current.FindResource("str_InitialSymbolFiles"); + public static string NO_FILES_AVAILABLE = + (string)Application.Current.FindResource("str_NoFilesAvailable"); public static string OPEN_DATA_DOESNT_EXIST = (string)Application.Current.FindResource("str_OpenDataDoesntExist"); public static string OPEN_DATA_EMPTY = @@ -138,6 +144,10 @@ namespace SourceGenWPF.Res { (string)Application.Current.FindResource("str_RuntimeDirNotFound"); public static string RUNTIME_DIR_NOT_FOUND_CAPTION = (string)Application.Current.FindResource("str_RuntimeDirNotFoundCaption"); + public static string SAVE_BEFORE_ASM = + (string)Application.Current.FindResource("str_SaveBeforeAsm"); + public static string SAVE_BEFORE_ASM_CAPTION = + (string)Application.Current.FindResource("str_SaveBeforeAsmCaption"); public static string SETUP_SYSTEM_SUMMARY_FMT = (string)Application.Current.FindResource("str_SetupSystemSummaryFmt"); } diff --git a/SourceGenWPF/SourceGenWPF.csproj b/SourceGenWPF/SourceGenWPF.csproj index 897816b..051146e 100644 --- a/SourceGenWPF/SourceGenWPF.csproj +++ b/SourceGenWPF/SourceGenWPF.csproj @@ -73,6 +73,9 @@ + + GenAndAsm.xaml + @@ -158,6 +161,10 @@ + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/SourceGenWPF/WpfGui/MainWindow.xaml.cs b/SourceGenWPF/WpfGui/MainWindow.xaml.cs index 7e62f40..7896395 100644 --- a/SourceGenWPF/WpfGui/MainWindow.xaml.cs +++ b/SourceGenWPF/WpfGui/MainWindow.xaml.cs @@ -718,8 +718,7 @@ namespace SourceGenWPF.WpfGui { #region Command handlers private void AssembleCmd_Executed(object sender, ExecutedRoutedEventArgs e) { - // test - Debug.WriteLine("assembling"); + mMainCtrl.AssembleProject(); } private void CloseCmd_Executed(object sender, ExecutedRoutedEventArgs e) {