/* * 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.Text; using System.Web.Script.Serialization; using System.Windows; using Asm65; using CommonUtil; using CommonWPF; using SourceGenWPF.Sandbox; using SourceGenWPF.WpfGui; 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 { private const string SETTINGS_FILE_NAME = "SourceGen-settings"; #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; /// /// Data backing the code list. /// public LineListGen CodeLineList { get; private set; } #endregion Project state /// /// Reference back to MainWindow object. /// private MainWindow mMainWin; /// /// List of recently-opened projects. /// public List RecentProjectPaths = new List(MAX_RECENT_PROJECTS); public const int MAX_RECENT_PROJECTS = 6; /// /// Analyzed selection state, updated whenever the selection changes. /// public SelectionState SelectionAnalysis { get; set; } /// /// 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; /// /// Code list column numbers. /// public enum CodeListColumn { Offset = 0, Address, Bytes, Flags, Attributes, Label, Opcode, Operand, Comment, COUNT // must be last; must equal number of columns } /// /// Clipboard format enumeration. /// public enum ClipLineFormat { Unknown = -1, AssemblerSource = 0, Disassembly = 1 } /// /// True if a project is open and AnalyzeUncategorizedData is enabled. /// public bool IsAnalyzeUncategorizedDataEnabled { get { if (mProject == null) { return false; } return mProject.ProjectProps.AnalysisParams.AnalyzeUncategorizedData; } } #region Init and settings public MainController(MainWindow win) { mMainWin = win; ScriptManager.UseKeepAliveHack = true; } /// /// Early initialization, before the window is visible. Notably, we want to get the /// window placement data, so we can position and size the window before it's first /// drawn (avoids a blink). /// public void WindowSourceInitialized() { // Load the settings from the file. If this fails we have no way to tell the user, // so just keep going. LoadAppSettings(); SetAppWindowLocation(); } /// /// 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; } try { PluginDllCache.PreparePluginDir(); } catch (Exception ex) { string pluginPath = PluginDllCache.GetPluginDirPath(); if (pluginPath == null) { pluginPath = ""; } string msg = string.Format(Res.Strings.PLUGIN_DIR_FAIL_FMT, pluginPath + ": " + ex.Message); MessageBox.Show(msg, Res.Strings.PLUGIN_DIR_FAIL_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); Application.Current.Shutdown(); return; } // Place the main window and apply the various settings. ApplyAppSettings(); #if false UpdateMenuItemsAndTitle(); #endif mMainWin.UpdateRecentLinks(); ProcessCommandLine(); // Create an initial value. SelectionAnalysis = UpdateSelectionState(); } private void ProcessCommandLine() { string[] args = Environment.GetCommandLineArgs(); if (args.Length == 2) { DoOpenFile(Path.GetFullPath(args[1])); } } /// /// Loads settings from the settings file into AppSettings.Global. Does not apply /// them to the ProjectView. /// private void LoadAppSettings() { AppSettings settings = AppSettings.Global; // Set some default settings for first-time use. The general rule is to set // a default value of false, 0, or the empty string, so we only need to set // values here when that isn't the case. The point at which the setting is // actually used is expected to do something reasonable by default. settings.SetBool(AppSettings.SYMWIN_SHOW_USER, true); settings.SetBool(AppSettings.SYMWIN_SHOW_PROJECT, true); settings.SetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false); settings.SetBool(AppSettings.SYMWIN_SHOW_AUTO, false); settings.SetBool(AppSettings.SYMWIN_SHOW_CONST, true); settings.SetBool(AppSettings.SYMWIN_SHOW_ADDR, true); settings.SetBool(AppSettings.SYMWIN_SORT_ASCENDING, true); settings.SetInt(AppSettings.SYMWIN_SORT_COL, (int)Symbol.SymbolSortField.Name); settings.SetBool(AppSettings.FMT_UPPER_OPERAND_A, true); settings.SetBool(AppSettings.FMT_UPPER_OPERAND_S, true); settings.SetBool(AppSettings.FMT_ADD_SPACE_FULL_COMMENT, true); settings.SetString(AppSettings.FMT_OPCODE_SUFFIX_LONG, "l"); settings.SetString(AppSettings.FMT_OPERAND_PREFIX_ABS, "a:"); settings.SetString(AppSettings.FMT_OPERAND_PREFIX_LONG, "f:"); settings.SetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, true); settings.SetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, true); #if DEBUG settings.SetBool(AppSettings.DEBUG_MENU_ENABLED, true); #else settings.SetBool(AppSettings.DEBUG_MENU_ENABLED, false); #endif // Make sure we have entries for these. settings.SetString(AppSettings.CDLV_FONT_FAMILY, mMainWin.CodeListFontFamily.ToString()); settings.SetInt(AppSettings.CDLV_FONT_SIZE, (int)mMainWin.CodeListFontSize); // Load the settings file, and merge it into the globals. string runtimeDataDir = RuntimeDataAccess.GetDirectory(); if (runtimeDataDir == null) { Debug.WriteLine("Unable to load settings file"); return; } string settingsDir = Path.GetDirectoryName(runtimeDataDir); string settingsPath = Path.Combine(settingsDir, SETTINGS_FILE_NAME); try { string text = File.ReadAllText(settingsPath); AppSettings fileSettings = AppSettings.Deserialize(text); AppSettings.Global.MergeSettings(fileSettings); Debug.WriteLine("Settings file loaded and merged"); } catch (Exception ex) { Debug.WriteLine("Unable to read settings file: " + ex.Message); } } /// /// Saves AppSettings to a file. /// private void SaveAppSettings() { if (!AppSettings.Global.Dirty) { Debug.WriteLine("Settings not dirty, not saving"); return; } // Main window position and size. AppSettings.Global.SetString(AppSettings.MAIN_WINDOW_PLACEMENT, mMainWin.GetPlacement()); // Horizontal splitters. AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH, (int)mMainWin.LeftPanelWidth); AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH, (int)mMainWin.RightPanelWidth); // Vertical splitters. AppSettings.Global.SetInt(AppSettings.MAIN_REFERENCES_HEIGHT, (int)mMainWin.ReferencesPanelHeight); AppSettings.Global.SetInt(AppSettings.MAIN_SYMBOLS_HEIGHT, (int)mMainWin.SymbolsPanelHeight); mMainWin.CaptureColumnWidths(); string runtimeDataDir = RuntimeDataAccess.GetDirectory(); if (runtimeDataDir == null) { Debug.WriteLine("Unable to save settings file"); return; } string settingsDir = Path.GetDirectoryName(runtimeDataDir); string settingsPath = Path.Combine(settingsDir, SETTINGS_FILE_NAME); try { string cereal = AppSettings.Global.Serialize(); File.WriteAllText(settingsPath, cereal); AppSettings.Global.Dirty = false; Debug.WriteLine("Saved settings (" + settingsPath + ")"); } catch (Exception ex) { Debug.WriteLine("Failed to save settings: " + ex.Message); } } /// /// Replaces the contents of the global settings object with the new settings, /// then applies them to the project. /// /// public void SetAppSettings(AppSettings settings) { AppSettings.Global.ReplaceSettings(settings); ApplyAppSettings(); // We get called whenever Apply or OK is hit in the settings editor, so it's // a pretty good time to save the settings out. SaveAppSettings(); } /// /// Sets the app window's location and size. This should be called before the window has /// finished initialization. /// private void SetAppWindowLocation() { const int DEFAULT_SPLIT = 250; AppSettings settings = AppSettings.Global; string placement = settings.GetString(AppSettings.MAIN_WINDOW_PLACEMENT, null); if (placement != null) { mMainWin.SetPlacement(placement); } mMainWin.LeftPanelWidth = settings.GetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH, DEFAULT_SPLIT); mMainWin.RightPanelWidth = settings.GetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH, DEFAULT_SPLIT); mMainWin.ReferencesPanelHeight = settings.GetInt(AppSettings.MAIN_REFERENCES_HEIGHT, 350); mMainWin.SymbolsPanelHeight = settings.GetInt(AppSettings.MAIN_SYMBOLS_HEIGHT, 400); mMainWin.RestoreColumnWidths(); } /// /// 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); #endif // Get the configured font info. If nothing is configured, use whatever the // code list happens to be using now. string fontFamilyName = settings.GetString(AppSettings.CDLV_FONT_FAMILY, null); if (fontFamilyName == null) { fontFamilyName = mMainWin.CodeListFontFamily.ToString(); } int size = settings.GetInt(AppSettings.CDLV_FONT_SIZE, -1); if (size <= 0) { size = (int)mMainWin.CodeListFontSize; } mMainWin.SetCodeListFont(fontFamilyName, size); // Update the column widths. This was done earlier during init, but may need to be // repeated if the show/hide buttons were used in Settings. mMainWin.RestoreColumnWidths(); // Unpack the recent-project list. UnpackRecentProjectList(); #if false // 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 (CodeLineList != 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); } } private void UnpackRecentProjectList() { RecentProjectPaths.Clear(); string cereal = AppSettings.Global.GetString( AppSettings.PRVW_RECENT_PROJECT_LIST, null); if (string.IsNullOrEmpty(cereal)) { return; } try { JavaScriptSerializer ser = new JavaScriptSerializer(); RecentProjectPaths = ser.Deserialize>(cereal); } catch (Exception ex) { Debug.WriteLine("Failed deserializing recent projects: " + ex.Message); return; } } /// /// 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 = RecentProjectPaths.IndexOf(projectPath); if (index == 0) { // Already in the list, nothing changes. No need to update anything else. return; } if (index > 0) { RecentProjectPaths.RemoveAt(index); } RecentProjectPaths.Insert(0, projectPath); // Trim the list to the max allowed. while (RecentProjectPaths.Count > MAX_RECENT_PROJECTS) { Debug.WriteLine("Recent projects: dropping " + RecentProjectPaths[MAX_RECENT_PROJECTS]); RecentProjectPaths.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(RecentProjectPaths); AppSettings.Global.SetString(AppSettings.PRVW_RECENT_PROJECT_LIST, cereal); mMainWin.UpdateRecentLinks(); } #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(mMainWin, messages, ProjectLoadIssues.Buttons.Continue); dlg.ShowDialog(); } CodeLineList = new LineListGen(mProject, mMainWin.CodeDisplayList, mOutputFormatter, mPseudoOpNames); RefreshProject(UndoableChange.ReanalysisScope.CodeAndData); // Populate the Symbols list. PopulateSymbolsList(); // Load initial contents of Notes panel. PopulateNotesList(); mMainWin.ShowCodeListView = true; mNavStack.Clear(); 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"); int topItemIndex = mMainWin.CodeListView_GetTopIndex(); LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate( CodeLineList, mMainWin.CodeDisplayList.SelectedIndices, CodeLineList[topItemIndex].FileOffset); //savedSel.DebugDump(); // Clear this so we don't try to fiddle with it later. mTargetHighlightIndex = -1; 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(CodeLineList, out topItemIndex); //newSel.DebugDump(); // Restore the selection. The selection-changed event will cause updates to the // references, notes, and info panels. mReanalysisTimer.StartTask("Restore selection and top position"); mMainWin.CodeListView_SetSelection(newSel); mMainWin.CodeListView_SetTopIndex(topItemIndex); mReanalysisTimer.EndTask("Restore selection and top position"); // Update the Notes and Symbols windows. // TODO: references? PopulateNotesList(); PopulateSymbolsList(); 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); CodeLineList.SetFormatter(mOutputFormatter); CodeLineList.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; CodeLineList.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"); CodeLineList.GenerateAll(); mReanalysisTimer.EndTask("Generate DisplayList"); } #endregion Project management #region Main window UI event handlers public void NewProject() { if (!CloseProject()) { return; } string sysDefsPath = RuntimeDataAccess.GetPathName("SystemDefs.json"); if (sysDefsPath == null) { MessageBox.Show(Res.Strings.ERR_LOAD_CONFIG_FILE, Res.Strings.OPERATION_FAILED, MessageBoxButton.OK, MessageBoxImage.Error); return; } SystemDefSet sds = null; try { sds = SystemDefSet.ReadFile(sysDefsPath); } catch (Exception ex) { Debug.WriteLine("Failed loading system def set: " + ex); MessageBox.Show(Res.Strings.ERR_LOAD_CONFIG_FILE, Res.Strings.OPERATION_FAILED, MessageBoxButton.OK, MessageBoxImage.Error); return; } NewProject dlg = new NewProject(mMainWin, sds); if (dlg.ShowDialog() != true) { return; } bool ok = PrepareNewProject(Path.GetFullPath(dlg.DataFileName), dlg.SystemDef); if (ok) { FinishPrep(); } } public void OpenRecentProject(int projIndex) { if (!CloseProject()) { return; } DoOpenFile(RecentProjectPaths[projIndex]); } /// /// Handles opening an existing project by letting the user select the project file. /// public void OpenProject() { 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(mMainWin, 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(mMainWin, 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(mMainWin, 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; } /// /// Saves the project, querying for the filename. /// /// True on success, false if the save attempt failed or was canceled. public bool SaveProjectAs() { 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) { Debug.WriteLine("SaveAs canceled by user"); return false; } string pathName = Path.GetFullPath(fileDlg.FileName); Debug.WriteLine("Project save path: " + pathName); if (!DoSave(pathName)) { return false; } // Success, record the path name. mProjectPathName = mProject.ProjectPathName = pathName; // add it to the title bar #if false UpdateMenuItemsAndTitle(); #endif return true; } /// /// Saves the project. If it hasn't been saved before, use save-as behavior instead. /// /// True on success, false if the save attempt failed. public bool SaveProject() { if (string.IsNullOrEmpty(mProjectPathName)) { return SaveProjectAs(); } 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); // Seems like a good time to save this off too. SaveAppSettings(); return true; } /// /// Handles main window closing. /// /// True if it's okay for the window to close, false to cancel it. public bool WindowClosing() { SaveAppSettings(); return CloseProject(); } /// /// 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("CloseProject() - dirty=" + (mProject == null ? "N/A" : mProject.IsDirty.ToString())); if (mProject != null && mProject.IsDirty) { DiscardChanges dlg = new DiscardChanges(mMainWin); bool? ok = dlg.ShowDialog(); if (ok != true) { return false; } else if (dlg.UserChoice == DiscardChanges.Choice.SaveAndContinue) { if (!SaveProject()) { 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; mTargetHighlightIndex = -1; // Clear this to release the memory. mMainWin.CodeDisplayList.Clear(); mMainWin.InfoPanelContents = string.Empty; 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; } 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(); } /// /// Opens the application settings dialog. All changes to settings are made directly /// to the AppSettings.Global object. /// public void EditAppSettings() { EditAppSettings dlg = new EditAppSettings(mMainWin, mMainWin, this, WpfGui.EditAppSettings.Tab.Unknown, AsmGen.AssemblerInfo.Id.Unknown); dlg.ShowDialog(); } public void HandleCodeListDoubleClick(int row, int col) { Debug.WriteLine("DCLICK: row=" + row + " col=" + col); // Clicking on some types of lines, such as ORG directives, results in // specific behavior regardless of which column you click in. We're just // checking the clicked-on line to decide what action to take. If it doesn't // make sense to do for a multi-line selection, the action will have been // disabled. LineListGen.Line line = CodeLineList[row]; switch (line.LineType) { case LineListGen.Line.Type.EquDirective: // Currently only does something for project symbols; platform symbols // do nothing. #if false if (editProjectSymbolToolStripMenuItem.Enabled) { EditProjectSymbol_Click(sender, e); } #endif break; case LineListGen.Line.Type.OrgDirective: if (CanEditAddress()) { EditAddress(); } break; case LineListGen.Line.Type.RegWidthDirective: if (CanEditStatusFlags()) { EditStatusFlags(); } break; case LineListGen.Line.Type.LongComment: if (CanEditLongComment()) { EditLongComment(); } break; case LineListGen.Line.Type.Note: #if false if (editNoteToolStripMenuItem.Enabled) { EditNote_Click(sender, e); } #endif break; case LineListGen.Line.Type.Code: case LineListGen.Line.Type.Data: // For code and data, we have to break it down by column. switch ((CodeListColumn)col) { case CodeListColumn.Offset: // does nothing break; case CodeListColumn.Address: // edit address if (CanEditAddress()) { EditAddress(); } break; case CodeListColumn.Bytes: #if false if (showHexDumpToolStripMenuItem.Enabled) { ShowHexDump_Click(sender, e); } #endif break; case CodeListColumn.Flags: if (CanEditStatusFlags()) { EditStatusFlags(); } break; case CodeListColumn.Attributes: // does nothing break; case CodeListColumn.Label: if (CanEditLabel()) { EditLabel(); } break; case CodeListColumn.Opcode: // File offset should always be valid, since we excluded the EQU // statements and header comment earlier. if (line.FileOffset >= 0) { Anattrib attr = mProject.GetAnattrib(line.FileOffset); FormatDescriptor dfd = attr.DataDescriptor; // Does this have an operand with an in-file target offset? // (Resolve it as a numeric reference.) if (attr.OperandOffset >= 0) { // Yup, find the line for that offset and jump to it. GoToOffset(attr.OperandOffset, false, true); } else if (dfd != null && dfd.HasSymbol) { // Operand has a symbol, do a symbol lookup. int labelOffset = mProject.FindLabelOffsetByName( dfd.SymbolRef.Label); if (labelOffset >= 0) { GoToOffset(labelOffset, false, true); } } else if (attr.IsDataStart || attr.IsInlineDataStart) { // If it's an Address or Symbol, we can try to resolve // the value. (Symbols should have been resolved by the // previous clause, but Address entries would not have been.) int operandOffset = DataAnalysis.GetDataOperandOffset( mProject, line.FileOffset); if (operandOffset >= 0) { GoToOffset(operandOffset, false, true); } } } break; case CodeListColumn.Operand: if (CanEditOperand()) { EditOperand(); } break; case CodeListColumn.Comment: #if false if (editCommentToolStripMenuItem.Enabled) { EditComment_Click(sender, e); } #endif break; } break; default: Debug.WriteLine("Double-click: unhandled line type " + line.LineType); break; } } public bool CanEditAddress() { if (SelectionAnalysis.mNumItemsSelected != 1) { return false; } EntityCounts counts = SelectionAnalysis.mEntityCounts; // Line must be code, data, or an ORG directive. return (counts.mDataLines > 0 || counts.mCodeLines > 0) || (SelectionAnalysis.mLineType == LineListGen.Line.Type.OrgDirective); } public void EditAddress() { int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); int offset = CodeLineList[selIndex].FileOffset; Anattrib attr = mProject.GetAnattrib(offset); EditAddress dlg = new EditAddress(mMainWin, attr.Address, mProject.CpuDef.MaxAddressValue); if (dlg.ShowDialog() != true) { return; } if (offset == 0 && dlg.Address < 0) { // Not allowed. The AddressMap will just put it back, which confuses // the undo operation. Debug.WriteLine("EditAddress: not allowed to remove address at offset +000000"); } else if (attr.Address != dlg.Address) { Debug.WriteLine("EditAddress: changing addr at offset +" + offset.ToString("x6") + " to " + dlg.Address); AddressMap addrMap = mProject.AddrMap; // Get the previous address map entry for this exact offset, if one // exists. This may be different from the value used as the default // (attr.Address), which is the address assigned to the offset, in // the case where no previous mapping existed. int prevAddress = addrMap.Get(offset); UndoableChange uc = UndoableChange.CreateAddressChange(offset, prevAddress, dlg.Address); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } else { Debug.WriteLine("EditAddress: no change"); } } public void EditHeaderComment() { EditLongComment(LineListGen.Line.HEADER_COMMENT_OFFSET); } public bool CanEditLabel() { if (SelectionAnalysis.mNumItemsSelected != 1) { return false; } EntityCounts counts = SelectionAnalysis.mEntityCounts; // Single line, code or data. return (counts.mDataLines > 0 || counts.mCodeLines > 0); } public void EditLabel() { int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); int offset = CodeLineList[selIndex].FileOffset; Anattrib attr = mProject.GetAnattrib(offset); EditLabel dlg = new EditLabel(mMainWin, attr.Symbol, attr.Address, mProject.SymbolTable); if (dlg.ShowDialog() != true) { return; } // NOTE: if label matching is case-insensitive, we want to allow a situation // where a label is being renamed from "FOO" to "Foo". (We should be able to // test for object equality on the Symbol.) if (attr.Symbol != dlg.LabelSym) { Debug.WriteLine("Changing label at offset +" + offset.ToString("x6")); // For undo/redo, we want to update the UserLabels value. This may // be different from the Anattrib symbol, which can have an auto-generated // value. Symbol oldUserValue = null; if (mProject.UserLabels.ContainsKey(offset)) { oldUserValue = mProject.UserLabels[offset]; } UndoableChange uc = UndoableChange.CreateLabelChange(offset, oldUserValue, dlg.LabelSym); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } } public bool CanEditLongComment() { if (SelectionAnalysis.mNumItemsSelected != 1) { return false; } EntityCounts counts = SelectionAnalysis.mEntityCounts; // Single line, code or data, or a long comment. return (counts.mDataLines > 0 || counts.mCodeLines > 0 || SelectionAnalysis.mLineType == LineListGen.Line.Type.LongComment); } public void EditLongComment() { int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); int offset = CodeLineList[selIndex].FileOffset; EditLongComment(offset); } private void EditLongComment(int offset) { EditLongComment dlg = new EditLongComment(mMainWin, mOutputFormatter); if (mProject.LongComments.TryGetValue(offset, out MultiLineComment oldComment)) { dlg.LongComment = oldComment; } dlg.ShowDialog(); if (dlg.DialogResult == true) { MultiLineComment newComment = dlg.LongComment; if (oldComment != newComment) { Debug.WriteLine("Changing long comment at +" + offset.ToString("x6")); UndoableChange uc = UndoableChange.CreateLongCommentChange(offset, oldComment, newComment); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } } } public bool CanEditOperand() { if (SelectionAnalysis.mNumItemsSelected == 0) { return false; } else if (SelectionAnalysis.mNumItemsSelected == 1) { int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); int selOffset = CodeLineList[selIndex].FileOffset; bool editInstr = (CodeLineList[selIndex].LineType == LineListGen.Line.Type.Code && mProject.GetAnattrib(selOffset).IsInstructionWithOperand); bool editData = (CodeLineList[selIndex].LineType == LineListGen.Line.Type.Data); return editInstr || editData; } else { // Data operands are one of the few things we can edit in bulk. It's okay // if meta-data like ORGs and Notes are selected, but we don't allow it if // any code is selected. EntityCounts counts = SelectionAnalysis.mEntityCounts; return (counts.mDataLines > 0 && counts.mCodeLines == 0); } } public void EditOperand() { int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); int selOffset = CodeLineList[selIndex].FileOffset; if (CodeLineList[selIndex].LineType == LineListGen.Line.Type.Code) { EditInstructionOperand(selOffset); } else { Debug.Assert(CodeLineList[selIndex].LineType == LineListGen.Line.Type.Data); EditDataOperand(selOffset); } } private void EditInstructionOperand(int offset) { EditInstructionOperand dlg = new EditInstructionOperand(mMainWin, offset, mProject, mOutputFormatter); // We'd really like to pass in an indication of what the "default" format actually // resolved to, but we don't always know. If this offset has a FormatDescriptor, // we might not have auto-generated the label that would have been used otherwise. // We're editing the FormatDescriptor from OperandFormats, not Anattribs; // the latter may have auto-generated stuff. if (mProject.OperandFormats.TryGetValue(offset, out FormatDescriptor dfd)) { dlg.FormatDescriptor = dfd; } dlg.ShowDialog(); if (dlg.DialogResult != true) { return; } ChangeSet cs = new ChangeSet(1); // Handle shortcut actions. if (dlg.FormatDescriptor != dfd && dlg.ShortcutAction != WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelInstead) { // Note EditOperand returns a null descriptor when the user selects Default. // This is different from how EditData works, since that has to deal with // multiple regions. Debug.WriteLine("Changing " + dfd + " to " + dlg.FormatDescriptor); UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset, dfd, dlg.FormatDescriptor); cs.Add(uc); } else if (dfd != null && dlg.ShortcutAction == WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelInstead) { Debug.WriteLine("Removing existing label for CreateLabelInstead"); UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset, dfd, null); cs.Add(uc); } else { Debug.WriteLine("No change to format descriptor"); } switch (dlg.ShortcutAction) { case WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelInstead: case WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelAlso: Debug.Assert(!mProject.UserLabels.ContainsKey(dlg.ShortcutArg)); Anattrib targetAttr = mProject.GetAnattrib(dlg.ShortcutArg); Symbol newLabel = new Symbol(dlg.FormatDescriptor.SymbolRef.Label, targetAttr.Address, Symbol.Source.User, Symbol.Type.LocalOrGlobalAddr); UndoableChange uc = UndoableChange.CreateLabelChange(dlg.ShortcutArg, null, newLabel); cs.Add(uc); break; case WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateProjectSymbolAlso: Debug.Assert(!mProject.ProjectProps.ProjectSyms.ContainsKey( dlg.FormatDescriptor.SymbolRef.Label)); DefSymbol defSym = new DefSymbol(dlg.FormatDescriptor.SymbolRef.Label, dlg.ShortcutArg, Symbol.Source.Project, Symbol.Type.ExternalAddr, FormatDescriptor.SubType.Hex, string.Empty, string.Empty); ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps); newProps.ProjectSyms.Add(defSym.Label, defSym); uc = UndoableChange.CreateProjectPropertiesChange( mProject.ProjectProps, newProps); cs.Add(uc); break; case WpfGui.EditInstructionOperand.SymbolShortcutAction.None: break; } if (cs.Count != 0) { ApplyUndoableChanges(cs); } } private void EditDataOperand(int offset) { Debug.Assert(mMainWin.CodeListView_GetSelectionCount() > 0); TypedRangeSet trs = GroupedOffsetSetFromSelected(); if (trs.Count == 0) { Debug.Assert(false, "EditDataOperand found nothing to edit"); // shouldn't happen return; } // If the first offset has a FormatDescriptor, pass that in as a recommendation // for the default value in the dialog. This allows single-item editing to work // as expected. If the format can't be applied to the full selection (which // would disable that radio button), the dialog will have to pick something // that does work. // // We could pull this out of Anattribs, which would let the dialog reflect the // auto-format that the user was just looking at. However, I think it's better // if the dialog shows what's actually there, i.e. no formatting at all. IEnumerator iter = (IEnumerator)trs.GetEnumerator(); iter.MoveNext(); TypedRangeSet.Tuple firstOffset = iter.Current; mProject.OperandFormats.TryGetValue(firstOffset.Value, out FormatDescriptor dfd); EditDataOperand dlg = new EditDataOperand(mMainWin, mProject.FileData, mProject.SymbolTable, mOutputFormatter, trs, dfd); dlg.ShowDialog(); if (dlg.DialogResult == true) { // Merge the changes into the OperandFormats list. We need to remove all // FormatDescriptors that overlap the selected region. We don't need to // pass the selection set in, because the dlg.Results list spans the exact // set of ranges. // // If nothing actually changed, don't generate an undo record. ChangeSet cs = mProject.GenerateFormatMergeSet(dlg.Results); if (cs.Count != 0) { ApplyUndoableChanges(cs); } else { Debug.WriteLine("No change to data formats"); } } } public bool CanEditStatusFlags() { if (SelectionAnalysis.mNumItemsSelected != 1) { return false; } EntityCounts counts = SelectionAnalysis.mEntityCounts; // Single line, must be code or a RegWidth directive. return (SelectionAnalysis.mLineType == LineListGen.Line.Type.Code || SelectionAnalysis.mLineType == LineListGen.Line.Type.RegWidthDirective); } public void EditStatusFlags() { int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); int offset = CodeLineList[selIndex].FileOffset; EditStatusFlags dlg = new EditStatusFlags(mMainWin, mProject.StatusFlagOverrides[offset], mProject.CpuDef.HasEmuFlag); if (dlg.ShowDialog() != true) { return; } if (dlg.FlagValue != mProject.StatusFlagOverrides[offset]) { UndoableChange uc = UndoableChange.CreateStatusFlagChange(offset, mProject.StatusFlagOverrides[offset], dlg.FlagValue); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } } public void EditProjectProperties() { string projectDir = string.Empty; if (!string.IsNullOrEmpty(mProjectPathName)) { projectDir = Path.GetDirectoryName(mProjectPathName); } EditProjectProperties dlg = new EditProjectProperties(mMainWin, mProject.ProjectProps, projectDir, mOutputFormatter); dlg.ShowDialog(); ProjectProperties newProps = dlg.NewProps; // The dialog result doesn't matter, because the user might have hit "apply" // before hitting "cancel". if (newProps != null) { UndoableChange uc = UndoableChange.CreateProjectPropertiesChange( mProject.ProjectProps, newProps); ApplyUndoableChanges(new ChangeSet(uc)); } } public void Find() { FindBox dlg = new FindBox(mMainWin, mFindString); if (dlg.ShowDialog() == true) { mFindString = dlg.TextToFind; mFindStartIndex = -1; FindText(); } } public void FindNext() { FindText(); } private void FindText() { if (string.IsNullOrEmpty(mFindString)) { return; } // Start from the topmost selected line, or the start of the file if nothing // is selected. int index = mMainWin.CodeListView_GetFirstSelectedIndex(); if (index < 0) { index = 0; } // Start one past the selected item. index++; if (index == CodeLineList.Count) { index = 0; } //Debug.WriteLine("FindText index=" + index + " start=" + mFindStartIndex + // " str=" + mFindString); while (index != mFindStartIndex) { if (mFindStartIndex < 0) { // need to latch this inside the loop so the initial test doesn't fail mFindStartIndex = index; } string searchStr = CodeLineList.GetSearchString(index); int matchPos = searchStr.IndexOf(mFindString, StringComparison.InvariantCultureIgnoreCase); if (matchPos >= 0) { //Debug.WriteLine("Match " + index + ": " + searchStr); mMainWin.CodeListView_EnsureVisible(index); mMainWin.CodeListView_DeselectAll(); mMainWin.CodeListView_SelectRange(index, 1); return; } index++; if (index == CodeLineList.Count) { index = 0; } } // Announce that we've wrapped around, then clear the start index. MessageBox.Show(Res.Strings.FIND_REACHED_START, Res.Strings.FIND_REACHED_START_CAPTION, MessageBoxButton.OK, MessageBoxImage.Information); mFindStartIndex = -1; mMainWin.CodeListView_Focus(); } public void Goto() { GotoBox dlg = new GotoBox(mMainWin, mProject, mOutputFormatter); if (dlg.ShowDialog() == true) { GoToOffset(dlg.TargetOffset, false, true); mMainWin.CodeListView_Focus(); } } /// /// Moves the view and selection to the specified offset. We want to select stuff /// differently if we're jumping to a note vs. jumping to an instruction. /// /// Offset to jump to. /// If set, push new offset onto navigation stack. public void GoToOffset(int gotoOffset, bool jumpToNote, bool doPush) { NavStack.Location prevLoc = GetCurrentlySelectedLocation(); if (gotoOffset == prevLoc.Offset && jumpToNote == prevLoc.IsNote) { // we're jumping to ourselves? Debug.WriteLine("Ignoring goto to current position"); return; } int topLineIndex = CodeLineList.FindLineIndexByOffset(gotoOffset); if (topLineIndex < 0) { Debug.Assert(false, "failed goto offset +" + gotoOffset.ToString("x6")); return; } int lastLineIndex; if (jumpToNote) { // Select all note lines, disregard the rest. while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Note) { topLineIndex++; Debug.Assert(CodeLineList[topLineIndex].FileOffset == gotoOffset); } lastLineIndex = topLineIndex + 1; while (lastLineIndex < CodeLineList.Count && CodeLineList[lastLineIndex].LineType == LineListGen.Line.Type.Note) { lastLineIndex++; } } else if (gotoOffset < 0) { // This is the offset of the header comment or a .EQ directive. Don't mess with it. lastLineIndex = topLineIndex + 1; } else { // Advance to the code or data line. while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Code && CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Data) { topLineIndex++; } lastLineIndex = topLineIndex + 1; } // Make sure the item is visible. For notes, this can span multiple lines. mMainWin.CodeListView_EnsureVisible(lastLineIndex - 1); mMainWin.CodeListView_EnsureVisible(topLineIndex); // Update the selection. mMainWin.CodeListView_DeselectAll(); mMainWin.CodeListView_SelectRange(topLineIndex, lastLineIndex - topLineIndex); if (doPush) { // Update the back stack and associated controls. mNavStack.Push(prevLoc); } } private NavStack.Location GetCurrentlySelectedLocation() { int index = mMainWin.CodeListView_GetFirstSelectedIndex(); if (index < 0) { // nothing selected, use top instead index = mMainWin.CodeListView_GetTopIndex(); } int offset = CodeLineList[index].FileOffset; bool isNote = (CodeLineList[index].LineType == LineListGen.Line.Type.Note); return new NavStack.Location(offset, isNote); } public bool CanNavigateBackward() { return mNavStack.HasBackward; } public void NavigateBackward() { Debug.Assert(mNavStack.HasBackward); NavStack.Location backLoc = mNavStack.MoveBackward(GetCurrentlySelectedLocation()); GoToOffset(backLoc.Offset, backLoc.IsNote, false); } public bool CanNavigateForward() { return mNavStack.HasForward; } public void NavigateForward() { Debug.Assert(mNavStack.HasForward); NavStack.Location fwdLoc = mNavStack.MoveForward(GetCurrentlySelectedLocation()); GoToOffset(fwdLoc.Offset, fwdLoc.IsNote, false); } /// /// Scrolls the code list so that the specified label is shown. /// /// Label symbol. public void GoToLabel(Symbol sym) { if (sym.IsInternalLabel) { int offset = mProject.FindLabelOffsetByName(sym.Label); if (offset >= 0) { GoToOffset(offset, false, true); } else { Debug.WriteLine("DClick symbol: " + sym + ": label not found"); } } else { Debug.WriteLine("DClick symbol: " + sym + ": not label"); } } public void SelectionChanged() { SelectionAnalysis = UpdateSelectionState(); UpdateReferencesPanel(); UpdateInfoPanel(); UpdateSelectionHighlight(); } /// /// 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 mNumItemsSelected; // Single selection: the type of line selected. (Multi-sel: Unclassified) public LineListGen.Line.Type mLineType; // Single selection: is line an instruction with an operand. (Multi-sel: False) public bool mIsInstructionWithOperand; // Single selection: is line an EQU directive for a project symbol. (Multi-sel: False) public bool mIsProjectSymbolEqu; // Some totals. public EntityCounts mEntityCounts; public SelectionState() { mLineType = LineListGen.Line.Type.Unclassified; mEntityCounts = new EntityCounts(); } } /// /// Updates Actions menu enable states when the selection changes. /// /// is selected. public SelectionState UpdateSelectionState() { int selCount = mMainWin.CodeListView_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.CodeListView_GetFirstSelectedIndex(); state.mNumItemsSelected = 1; state.mEntityCounts = GatherEntityCounts(firstIndex); LineListGen.Line line = CodeLineList[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.mNumItemsSelected = 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 = CodeLineList[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.CodeListView_GetFirstSelectedIndex(); if (firstIndex < 0) { // empty selection return false; } int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex(); 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 = CodeLineList[firstIndex]; LineListGen.Line lastItem = CodeLineList[lastIndex]; if (firstItem.FileOffset == lastItem.FileOffset && firstItem.LineType == lastItem.LineType) { return true; } return false; } /// /// Updates the selection highlight. When a code item with an operand offset is /// selected, such as a branch, we want to highlight the address and label of the /// target. /// private void UpdateSelectionHighlight() { int targetIndex = FindSelectionHighlight(); if (mTargetHighlightIndex != targetIndex) { mMainWin.CodeListView_RemoveSelectionHighlight(mTargetHighlightIndex); mMainWin.CodeListView_AddSelectionHighlight(targetIndex); mTargetHighlightIndex = targetIndex; Debug.WriteLine("Selection highlight now " + targetIndex); } } private int FindSelectionHighlight() { if (mMainWin.CodeListView_GetSelectionCount() != 1) { return -1; } int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); LineListGen.Line line = CodeLineList[selIndex]; if (!line.IsCodeOrData) { return -1; } Debug.Assert(line.FileOffset >= 0); // Does this have an operand with an in-file target offset? Anattrib attr = mProject.GetAnattrib(line.FileOffset); if (attr.OperandOffset >= 0) { return CodeLineList.FindCodeDataIndexByOffset(attr.OperandOffset); } else if (attr.IsDataStart || attr.IsInlineDataStart) { // If it's an Address or Symbol, we can try to resolve // the value. int operandOffset = DataAnalysis.GetDataOperandOffset(mProject, line.FileOffset); if (operandOffset >= 0) { return CodeLineList.FindCodeDataIndexByOffset(operandOffset); } } return -1; } public bool CanUndo() { return (mProject != null && mProject.CanUndo); } /// /// Handles Edit - Undo. /// public void UndoChanges() { if (!mProject.CanUndo) { Debug.Assert(false, "Nothing to undo"); return; } ChangeSet cs = mProject.PopUndoSet(); ApplyChanges(cs, true); #if false UpdateMenuItemsAndTitle(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } #endif } public bool CanRedo() { return (mProject != null && mProject.CanRedo); } /// /// Handles Edit - Redo. /// public void RedoChanges() { if (!mProject.CanRedo) { Debug.Assert(false, "Nothing to redo"); return; } ChangeSet cs = mProject.PopRedoSet(); ApplyChanges(cs, false); #if false UpdateMenuItemsAndTitle(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } #endif } /// /// Handles the four Actions - edit hint commands. /// /// Type of hint to apply. /// If set, only the first byte on each line is hinted. public void MarkAsType(CodeAnalysis.TypeHint hint, bool firstByteOnly) { RangeSet sel; if (firstByteOnly) { sel = new RangeSet(); foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) { int offset = CodeLineList[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 = CodeLineList[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; } /// /// Handles Help - Help /// public void ShowHelp() { HelpAccess.ShowHelp(HelpAccess.Topic.Contents); } /// /// Handles Help - About /// public void ShowAboutBox() { AboutBox dlg = new AboutBox(mMainWin); dlg.ShowDialog(); } public void ToggleDataScan() { ProjectProperties oldProps = mProject.ProjectProps; ProjectProperties newProps = new ProjectProperties(oldProps); newProps.AnalysisParams.AnalyzeUncategorizedData = !newProps.AnalysisParams.AnalyzeUncategorizedData; UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(oldProps, newProps); ApplyUndoableChanges(new ChangeSet(uc)); } /// /// Converts the ListView's 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. /// /// Contiguous regions with user labels or address changes are split into /// independent regions by using a serial number for the range type. Same for /// long comments and notes. /// /// We don't split based on existing data format items. That would make it impossible /// to convert from (say) a collection of single bytes to a collection of double bytes /// or a string. It should not be possible to select part of a formatted section, /// unless the user has been playing weird games with type hints to get overlapping /// format descriptors. /// /// TypedRangeSet with all offsets. private TypedRangeSet GroupedOffsetSetFromSelected() { TypedRangeSet rs = new TypedRangeSet(); int groupNum = 0; int expectedAddr = -1; DateTime startWhen = DateTime.Now; int prevOffset = -1; foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) { // Don't add an offset to the set if the only part of it that is selected // is a directive or blank line. We only care about file offsets, so skip // anything that isn't code or data. if (!CodeLineList[index].IsCodeOrData) { continue; } int offset = CodeLineList[index].FileOffset; if (offset == prevOffset) { // This is a continuation of a multi-line item like a string. We've // already accounted for all bytes associated with this offset. continue; } Anattrib attr = mProject.GetAnattrib(offset); if (expectedAddr == -1) { expectedAddr = attr.Address; } // Check for user labels. if (mProject.UserLabels.ContainsKey(offset)) { //if (mProject.GetAnattrib(offset).Symbol != null) { // We consider auto labels when splitting regions for the data analysis, // but I don't think we want to take them into account here. The specific // example that threw me was loading a 16-bit value from an address table. // The code does "LDA table,X / STA / LDA table+1,X / STA", which puts auto // labels at the first two addresses -- splitting the region. That's good // for the uncategorized data analyzer, but very annoying if you want to // slap a 16-bit numeric format on all entries in a table. groupNum++; } else if (mProject.HasCommentOrNote(offset)) { // Don't carry across a long comment or note. groupNum++; } else if (attr.Address != expectedAddr) { // For a contiguous selection, this should only happen if there's a .ORG // address change. For non-contiguous selection this is expected. In the // latter case, incrementing the group number is unnecessary but harmless. Debug.WriteLine("Address break: " + attr.Address + " vs. " + expectedAddr); //Debug.Assert(mProject.AddrMap.Get(offset) >= 0); expectedAddr = attr.Address; groupNum++; } // Mark every byte of an instruction or multi-byte data item -- // everything that is represented by the line the user selected. Control // statements and blank lines aren't relevant here, as we only care about // file offsets. int len = CodeLineList[index].OffsetSpan; // attr.Length; Debug.Assert(len > 0); for (int i = offset; i < offset + len; i++) { rs.Add(i, groupNum); } // Advance the address. expectedAddr += len; prevOffset = offset; } Debug.WriteLine("Offset selection conv took " + (DateTime.Now - startWhen).TotalMilliseconds + " ms"); return rs; } #endregion Main window UI event handlers #region References panel /// /// Updates the "references" panel to reflect the current selection. /// /// The number of references to any given address should be relatively small, and /// won't change without a data refresh, so recreating the list every time shouldn't /// be a problem. /// private void UpdateReferencesPanel() { mMainWin.ReferencesList.Clear(); if (mMainWin.CodeListView_GetSelectionCount() != 1) { // Nothing selected, or multiple lines selected. return; } int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); LineListGen.Line.Type type = CodeLineList[lineIndex].LineType; if (type != LineListGen.Line.Type.Code && type != LineListGen.Line.Type.Data && type != LineListGen.Line.Type.EquDirective) { // Code, data, and platform symbol EQUs have xrefs. return; } // Find the appropriate xref set. int offset = CodeLineList[lineIndex].FileOffset; XrefSet xrefs; if (offset < 0) { int index = LineListGen.DefSymIndexFromOffset(offset); DefSymbol defSym = mProject.ActiveDefSymbolList[index]; xrefs = defSym.Xrefs; } else { xrefs = mProject.GetXrefSet(offset); } if (xrefs == null || xrefs.Count == 0) { return; } // TODO(someday): localization Asm65.Formatter formatter = mOutputFormatter; bool showBank = !mProject.CpuDef.HasAddr16; for (int i = 0; i < xrefs.Count; i++) { XrefSet.Xref xr = xrefs[i]; string typeStr; switch (xr.Type) { case XrefSet.XrefType.SubCallOp: typeStr = "call "; break; case XrefSet.XrefType.BranchOp: typeStr = "branch "; break; case XrefSet.XrefType.RefFromData: typeStr = "data "; break; case XrefSet.XrefType.MemAccessOp: switch (xr.AccType) { case OpDef.MemoryEffect.Read: typeStr = "read "; break; case OpDef.MemoryEffect.Write: typeStr = "write "; break; case OpDef.MemoryEffect.ReadModifyWrite: typeStr = "rmw "; break; case OpDef.MemoryEffect.None: // e.g. LDA # kvp in mProject.Notes) { int offset = kvp.Key; MultiLineComment mlc = kvp.Value; // Replace line break with bullet. If there's a single CRLF at the end, strip it. string nocrlfStr; if (mlc.Text.EndsWith("\r\n")) { nocrlfStr = mlc.Text.Substring(0, mlc.Text.Length - 2).Replace("\r\n", " \u2022 "); } else { nocrlfStr = mlc.Text.Replace("\r\n", " \u2022 "); } MainWindow.NotesListItem nli = new MainWindow.NotesListItem(offset, mOutputFormatter.FormatOffset24(offset), nocrlfStr, mlc.BackgroundColor); mMainWin.NotesList.Add(nli); } } #endregion Notes panel #region Symbols panel private void PopulateSymbolsList() { mMainWin.SymbolsList.Clear(); foreach (Symbol sym in mProject.SymbolTable) { MainWindow.SymbolsListItem sli = new MainWindow.SymbolsListItem(sym, sym.SourceTypeString, mOutputFormatter.FormatHexValue(sym.Value, 0), sym.Label); mMainWin.SymbolsList.Add(sli); } } #endregion Symbols panel #region Info panel private void UpdateInfoPanel() { if (mMainWin.CodeListView_GetSelectionCount() != 1) { // Nothing selected, or multiple lines selected. mMainWin.InfoPanelContents = string.Empty; return; } int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex(); LineListGen.Line line = CodeLineList[lineIndex]; StringBuilder sb = new StringBuilder(250); // TODO(someday): this should be made easier to localize string lineTypeStr; string extraStr = string.Empty; switch (line.LineType) { case LineListGen.Line.Type.Code: lineTypeStr = "code"; break; case LineListGen.Line.Type.Data: if (mProject.GetAnattrib(line.FileOffset).IsInlineData) { lineTypeStr = "inline data"; } else { lineTypeStr = "data"; } break; case LineListGen.Line.Type.LongComment: lineTypeStr = "comment"; break; case LineListGen.Line.Type.Note: lineTypeStr = "note"; break; case LineListGen.Line.Type.Blank: lineTypeStr = "blank line"; //lineTypeStr = "blank line (+" + // mOutputFormatter.FormatOffset24(line.FileOffset) + ")"; break; case LineListGen.Line.Type.OrgDirective: lineTypeStr = "address directive"; break; case LineListGen.Line.Type.RegWidthDirective: lineTypeStr = "register width directive"; break; case LineListGen.Line.Type.EquDirective: { lineTypeStr = "equate"; int symIndex = LineListGen.DefSymIndexFromOffset(line.FileOffset); DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex]; string sourceStr; if (defSym.SymbolSource == Symbol.Source.Project) { sourceStr = "project symbol definition"; } else if (defSym.SymbolSource == Symbol.Source.Platform) { sourceStr = "platform symbol file"; } else { sourceStr = "???"; } extraStr = "Source: " + sourceStr; } break; default: lineTypeStr = "???"; break; } // For anything that isn't code or data, show something simple and bail. if (line.OffsetSpan == 0) { sb.AppendFormat(Res.Strings.INFO_LINE_SUM_NON_FMT, lineIndex, lineTypeStr); #if DEBUG sb.Append(" [offset=+" + line.FileOffset.ToString("x6") + "]"); #endif if (!string.IsNullOrEmpty(extraStr)) { sb.Append("\r\n\r\n"); sb.Append(extraStr); } mMainWin.InfoPanelContents = sb.ToString(); return; } Debug.Assert(line.IsCodeOrData); Anattrib attr = mProject.GetAnattrib(line.FileOffset); // Show number of bytes of code/data. if (line.OffsetSpan == 1) { sb.AppendFormat(Res.Strings.INFO_LINE_SUM_SINGULAR_FMT, lineIndex, line.OffsetSpan, lineTypeStr); } else { sb.AppendFormat(Res.Strings.INFO_LINE_SUM_PLURAL_FMT, lineIndex, line.OffsetSpan, lineTypeStr); } sb.Append("\r\n"); if (!mProject.OperandFormats.TryGetValue(line.FileOffset, out FormatDescriptor dfd)) { // No user-specified format, but there may be a generated format. sb.AppendFormat(Res.Strings.INFO_FD_SUM_FMT, Res.Strings.DEFAULT_VALUE); if (attr.DataDescriptor != null) { sb.Append(" ["); sb.Append(attr.DataDescriptor.ToUiString()); sb.Append("]"); } } else { // User-specified operand format. // If the descriptor has a weak reference to an unknown symbol, should we // call that out here? sb.AppendFormat(Res.Strings.INFO_FD_SUM_FMT, dfd.ToUiString()); } sb.Append("\r\n"); // Debug only //sb.Append("DEBUG: opAddr=" + attr.OperandAddress.ToString("x4") + // " opOff=" + attr.OperandOffset.ToString("x4") + "\r\n"); sb.Append("\u2022Attributes:"); if (attr.IsHinted) { sb.Append(" Hinted("); for (int i = 0; i < line.OffsetSpan; i++) { switch (mProject.TypeHints[line.FileOffset + i]) { case CodeAnalysis.TypeHint.Code: sb.Append("C"); break; case CodeAnalysis.TypeHint.Data: sb.Append("D"); break; case CodeAnalysis.TypeHint.InlineData: sb.Append("I"); break; default: break; } if (i > 8) { sb.Append("..."); break; } } sb.Append(')'); } if (attr.IsEntryPoint) { sb.Append(" EntryPoint"); } if (attr.IsBranchTarget) { sb.Append(" BranchTarget"); } if (attr.DoesNotContinue) { sb.Append(" NoContinue"); } if (attr.DoesNotBranch) { sb.Append(" NoBranch"); } if (mProject.StatusFlagOverrides[line.FileOffset].AsInt != 0) { sb.Append(" StatusFlags"); } sb.Append("\r\n\r\n"); if (attr.IsInstruction) { Asm65.OpDef op = mProject.CpuDef.GetOpDef(mProject.FileData[line.FileOffset]); string shortDesc = mOpDesc.GetShortDescription(op.Mnemonic); if (!string.IsNullOrEmpty(shortDesc)) { if (op.IsUndocumented) { sb.Append("\u25b6[*] "); } else { sb.Append("\u25b6 "); } sb.Append(shortDesc); string addrStr = mOpDesc.GetAddressModeDescription(op.AddrMode); if (!string.IsNullOrEmpty(addrStr)) { sb.Append(", "); sb.Append(addrStr); } sb.Append("\r\n"); } sb.Append("\u2022Cycles: "); int cycles = op.Cycles; Asm65.OpDef.CycleMod cycMods = op.CycleMods; sb.Append(cycles.ToString()); if (cycMods != 0) { sb.Append(" ("); int workBits = (int)cycMods; while (workBits != 0) { // Isolate rightmost bit. int firstBit = (~workBits + 1) & workBits; sb.Append(mOpDesc.GetCycleModDescription((OpDef.CycleMod)firstBit)); // Remove from set. workBits &= ~firstBit; if (workBits != 0) { // more to come sb.Append(", "); } } sb.Append(")"); } sb.Append("\r\n"); const string FLAGS = "NVMXDIZC"; sb.Append("\u2022Flags affected: "); Asm65.StatusFlags affectedFlags = op.FlagsAffected; for (int i = 0; i < 8; i++) { if (affectedFlags.GetBit((StatusFlags.FlagBits)(7 - i)) >= 0) { sb.Append(' '); sb.Append(FLAGS[i]); } else { sb.Append(" -"); } } sb.Append("\r\n"); string longDesc = mOpDesc.GetLongDescription(op.Mnemonic); if (!string.IsNullOrEmpty(longDesc)) { sb.Append("\r\n"); sb.Append(longDesc); sb.Append("\r\n"); } } else { // do we want descriptions of the pseudo-ops? } // Publish mMainWin.InfoPanelContents = sb.ToString(); } #endregion Info panel #region Debug features public void RunSourceGenerationTests() { Tests.WpfGui.GenTestRunner dlg = new Tests.WpfGui.GenTestRunner(mMainWin); dlg.ShowDialog(); } #endregion } }