/* * 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 Microsoft.Win32; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Windows; using Asm65; using CommonUtil; using SourceGenWPF.ProjWin; using System.Web.Script.Serialization; namespace SourceGenWPF { /// /// This class manages user interaction. The goal is for this to be relatively /// GUI-toolkit-agnostic, with all the WPF stuff tucked into the code-behind files. An /// instance of this class is created by MainWindow when the app starts. /// /// There is some Windows-specific stuff, like MessageBox and OpenFileDialog. /// public class MainController { #region Project state // Currently open project, or null if none. private DisasmProject mProject; // Pathname to 65xx data file. private string mDataPathName; // Pathname of .dis65 file. This will be empty for a new project. private string mProjectPathName; #if false /// /// Symbol subset, used to supply data to the symbol ListView. Initialized with /// an empty symbol table. /// private SymbolTableSubset mSymbolSubset; #endif /// /// Data backing the code list. /// public LineListGen CodeListGen { get; private set; } #endregion Project state /// /// Reference back to MainWindow object. /// private MainWindow mMainWin; /// /// List of recently-opened projects. /// private List mRecentProjectPaths = new List(MAX_RECENT_PROJECTS); public const int MAX_RECENT_PROJECTS = 6; /// /// Activity log generated by the code and data analyzers. Displayed in window. /// private DebugLog mGenerationLog; /// /// Timing data generated during analysis. /// TaskTimer mReanalysisTimer = new TaskTimer(); /// /// Stack for navigate forward/backward. /// private NavStack mNavStack = new NavStack(); /// /// Output format configuration. /// private Formatter.FormatConfig mFormatterConfig; /// /// Output format controller. /// /// This is shared with the DisplayList. /// private Formatter mOutputFormatter; /// /// Pseudo-op names. /// /// This is shared with the DisplayList. /// private PseudoOp.PseudoOpNames mPseudoOpNames; /// /// String we most recently searched for. /// private string mFindString = string.Empty; /// /// Initial start point of most recent search. /// private int mFindStartIndex = -1; /// /// Used to highlight the line that is the target of the selected line. /// private int mTargetHighlightIndex = -1; /// /// CPU definition used when the Formatter was created. If the CPU choice or /// inclusion of undocumented opcodes changes, we need to wipe the formatter. /// private CpuDef mOutputFormatterCpuDef; /// /// Instruction description object. Used for Info window. /// private OpDescription mOpDesc = OpDescription.GetOpDescription(null); /// /// If true, plugins will execute in the main application's AppDomain instead of /// the sandbox. /// private bool mUseMainAppDomainForPlugins = false; #region Init and settings public MainController(MainWindow win) { mMainWin = win; } /// /// Perform one-time initialization after the Window has finished loading. We defer /// to this point so we can report fatal errors directly to the user. /// public void WindowLoaded() { if (RuntimeDataAccess.GetDirectory() == null) { MessageBox.Show(Res.Strings.RUNTIME_DIR_NOT_FOUND, Res.Strings.RUNTIME_DIR_NOT_FOUND_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); Application.Current.Shutdown(); return; } #if false try { PluginDllCache.PreparePluginDir(); } catch (Exception ex) { string pluginPath = PluginDllCache.GetPluginDirPath(); if (pluginPath == null) { pluginPath = ""; } string msg = string.Format(Properties.Resources.PLUGIN_DIR_FAIL, pluginPath + ": " + ex.Message); MessageBox.Show(this, msg, Properties.Resources.PLUGIN_DIR_FAIL_CAPTION, MessageBoxButtons.OK, MessageBoxIcon.Error); Application.Exit(); return; } #endif #if false logoPictureBox.ImageLocation = RuntimeDataAccess.GetPathName(LOGO_FILE_NAME); versionLabel.Text = string.Format(Properties.Resources.VERSION_FMT, Program.ProgramVersion); toolStripStatusLabel.Text = Properties.Resources.STATUS_READY; mProjectControl = this.codeListView; mNoProjectControl = this.noProjectPanel; // Clone the menu structure from the designer. The same items are used for // both Edit > Actions and the right-click context menu in codeListView. mActionsMenuItems = new ToolStripItem[actionsToolStripMenuItem.DropDownItems.Count]; for (int i = 0; i < actionsToolStripMenuItem.DropDownItems.Count; i++) { mActionsMenuItems[i] = actionsToolStripMenuItem.DropDownItems[i]; } #endif #if false // Load the settings from the file. Some things (like the symbol subset) need // these. The general "apply settings" doesn't happen until a bit later, after // the sub-windows have been initialized. LoadAppSettings(); // Init primary ListView (virtual, ownerdraw) InitCodeListView(); // Init Symbols ListView (virtual, non-ownerdraw) mSymbolSubset = new SymbolTableSubset(new SymbolTable()); symbolListView.SetDoubleBuffered(true); InitSymbolListView(); // Init References ListView (non-virtual, non-ownerdraw) referencesListView.SetDoubleBuffered(true); // Place the main window and apply the various settings. SetAppWindowLocation(); #endif ApplyAppSettings(); #if false UpdateActionMenu(); UpdateMenuItemsAndTitle(); UpdateRecentLinks(); ShowNoProject(); #endif ProcessCommandLine(); } private void ProcessCommandLine() { string[] args = Environment.GetCommandLineArgs(); if (args.Length == 2) { DoOpenFile(Path.GetFullPath(args[1])); } } /// /// Applies "actionable" settings to the ProjectView, pulling them out of the global /// settings object. If a project is open, refreshes the display list and all sub-windows. /// private void ApplyAppSettings() { Debug.WriteLine("ApplyAppSettings..."); AppSettings settings = AppSettings.Global; // Set up the formatter. mFormatterConfig = new Formatter.FormatConfig(); AsmGen.GenCommon.ConfigureFormatterFromSettings(AppSettings.Global, ref mFormatterConfig); mFormatterConfig.mEndOfLineCommentDelimiter = ";"; mFormatterConfig.mFullLineCommentDelimiterBase = ";"; mFormatterConfig.mBoxLineCommentDelimiter = string.Empty; mFormatterConfig.mAllowHighAsciiCharConst = true; mOutputFormatter = new Formatter(mFormatterConfig); mOutputFormatterCpuDef = null; // Set pseudo-op names. Entries aren't allowed to be blank, so we start with the // default values and merge in whatever the user has configured. mPseudoOpNames = PseudoOp.sDefaultPseudoOpNames.GetCopy(); string pseudoCereal = settings.GetString(AppSettings.FMT_PSEUDO_OP_NAMES, null); if (!string.IsNullOrEmpty(pseudoCereal)) { PseudoOp.PseudoOpNames deser = PseudoOp.PseudoOpNames.Deserialize(pseudoCereal); if (deser != null) { mPseudoOpNames.Merge(deser); } } #if false // Configure the Symbols window. symbolUserCheckBox.Checked = settings.GetBool(AppSettings.SYMWIN_SHOW_USER, false); symbolAutoCheckBox.Checked = settings.GetBool(AppSettings.SYMWIN_SHOW_AUTO, false); symbolProjectCheckBox.Checked = settings.GetBool(AppSettings.SYMWIN_SHOW_PROJECT, false); symbolPlatformCheckBox.Checked = settings.GetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false); symbolConstantCheckBox.Checked = settings.GetBool(AppSettings.SYMWIN_SHOW_CONST, false); symbolAddressCheckBox.Checked = settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR, false); // Set the code list view font. string fontStr = settings.GetString(AppSettings.CDLV_FONT, null); if (!string.IsNullOrEmpty(fontStr)) { FontConverter cvt = new FontConverter(); try { Font font = cvt.ConvertFromInvariantString(fontStr) as Font; codeListView.Font = font; Debug.WriteLine("Set font to " + font.ToString()); } catch (Exception ex) { Debug.WriteLine("Font convert failed: " + ex.Message); } } // Unpack the recent-project list. UnpackRecentProjectList(); // Enable the DEBUG menu if configured. bool showDebugMenu = AppSettings.Global.GetBool(AppSettings.DEBUG_MENU_ENABLED, false); if (dEBUGToolStripMenuItem.Visible != showDebugMenu) { dEBUGToolStripMenuItem.Visible = showDebugMenu; mainMenuStrip.Refresh(); } #endif // Finally, update the display list generator with all the fancy settings. if (CodeListGen != null) { // Regenerate the display list with the latest formatter config and // pseudo-op definition. (These are set as part of the refresh.) UndoableChange uc = UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DisplayOnly); ApplyChanges(new ChangeSet(uc), false); } } /// /// Ensures that the named project is at the top of the list. If it's elsewhere /// in the list, move it to the top. Excess items are removed. /// /// private void UpdateRecentProjectList(string projectPath) { if (string.IsNullOrEmpty(projectPath)) { // This can happen if you create a new project, then close the window // without having saved it. return; } int index = mRecentProjectPaths.IndexOf(projectPath); if (index == 0) { // Already in the list, nothing changes. No need to update anything else. return; } if (index > 0) { mRecentProjectPaths.RemoveAt(index); } mRecentProjectPaths.Insert(0, projectPath); // Trim the list to the max allowed. while (mRecentProjectPaths.Count > MAX_RECENT_PROJECTS) { Debug.WriteLine("Recent projects: dropping " + mRecentProjectPaths[MAX_RECENT_PROJECTS]); mRecentProjectPaths.RemoveAt(MAX_RECENT_PROJECTS); } // Store updated list in app settings. JSON-in-JSON is ugly and inefficient, // but it'll do for now. JavaScriptSerializer ser = new JavaScriptSerializer(); string cereal = ser.Serialize(mRecentProjectPaths); AppSettings.Global.SetString(AppSettings.PRVW_RECENT_PROJECT_LIST, cereal); #if false UpdateRecentLinks(); #endif } #endregion Init and settings #region Project management private bool PrepareNewProject(string dataPathName, SystemDef sysDef) { DisasmProject proj = new DisasmProject(); mDataPathName = dataPathName; mProjectPathName = string.Empty; byte[] fileData = null; try { fileData = LoadDataFile(dataPathName); } catch (Exception ex) { Debug.WriteLine("PrepareNewProject exception: " + ex); string message = Res.Strings.OPEN_DATA_FAIL_CAPTION; string caption = Res.Strings.OPEN_DATA_FAIL_MESSAGE + ": " + ex.Message; MessageBox.Show(caption, message, MessageBoxButton.OK, MessageBoxImage.Error); return false; } proj.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins; proj.Initialize(fileData.Length); proj.PrepForNew(fileData, Path.GetFileName(dataPathName)); proj.LongComments.Add(LineListGen.Line.HEADER_COMMENT_OFFSET, new MultiLineComment("6502bench SourceGen v" + App.ProgramVersion)); // The system definition provides a set of defaults that can be overridden. // We pull everything of interest out and then discard the object. proj.ApplySystemDef(sysDef); mProject = proj; return true; } private void FinishPrep() { string messages = mProject.LoadExternalFiles(); if (messages.Length != 0) { // ProjectLoadIssues isn't quite the right dialog, but it'll do. ProjectLoadIssues dlg = new ProjectLoadIssues(messages, ProjectLoadIssues.Buttons.Continue); dlg.ShowDialog(); } CodeListGen = new LineListGen(mProject, mMainWin.CodeDisplayList, mOutputFormatter, mPseudoOpNames); // Prep the symbol table subset object. Replace the old one with a new one. //mSymbolSubset = new SymbolTableSubset(mProject.SymbolTable); RefreshProject(UndoableChange.ReanalysisScope.CodeAndData); //ShowProject(); //InvalidateControls(null); mMainWin.ShowCodeListView = true; mNavStack.Clear(); // Want to do this after ShowProject() or we see a weird glitch. UpdateRecentProjectList(mProjectPathName); } /// /// Loads the data file, reading it entirely into memory. /// /// All errors are reported as exceptions. /// /// Full pathname. /// Data file contents. private byte[] LoadDataFile(string dataFileName) { byte[] fileData; using (FileStream fs = File.Open(dataFileName, FileMode.Open, FileAccess.Read)) { // Check length; should have been caught earlier. if (fs.Length > DisasmProject.MAX_DATA_FILE_SIZE) { throw new InvalidDataException( string.Format(Res.Strings.OPEN_DATA_TOO_LARGE_FMT, fs.Length / 1024, DisasmProject.MAX_DATA_FILE_SIZE / 1024)); } else if (fs.Length == 0) { throw new InvalidDataException(Res.Strings.OPEN_DATA_EMPTY); } fileData = new byte[fs.Length]; int actual = fs.Read(fileData, 0, (int)fs.Length); if (actual != fs.Length) { // Not expected -- should be able to read the entire file in one shot. throw new Exception(Res.Strings.OPEN_DATA_PARTIAL_READ); } } return fileData; } /// /// Applies the changes to the project, adds them to the undo stack, and updates /// the display. /// /// Set of changes to apply. private void ApplyUndoableChanges(ChangeSet cs) { if (cs.Count == 0) { Debug.WriteLine("ApplyUndoableChanges: change set is empty"); } ApplyChanges(cs, false); mProject.PushChangeSet(cs); #if false UpdateMenuItemsAndTitle(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } #endif } /// /// Applies the changes to the project, and updates the display. /// /// This is called by the undo/redo commands. Don't call this directly from the /// various UI-driven functions, as this does not add the change to the undo stack. /// /// Set of changes to apply. /// If set, undo the changes instead. private void ApplyChanges(ChangeSet cs, bool backward) { mReanalysisTimer.Clear(); mReanalysisTimer.StartTask("ProjectView.ApplyChanges()"); mReanalysisTimer.StartTask("Save selection"); #if false int topItem = codeListView.TopItem.Index; #else int topItem = 0; #endif int topOffset = CodeListGen[topItem].FileOffset; LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate( CodeListGen, mMainWin.CodeDisplayList.SelectedIndices, topOffset); //savedSel.DebugDump(); mReanalysisTimer.EndTask("Save selection"); mReanalysisTimer.StartTask("Apply changes"); UndoableChange.ReanalysisScope needReanalysis = mProject.ApplyChanges(cs, backward, out RangeSet affectedOffsets); mReanalysisTimer.EndTask("Apply changes"); string refreshTaskStr = "Refresh w/reanalysis=" + needReanalysis; mReanalysisTimer.StartTask(refreshTaskStr); if (needReanalysis != UndoableChange.ReanalysisScope.None) { Debug.WriteLine("Refreshing project (" + needReanalysis + ")"); RefreshProject(needReanalysis); } else { Debug.WriteLine("Refreshing " + affectedOffsets.Count + " offsets"); RefreshCodeListViewEntries(affectedOffsets); mProject.Validate(); // shouldn't matter w/o reanalysis, but do it anyway } mReanalysisTimer.EndTask(refreshTaskStr); DisplayListSelection newSel = savedSel.Restore(CodeListGen, out int topIndex); //newSel.DebugDump(); // Refresh the various windows, and restore the selection. mReanalysisTimer.StartTask("Invalidate controls"); #if false InvalidateControls(newSel); #endif mReanalysisTimer.EndTask("Invalidate controls"); // This apparently has to be done after the EndUpdate, and inside try/catch. // See https://stackoverflow.com/questions/626315/ for notes. try { Debug.WriteLine("Setting TopItem to index=" + topIndex); #if false codeListView.TopItem = codeListView.Items[topIndex]; #endif } catch (NullReferenceException) { Debug.WriteLine("Caught an NRE from TopItem"); } mReanalysisTimer.EndTask("ProjectView.ApplyChanges()"); //mReanalysisTimer.DumpTimes("ProjectView timers:", mGenerationLog); #if false if (mShowAnalysisTimersDialog != null) { string timerStr = mReanalysisTimer.DumpToString("ProjectView timers:"); mShowAnalysisTimersDialog.BodyText = timerStr; } #endif // Lines may have moved around. Update the selection highlight. It's important // we do it here, and not down in DoRefreshProject(), because at that point the // ListView's selection index could be referencing a line off the end. #if false UpdateSelectionHighlight(); #endif } /// /// Refreshes the project after something of substance has changed. Some /// re-analysis will be done, followed by a complete rebuild of the DisplayList. /// /// Indicates whether reanalysis is required, and /// what level. private void RefreshProject(UndoableChange.ReanalysisScope reanalysisRequired) { Debug.Assert(reanalysisRequired != UndoableChange.ReanalysisScope.None); // NOTE: my goal is to arrange things so that reanalysis (data-only, and ideally // code+data) takes less than 100ms. With that response time there's no need for // background processing and progress bars. Since we need to do data-only // reanalysis after many common operations, the program becomes unpleasant to // use if we miss this goal, and progress bars won't make it less so. // Changing the CPU type or whether undocumented instructions are supported // invalidates the Formatter's mnemonic cache. We can change these values // through undo/redo, so we need to check it here. if (mOutputFormatterCpuDef != mProject.CpuDef) { // reference equality is fine Debug.WriteLine("CpuDef has changed, resetting formatter (now " + mProject.CpuDef + ")"); mOutputFormatter = new Formatter(mFormatterConfig); CodeListGen.SetFormatter(mOutputFormatter); CodeListGen.SetPseudoOpNames(mPseudoOpNames); mOutputFormatterCpuDef = mProject.CpuDef; } #if false if (mDisplayList.Count > 200000) { string prevStatus = toolStripStatusLabel.Text; // The Windows stuff can take 50-100ms, potentially longer than the actual // work, so don't bother unless the file is very large. try { mReanalysisTimer.StartTask("Do Windows stuff"); Application.UseWaitCursor = true; Cursor.Current = Cursors.WaitCursor; toolStripStatusLabel.Text = Res.Strings.STATUS_RECALCULATING; Refresh(); // redraw status label mReanalysisTimer.EndTask("Do Windows stuff"); DoRefreshProject(reanalysisRequired); } finally { Application.UseWaitCursor = false; toolStripStatusLabel.Text = prevStatus; } } else { #endif DoRefreshProject(reanalysisRequired); #if false } #endif if (FormatDescriptor.DebugCreateCount != 0) { Debug.WriteLine("FormatDescriptor total=" + FormatDescriptor.DebugCreateCount + " prefab=" + FormatDescriptor.DebugPrefabCount + " (" + (FormatDescriptor.DebugPrefabCount * 100) / FormatDescriptor.DebugCreateCount + "%)"); } } /// /// Updates all of the specified ListView entries. This is called after minor changes, /// such as editing a comment or renaming a label, that can be handled by regenerating /// selected parts of the DisplayList. /// /// private void RefreshCodeListViewEntries(RangeSet offsetSet) { IEnumerator iter = offsetSet.RangeListIterator; while (iter.MoveNext()) { RangeSet.Range range = iter.Current; CodeListGen.GenerateRange(range.Low, range.High); } } private void DoRefreshProject(UndoableChange.ReanalysisScope reanalysisRequired) { if (reanalysisRequired != UndoableChange.ReanalysisScope.DisplayOnly) { mGenerationLog = new CommonUtil.DebugLog(); mGenerationLog.SetMinPriority(CommonUtil.DebugLog.Priority.Debug); mGenerationLog.SetShowRelTime(true); mReanalysisTimer.StartTask("Call DisasmProject.Analyze()"); mProject.Analyze(reanalysisRequired, mGenerationLog, mReanalysisTimer); mReanalysisTimer.EndTask("Call DisasmProject.Analyze()"); } if (mGenerationLog != null) { //mReanalysisTimer.StartTask("Save _log"); //mGenerationLog.WriteToFile(@"C:\Src\WorkBench\SourceGen\TestData\_log.txt"); //mReanalysisTimer.EndTask("Save _log"); #if false if (mShowAnalyzerOutputDialog != null) { mShowAnalyzerOutputDialog.BodyText = mGenerationLog.WriteToString(); } #endif } mReanalysisTimer.StartTask("Generate DisplayList"); CodeListGen.GenerateAll(); mReanalysisTimer.EndTask("Generate DisplayList"); } #endregion Project management #region Main window UI event handlers #if false /// /// Restores the ListView selection by applying a diff between the old and /// new selection bitmaps. /// /// The virtual list view doesn't change the selection when we rebuild the /// list. It would be expensive to set all the bits, so we just update the /// entries that changed. /// /// Before returning, mCodeViewSelection is replaced with curSel. /// /// Selection bits for the current display list. private void RestoreSelection(DisplayListSelection curSel) { Debug.Assert(curSel != null); // We have to replace mCodeViewSelection immediately, because changing // the selection will cause ItemSelectionChanged events to fire, invoking // callbacks that expect the new selection object. Things will explode if // the older list was shorter. DisplayListSelection prevSel = mCodeViewSelection; mCodeViewSelection = curSel; // Set everything that has changed between the two sets. int debugNumChanged = 0; int count = Math.Min(prevSel.Length, curSel.Length); int i; for (i = 0; i < count; i++) { if (prevSel[i] != curSel[i]) { codeListView.Items[i].Selected = curSel[i]; debugNumChanged++; } } // Set everything that wasn't there before. New entries default to unselected, // so we only need to do this if the new value is "true". for (; i < curSel.Length; i++) { // An ItemSelectionChanged event will fire that will cause curSel[i] to // be assigned. This is fine. if (curSel[i]) { codeListView.Items[i].Selected = curSel[i]; debugNumChanged++; } } Debug.WriteLine("RestoreSelection: changed " + debugNumChanged + " of " + curSel.Length + " lines"); } #endif public void OpenRecentProject(int projIndex) { if (!CloseProject()) { return; } //DoOpenFile(mRecentProjectPaths[projIndex]); if (projIndex == 0) { DoOpenFile(@"C:\Src\6502bench\EXTRA\ZIPPY#ff2000.dis65"); } else { DoOpenFile(@"C:\Src\6502bench\EXTRA\CRYLLAN.MISSION#b30100.dis65"); } } /// /// Handles opening an existing project by letting the user select the project file. /// private void DoOpen() { if (!CloseProject()) { return; } OpenFileDialog fileDlg = new OpenFileDialog() { Filter = ProjectFile.FILENAME_FILTER + "|" + Res.Strings.FILE_FILTER_ALL, FilterIndex = 1 }; if (fileDlg.ShowDialog() != true) { return; } string projPathName = Path.GetFullPath(fileDlg.FileName); DoOpenFile(projPathName); } /// /// Handles opening an existing project, given a pathname to the project file. /// private void DoOpenFile(string projPathName) { Debug.WriteLine("DoOpenFile: " + projPathName); Debug.Assert(mProject == null); if (!File.Exists(projPathName)) { string msg = string.Format(Res.Strings.ERR_FILE_NOT_FOUND_FMT, projPathName); MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); return; } DisasmProject newProject = new DisasmProject(); newProject.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins; // Deserialize the project file. I want to do this before loading the data file // in case we decide to store the data file name in the project (e.g. the data // file is a disk image or zip archive, and we need to know which part(s) to // extract). if (!ProjectFile.DeserializeFromFile(projPathName, newProject, out FileLoadReport report)) { // Should probably use a less-busy dialog for something simple like // "permission denied", but the open file dialog handles most simple // stuff directly. ProjectLoadIssues dlg = new ProjectLoadIssues(report.Format(), ProjectLoadIssues.Buttons.Cancel); dlg.ShowDialog(); // ignore dlg.DialogResult return; } // Now open the data file, generating the pathname by stripping off the ".dis65" // extension. If we can't find the file, show a message box and offer the option to // locate it manually, repeating the process until successful or canceled. const string UNKNOWN_FILE = "UNKNOWN"; string dataPathName; if (projPathName.Length <= ProjectFile.FILENAME_EXT.Length) { dataPathName = UNKNOWN_FILE; } else { dataPathName = projPathName.Substring(0, projPathName.Length - ProjectFile.FILENAME_EXT.Length); } byte[] fileData; while ((fileData = FindValidDataFile(ref dataPathName, newProject, out bool cancel)) == null) { if (cancel) { // give up Debug.WriteLine("Abandoning attempt to open project"); return; } } // If there were warnings, notify the user and give the a chance to cancel. if (report.Count != 0) { ProjectLoadIssues dlg = new ProjectLoadIssues(report.Format(), ProjectLoadIssues.Buttons.ContinueOrCancel); bool? ok = dlg.ShowDialog(); if (ok != true) { return; } } mProject = newProject; mProjectPathName = mProject.ProjectPathName = projPathName; mProject.SetFileData(fileData, Path.GetFileName(dataPathName)); FinishPrep(); } /// /// Finds and loads the specified data file. The file's length and CRC must match /// the project's expectations. /// /// Full path to file. /// Project object. /// Returns true if we want to cancel the attempt. /// private byte[] FindValidDataFile(ref string dataPathName, DisasmProject proj, out bool cancel) { FileInfo fi = new FileInfo(dataPathName); if (!fi.Exists) { Debug.WriteLine("File '" + dataPathName + "' doesn't exist"); dataPathName = ChooseDataFile(dataPathName, Res.Strings.OPEN_DATA_DOESNT_EXIST); cancel = (dataPathName == null); return null; } if (fi.Length != proj.FileDataLength) { Debug.WriteLine("File '" + dataPathName + "' has length=" + fi.Length + ", expected " + proj.FileDataLength); dataPathName = ChooseDataFile(dataPathName, string.Format(Res.Strings.OPEN_DATA_WRONG_LENGTH_FMT, fi.Length, proj.FileDataLength)); cancel = (dataPathName == null); return null; } byte[] fileData = null; try { fileData = LoadDataFile(dataPathName); } catch (Exception ex) { Debug.WriteLine("File '" + dataPathName + "' failed to load: " + ex.Message); dataPathName = ChooseDataFile(dataPathName, string.Format(Res.Strings.OPEN_DATA_LOAD_FAILED_FMT, ex.Message)); cancel = (dataPathName == null); return null; } uint crc = CRC32.OnWholeBuffer(0, fileData); if (crc != proj.FileDataCrc32) { Debug.WriteLine("File '" + dataPathName + "' has CRC32=" + crc + ", expected " + proj.FileDataCrc32); // Format the CRC as signed decimal, so that interested parties can // easily replace the value in the .dis65 file. dataPathName = ChooseDataFile(dataPathName, string.Format(Res.Strings.OPEN_DATA_WRONG_CRC_FMT, (int)crc, (int)proj.FileDataCrc32)); cancel = (dataPathName == null); return null; } cancel = false; return fileData; } /// /// Displays a "do you want to pick a different file" message, then (on OK) allows the /// user to select a file. /// /// Pathname of original file. /// Message to display in the message box. /// Full path of file to open. private string ChooseDataFile(string origPath, string errorMsg) { DataFileLoadIssue dlg = new DataFileLoadIssue(origPath, errorMsg); bool? ok = dlg.ShowDialog(); if (ok != true) { return null; } OpenFileDialog fileDlg = new OpenFileDialog() { FileName = Path.GetFileName(origPath), Filter = Res.Strings.FILE_FILTER_ALL }; if (fileDlg.ShowDialog() != true) { return null; } string newPath = Path.GetFullPath(fileDlg.FileName); Debug.WriteLine("User selected data file " + newPath); return newPath; } private bool DoSaveAs() { SaveFileDialog fileDlg = new SaveFileDialog() { Filter = ProjectFile.FILENAME_FILTER + "|" + Res.Strings.FILE_FILTER_ALL, FilterIndex = 1, ValidateNames = true, AddExtension = true, FileName = Path.GetFileName(mDataPathName) + ProjectFile.FILENAME_EXT }; if (fileDlg.ShowDialog() == true) { string pathName = Path.GetFullPath(fileDlg.FileName); Debug.WriteLine("Project save path: " + pathName); if (DoSave(pathName)) { // Success, record the path name. mProjectPathName = mProject.ProjectPathName = pathName; // add it to the title bar #if false UpdateMenuItemsAndTitle(); #endif return true; } } return false; } // Save the project. If it hasn't been saved before, use save-as behavior instead. private bool DoSave() { if (string.IsNullOrEmpty(mProjectPathName)) { return DoSaveAs(); } return DoSave(mProjectPathName); } private bool DoSave(string pathName) { Debug.WriteLine("SAVING " + pathName); if (!ProjectFile.SerializeToFile(mProject, pathName, out string errorMessage)) { MessageBox.Show(Res.Strings.ERR_PROJECT_SAVE_FAIL + ": " + errorMessage, Res.Strings.OPERATION_FAILED, MessageBoxButton.OK, MessageBoxImage.Error); return false; } mProject.ResetDirtyFlag(); #if false // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } UpdateMenuItemsAndTitle(); #endif // Update this, in case this was a new project. UpdateRecentProjectList(pathName); #if false // Seems like a good time to save this off too. SaveAppSettings(); #endif return true; } /// /// Closes the project and associated modeless dialogs. Unsaved changes will be /// lost, so if the project has outstanding changes the user will be given the /// opportunity to cancel. /// /// True if the project was closed, false if the user chose to cancel. public bool CloseProject() { Debug.WriteLine("ProjectView.DoClose() - dirty=" + (mProject == null ? "N/A" : mProject.IsDirty.ToString())); if (mProject != null && mProject.IsDirty) { DiscardChanges dlg = new DiscardChanges(); bool? ok = dlg.ShowDialog(); if (ok != true) { return false; } else if (dlg.UserChoice == DiscardChanges.Choice.SaveAndContinue) { if (!DoSave()) { return false; } } } #if false // Close modeless dialogs that depend on project. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.Close(); } if (mShowAnalysisTimersDialog != null) { mShowAnalysisTimersDialog.Close(); } if (mShowAnalyzerOutputDialog != null) { mShowAnalyzerOutputDialog.Close(); } if (mHexDumpDialog != null) { mHexDumpDialog.Close(); } #endif // Discard all project state. if (mProject != null) { mProject.Cleanup(); mProject = null; } mDataPathName = null; mProjectPathName = null; #if false mSymbolSubset = new SymbolTableSubset(new SymbolTable()); mCodeViewSelection = new VirtualListViewSelection(); mDisplayList = null; codeListView.VirtualListSize = 0; //codeListView.Items.Clear(); ShowNoProject(); InvalidateControls(null); #endif mMainWin.ShowCodeListView = false; mGenerationLog = null; // Not necessary, but it lets us check the memory monitor to see if we got // rid of everything. GC.Collect(); return true; } public bool IsProjectOpen() { return mProject != null; } /// /// Gathered facts about the current selection. Recalculated whenever the selection /// changes. /// public class SelectionState { // Number of selected items or lines, reduced. This will be: // 0 if no lines are selected // 1 if a single *item* is selected (regardless of number of lines) // >1 if more than one item is selected (exact value not specified) public int mNumSelected; // Single selection: the type of line selected. public LineListGen.Line.Type mLineType; // Single selection: is line an instruction with an operand. public bool mIsInstructionWithOperand; // Single selection: is line an EQU directive for a project symbol. public bool mIsProjectSymbolEqu; // Some totals. public EntityCounts mEntityCounts; } /// /// Updates Actions menu enable states when the selection changes. /// /// is selected. public SelectionState UpdateSelectionState() { int selCount = mMainWin.GetSelectionCount(); Debug.WriteLine("SelectionChanged, selCount=" + selCount); SelectionState state = new SelectionState(); // Use IsSingleItemSelected(), rather than just checking sel.Count, because we // want the user to be able to e.g. EditData on a multi-line string even if all // lines in the string are selected. if (selCount < 0) { // nothing selected, leave everything set to false / 0 state.mEntityCounts = new EntityCounts(); } else if (IsSingleItemSelected()) { int firstIndex = mMainWin.GetFirstSelectedIndex(); state.mNumSelected = 1; state.mEntityCounts = GatherEntityCounts(firstIndex); LineListGen.Line line = CodeListGen[firstIndex]; state.mLineType = line.LineType; state.mIsInstructionWithOperand = (line.LineType == LineListGen.Line.Type.Code && mProject.GetAnattrib(line.FileOffset).IsInstructionWithOperand); if (line.LineType == LineListGen.Line.Type.EquDirective) { // See if this EQU directive is for a project symbol. int symIndex = LineListGen.DefSymIndexFromOffset(line.FileOffset); DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex]; state.mIsProjectSymbolEqu = (defSym.SymbolSource == Symbol.Source.Project); } } else { state.mNumSelected = 2; state.mEntityCounts = GatherEntityCounts(-1); } return state; } /// /// Entity count collection, for GatherEntityCounts. /// public class EntityCounts { public int mCodeLines; public int mDataLines; public int mBlankLines; public int mControlLines; public int mCodeHints; public int mDataHints; public int mInlineDataHints; public int mNoHints; }; /// /// Gathers a count of different line types and hints that are currently selected. /// /// If a single line is selected, pass the index in. /// Otherwise, pass -1 to traverse the entire line list. /// Object with computed totals. private EntityCounts GatherEntityCounts(int singleLineIndex) { //DateTime startWhen = DateTime.Now; int codeLines, dataLines, blankLines, controlLines; int codeHints, dataHints, inlineDataHints, noHints; codeLines = dataLines = blankLines = controlLines = 0; codeHints = dataHints = inlineDataHints = noHints = 0; int startIndex, endIndex; if (singleLineIndex < 0) { startIndex = 0; endIndex = mMainWin.CodeDisplayList.Count - 1; } else { startIndex = endIndex = singleLineIndex; } for (int i = startIndex; i <= endIndex; i++) { if (!mMainWin.CodeDisplayList.SelectedIndices[i]) { // not selected, ignore continue; } LineListGen.Line line = CodeListGen[i]; switch (line.LineType) { case LineListGen.Line.Type.Code: codeLines++; break; case LineListGen.Line.Type.Data: dataLines++; break; case LineListGen.Line.Type.Blank: // Don't generally care how many blank lines there are, but we do want // to exclude them from the other categories: if we have nothing but // blank lines, there's nothing to do. blankLines++; break; default: // These are only editable as single-line items. We do allow mass // code hint selection to include them (they will be ignored). // org, equ, rwid, long comment... controlLines++; break; } // A single line can span multiple offsets, each of which could have a // different hint. Note the code/data hints are only applied to the first // byte of each selected line, so we're not quite in sync with that. for (int offset = line.FileOffset; offset < line.FileOffset + line.OffsetSpan; offset++) { switch (mProject.TypeHints[offset]) { case CodeAnalysis.TypeHint.Code: codeHints++; break; case CodeAnalysis.TypeHint.Data: dataHints++; break; case CodeAnalysis.TypeHint.InlineData: inlineDataHints++; break; case CodeAnalysis.TypeHint.NoHint: noHints++; break; default: Debug.Assert(false); break; } } } //Debug.WriteLine("GatherEntityCounts (len=" + CodeListGen.Count + ") took " + // (DateTime.Now - startWhen).TotalMilliseconds + " ms"); return new EntityCounts() { mCodeLines = codeLines, mDataLines = dataLines, mBlankLines = blankLines, mControlLines = controlLines, mCodeHints = codeHints, mDataHints = dataHints, mInlineDataHints = inlineDataHints, mNoHints = noHints }; } /// /// Determines whether the current selection spans a single item. This could be a /// single-line item or a multi-line item. /// private bool IsSingleItemSelected() { int firstIndex = mMainWin.GetFirstSelectedIndex(); if (firstIndex < 0) { // empty selection return false; } int lastIndex = mMainWin.GetLastSelectedIndex(); Debug.WriteLine("CHECK: first=" + firstIndex + ", last=" + lastIndex); if (lastIndex == firstIndex) { // only one line is selected return true; } // Just check the first and last entries to see if they're the same. LineListGen.Line firstItem = CodeListGen[firstIndex]; LineListGen.Line lastItem = CodeListGen[lastIndex]; if (firstItem.FileOffset == lastItem.FileOffset && firstItem.LineType == lastItem.LineType) { return true; } return false; } public void MarkAsType(CodeAnalysis.TypeHint hint, bool firstByteOnly) { RangeSet sel; if (firstByteOnly) { sel = new RangeSet(); foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) { int offset = CodeListGen[index].FileOffset; if (offset >= 0) { // Not interested in the header stuff for hinting. sel.Add(offset); } } } else { sel = OffsetSetFromSelected(); } TypedRangeSet newSet = new TypedRangeSet(); TypedRangeSet undoSet = new TypedRangeSet(); foreach (int offset in sel) { if (offset < 0) { // header comment continue; } CodeAnalysis.TypeHint oldType = mProject.TypeHints[offset]; if (oldType == hint) { // no change, don't add to set continue; } undoSet.Add(offset, (int)oldType); newSet.Add(offset, (int)hint); } if (newSet.Count == 0) { Debug.WriteLine("No changes found (" + hint + ", " + sel.Count + " offsets)"); return; } UndoableChange uc = UndoableChange.CreateTypeHintChange(undoSet, newSet); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } /// /// Converts the set of selected items into a set of offsets. If a line /// spans multiple offsets (e.g. a 3-byte instruction), offsets for every /// byte are included. /// /// Boundaries such as labels and address changes are ignored. /// /// RangeSet with all offsets. private RangeSet OffsetSetFromSelected() { RangeSet rs = new RangeSet(); foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) { int offset = CodeListGen[index].FileOffset; // Mark every byte of an instruction or multi-byte data item -- // everything that is represented by the line the user selected. int len; if (offset >= 0) { len = mProject.GetAnattrib(offset).Length; } else { // header area len = 1; } Debug.Assert(len > 0); for (int i = offset; i < offset + len; i++) { rs.Add(i); } } return rs; } #endregion Main window UI event handlers } }