/* * Copyright 2018 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.IO; using System.Text; using System.Web.Script.Serialization; using System.Windows.Forms; using Asm65; using CommonUtil; using CommonWinForms; using SourceGen.Sandbox; namespace SourceGen.AppForms { /// /// Main application form. This is the top-level application object. /// public partial class ProjectView : Form { private const string LOGO_FILE_NAME = "Logo.png"; 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; /// /// Symbol subset, used to supply data to the symbol ListView. Initialized with /// an empty symbol table. /// private SymbolTableSubset mSymbolSubset; /// /// Current code list view selection. The length will match the DisplayList Count. /// /// A simple foreach through codeListView.SelectedIndices on a 500K-line data set /// takes about 2.5 seconds on a fast Win10 x64 machine. Fortunately the control /// notifies us of changes to the selection, so we can track it ourselves. /// private VirtualListViewSelection mCodeViewSelection = new VirtualListViewSelection(); /// /// Data backing the codeListView. /// private DisplayList mDisplayList; #endregion Project state /// /// Returns the font currently in use by the code ListView. /// public Font CodeListViewFont { get { return codeListView.Font; } } /// /// List of recently-opened projects. /// private List mRecentProjectPaths = new List(MAX_RECENT_PROJECTS); private const int MAX_RECENT_PROJECTS = 6; /// /// Menu items for the Actions and codeListView context menus. The menu items can /// only be in one place at a time, so we move them around when the menu is opened. /// private ToolStripItem[] mActionsMenuItems; /// /// Activity log generated by the code and data analyzers. Displayed in window. /// private DebugLog mGenerationLog; /// /// Timing data generated during analysis. /// TaskTimer mReanalysisTimer = new TaskTimer(); /// /// Base control to show when no project is open. /// private Control mNoProjectControl; /// /// Base control to show when project is open. /// private Control mProjectControl; /// /// Performance hack. /// private bool mRestoringSelection; /// /// 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; /// /// Set to true if the last key hit was Ctrl+H. /// private bool mCtrlHSeen; /// /// 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; /// /// Project hex dump viewer. This is just for viewing the project contents, so this /// dialog is tied to the project. /// /// The general-purpose file dump windows are independent (currently untracked). /// private Tools.HexDumpViewer mHexDumpDialog; /// /// Floating ASCII chart dialog. Not tied to the project. /// private Tools.AsciiChart mAsciiChartDialog; #region Init and settings public ProjectView() { InitializeComponent(); ScriptManager.UseKeepAliveHack = true; #if DEBUG debugModeEnabledLabel.Visible = true; #endif } private void ProjectView_Load(object sender, EventArgs e) { if (RuntimeDataAccess.GetDirectory() == null) { MessageBox.Show(this, Properties.Resources.RUNTIME_DIR_NOT_FOUND, Properties.Resources.RUNTIME_DIR_NOT_FOUND_CAPTION, MessageBoxButtons.OK, MessageBoxIcon.Error); Application.Exit(); return; } 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; } 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]; } // 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(); ApplyAppSettings(); UpdateActionMenu(); UpdateMenuItemsAndTitle(); UpdateRecentLinks(); ShowNoProject(); ProcessCommandLine(); } private void ProcessCommandLine() { string[] args = Environment.GetCommandLineArgs(); if (args.Length == 2) { DoOpenFile(Path.GetFullPath(args[1])); } } private void InitCodeListView() { ListView cv = codeListView; cv.View = View.Details; // Create an empty place-holder for the context menu. codeListView.ContextMenuStrip = new ContextMenuStrip(); // When the Actions or context menu are opened, all menu items get transferred over. codeListView.ContextMenuStrip.Opening += codeListView_CmsOpening; // When the context menu closes, move all the menu items back to the Actions menu, // so that you can open it with Alt-A. codeListView.ContextMenuStrip.Closed += ActionsMenuOpening; // Set default widths, in case we don't have a value for this in AppSettings. CodeListColumnWidths widths = GetDefaultCodeListColumnWidths(); SetCodeListHeaderWidths(widths); // This gets invoked when the user starts typing characters while the ListView // has focus. Useful in a file viewer to find something by the first few // characters. Less useful for us, though we could potentially use it as a // shortcut to jump to an offset or label. //codeListView.SearchForVirtualItem += // new SearchForVirtualItemEventHandler(codeListView_SearchForVirtualItem); // Can't simply set this to "true", because the member is protected. codeListView.SetDoubleBuffered(true); } /// /// 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 // retrieved 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)SymbolTableSubset.SortCol.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 // Load the settings file, and merge it into the globals. string settingsDir = Path.GetDirectoryName(RuntimeDataAccess.GetDirectory()); 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; } // Collect some window widths. Don't grab the main window size if we're // maximized or minimized. if (this.WindowState == FormWindowState.Normal || this.WindowState == FormWindowState.Maximized) { if (this.WindowState == FormWindowState.Normal) { AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_LOC_X, this.Location.X); AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_LOC_Y, this.Location.Y); AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_WIDTH, this.Size.Width); AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_HEIGHT, this.Size.Height); } AppSettings.Global.SetBool(AppSettings.MAIN_WINDOW_MAXIMIZED, this.WindowState == FormWindowState.Maximized); // Horizontal splitters. We want to record the panel widths, rather than the // splitter distance, because otherwise the right panel doesn't keep its size // when the middle section changes during maximization. AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH, mainSplitterLeft.SplitterDistance); AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH, this.Size.Width - (mainSplitterLeft.SplitterDistance + mainSplitterRight.SplitterDistance)); // Vertical splitters. AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_SIDE_SPLITTER_DIST, leftPanelSplitter.SplitterDistance); AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_SIDE_SPLITTER_DIST, rightPanelSplitter.SplitterDistance); } else { // VisualStudio appears to un-minimize its window before closing it. We // could probably do that and grab the size values on the way out. AppSettings.Global.SetBool(AppSettings.MAIN_WINDOW_MAXIMIZED, false); } SerializeReferencesColumnWidths(); SerializeNotesColumnWidths(); SerializeSymbolColumnWidths(); string settingsDir = Path.GetDirectoryName(RuntimeDataAccess.GetDirectory()); 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 ProjectView. /// /// 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. /// private void SetAppWindowLocation() { const int DEFAULT_WIDTH = 1280; const int DEFAULT_HEIGHT = 720; const int DEFAULT_SPLIT = 250; AppSettings settings = AppSettings.Global; // Main window size. this.Size = new Size( settings.GetInt(AppSettings.MAIN_WINDOW_WIDTH, DEFAULT_WIDTH), settings.GetInt(AppSettings.MAIN_WINDOW_HEIGHT, DEFAULT_HEIGHT)); // Left splitter width is distance from left edge of window. mainSplitterLeft.SplitterDistance = settings.GetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH, DEFAULT_SPLIT); // Right splitter distance is distance from right edge of left splitter, i.e. // the width of the middle section, *not* the width of the right panel. // --> splitter_distance = main_width - (left_width + right_width) int rightSplitWidth = settings.GetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH, DEFAULT_SPLIT); mainSplitterRight.SplitterDistance = this.Size.Width - (mainSplitterLeft.SplitterDistance + rightSplitWidth); // Vertical splits, e.g. References vs. Notes. leftPanelSplitter.SplitterDistance = settings.GetInt(AppSettings.MAIN_LEFT_SIDE_SPLITTER_DIST, 350); rightPanelSplitter.SplitterDistance = settings.GetInt(AppSettings.MAIN_RIGHT_SIDE_SPLITTER_DIST, 400); // Get working area of screen the system is going to display us on. Note that, // with multiple displays, the X/Y coordinates for the display's upper left may // be negative. Screen myScreen = Screen.FromControl(this); Rectangle scrArea = myScreen.WorkingArea; // Upper-left corner of our window that positions us in the center. Point centered = new Point(scrArea.X + (scrArea.Width - this.Size.Width) / 2, scrArea.Y + (scrArea.Height - this.Size.Height) / 2); // Get requested location (if any). Point loc = new Point(); loc.X = settings.GetInt(AppSettings.MAIN_WINDOW_LOC_X, int.MinValue); loc.Y = settings.GetInt(AppSettings.MAIN_WINDOW_LOC_Y, int.MinValue); if (loc.X == int.MinValue || loc.Y == int.MinValue) { // No setting exists; center it on the screen. loc = centered; } else { // See if this location makes sense. As a quick sanity check we test to see // if the top-center part of the window is visible. If not, it may not be // possible to reposition the window because the title bar won't be visible. // // Win10 seems to be okay with dragging a window below the icon bar at the // bottom, which makes it hard to get at. We offset the Y position by a few // pixels to give us some wiggle room. Point checkPoint = new Point(loc.X + this.Size.Width / 2, loc.Y + 8); if (!scrArea.Contains(checkPoint)) { Debug.WriteLine("Titlebar " + checkPoint + " not inside " + scrArea); loc = centered; } } this.Location = loc; if (settings.GetBool(AppSettings.MAIN_WINDOW_MAXIMIZED, false)) { this.WindowState = FormWindowState.Maximized; } // Configure column widths. string widthStr = settings.GetString(AppSettings.CDLV_COL_WIDTHS, null); if (!string.IsNullOrEmpty(widthStr)) { CodeListColumnWidths widths = CodeListColumnWidths.Deserialize(widthStr); if (widths != null) { SetCodeListHeaderWidths(widths); } } DeserializeReferencesColumnWidths(); DeserializeNotesColumnWidths(); DeserializeSymbolColumnWidths(); } /// /// 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); } } // 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(); } // Finally, update the display list with all the fancy settings. if (mDisplayList != 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); } } // Make sure we pick up changes to the window size or position. We don't catch // size-chage events for the left/right splitter widths because that should be picked // up by events on sub-windows. private void ProjectView_SizeOrLocChanged(object sender, EventArgs e) { AppSettings.Global.Dirty = true; } // This handles the splitters in the side panels, e.g. between References and Notes. private void SidePanelSplitter_SplitterMoved(object sender, SplitterEventArgs e) { AppSettings.Global.Dirty = true; } public void ShowProject() { mProjectControl.Show(); mNoProjectControl.Hide(); codeListView.Focus(); saveToolStripMenuItem.Enabled = true; saveToolStripButton.Enabled = true; saveAsToolStripMenuItem.Enabled = true; closeToolStripMenuItem.Enabled = true; assembleToolStripMenuItem.Enabled = true; printToolStripMenuItem.Enabled = true; copyToolStripMenuItem.Enabled = true; copyToolStripButton.Enabled = true; selectAllToolStripMenuItem.Enabled = true; findToolStripMenuItem.Enabled = true; findNextToolStripMenuItem.Enabled = true; gotoToolStripMenuItem.Enabled = true; editHeaderCommentToolStripMenuItem.Enabled = true; projectPropertiesToolStripMenuItem.Enabled = true; toggleDataScanToolStripMenuItem.Enabled = true; showUndoRedoHistoryToolStripMenuItem.Enabled = true; showAnalysisTimersToolStripMenuItem.Enabled = true; showAnalyzerOutputToolStripMenuItem.Enabled = true; toggleOwnerDrawToolStripMenuItem.Enabled = true; reanalyzeToolStripMenuItem.Enabled = true; toggleCommentRulersToolStripMenuItem.Enabled = true; extensionScriptInfoToolStripMenuItem.Enabled = true; mNavStack.Clear(); UpdateMenuItemsAndTitle(); } public void ShowNoProject() { mProjectControl.Hide(); mNoProjectControl.Show(); saveToolStripMenuItem.Enabled = false; saveToolStripButton.Enabled = false; saveAsToolStripMenuItem.Enabled = false; closeToolStripMenuItem.Enabled = false; assembleToolStripMenuItem.Enabled = false; printToolStripMenuItem.Enabled = false; copyToolStripMenuItem.Enabled = false; copyToolStripButton.Enabled = false; selectAllToolStripMenuItem.Enabled = false; findToolStripMenuItem.Enabled = false; findNextToolStripMenuItem.Enabled = false; gotoToolStripMenuItem.Enabled = false; editHeaderCommentToolStripMenuItem.Enabled = false; projectPropertiesToolStripMenuItem.Enabled = false; toggleDataScanToolStripMenuItem.Enabled = false; showUndoRedoHistoryToolStripMenuItem.Enabled = false; showAnalysisTimersToolStripMenuItem.Enabled = false; showAnalyzerOutputToolStripMenuItem.Enabled = false; toggleOwnerDrawToolStripMenuItem.Enabled = false; reanalyzeToolStripMenuItem.Enabled = false; toggleCommentRulersToolStripMenuItem.Enabled = false; extensionScriptInfoToolStripMenuItem.Enabled = false; UpdateMenuItemsAndTitle(); } private void UnpackRecentProjectList() { mRecentProjectPaths.Clear(); string cereal = AppSettings.Global.GetString( AppSettings.PRVW_RECENT_PROJECT_LIST, null); if (string.IsNullOrEmpty(cereal)) { return; } try { JavaScriptSerializer ser = new JavaScriptSerializer(); mRecentProjectPaths = 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 = 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); UpdateRecentLinks(); } /// /// Updates the links on the no-project control. /// private void UpdateRecentLinks() { if (mRecentProjectPaths.Count >= 1) { recentProjectLabel1.Visible = true; recentProjectLabel1.Text = string.Format(Properties.Resources.RECENT_PROJECT_LINK, 1, Path.GetFileName(mRecentProjectPaths[0])); } else { recentProjectLabel1.Visible = false; } if (mRecentProjectPaths.Count >= 2) { recentProjectLabel2.Visible = true; recentProjectLabel2.Text = string.Format(Properties.Resources.RECENT_PROJECT_LINK, 2, Path.GetFileName(mRecentProjectPaths[1])); } else { recentProjectLabel2.Visible = false; } } private void recentProjectsToolStripMenuItem_DropDownOpening(object sender, EventArgs e) { ToolStripItemCollection recents = recentProjectsToolStripMenuItem.DropDownItems; recents.Clear(); if (mRecentProjectPaths.Count == 0) { recents.Add(noRecentsToolStripMenuItem); } else { for (int i = 0; i < mRecentProjectPaths.Count; i++) { string pathName = mRecentProjectPaths[i]; string menuName = string.Format("{0}: {1}", i + 1, /*Path.GetFileName(*/ pathName /*)*/); recents.Add(new ToolStripMenuItem(menuName, null, (sendr, arg) => { if (DoClose()) { DoOpenFile(pathName); } })); } } } #endregion Init and settings #region Project management private bool PrepareNewProject(string dataPathName, Setup.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 = Properties.Resources.OPEN_DATA_FAIL_CAPTION; string caption = Properties.Resources.OPEN_DATA_FAIL_MESSAGE + ": " + ex.Message; MessageBox.Show(this, caption, message, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } proj.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins; proj.Initialize(fileData.Length); proj.PrepForNew(fileData, Path.GetFileName(dataPathName)); proj.LongComments.Add(DisplayList.Line.HEADER_COMMENT_OFFSET, new MultiLineComment("6502bench SourceGen v" + Program.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(); dlg.CanCancel = false; dlg.Messages = messages; dlg.ShowDialog(); dlg.Dispose(); } mDisplayList = new DisplayList(mProject, 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); // 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(Properties.Resources.OPEN_DATA_TOO_LARGE, fs.Length / 1024, DisasmProject.MAX_DATA_FILE_SIZE / 1024)); } else if (fs.Length == 0) { throw new InvalidDataException(Properties.Resources.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(Properties.Resources.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); UpdateMenuItemsAndTitle(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } } /// /// 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 topItem = codeListView.TopItem.Index; int topOffset = mDisplayList[topItem].FileOffset; DisplayList.SavedSelection savedSel = DisplayList.SavedSelection.Generate( mDisplayList, mCodeViewSelection, 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); VirtualListViewSelection newSel = savedSel.Restore(mDisplayList, out int topIndex); //newSel.DebugDump(); // Refresh the various windows, and restore the selection. mReanalysisTimer.StartTask("Invalidate controls"); InvalidateControls(newSel); 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); codeListView.TopItem = codeListView.Items[topIndex]; } catch (NullReferenceException) { Debug.WriteLine("Caught an NRE from TopItem"); } mReanalysisTimer.EndTask("ProjectView.ApplyChanges()"); //mReanalysisTimer.DumpTimes("ProjectView timers:", mGenerationLog); if (mShowAnalysisTimersDialog != null) { string timerStr = mReanalysisTimer.DumpToString("ProjectView timers:"); mShowAnalysisTimersDialog.BodyText = timerStr; } } /// /// 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); mDisplayList.SetFormatter(mOutputFormatter); mDisplayList.SetPseudoOpNames(mPseudoOpNames); mOutputFormatterCpuDef = mProject.CpuDef; } 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 = Properties.Resources.STATUS_RECALCULATING; Refresh(); // redraw status label mReanalysisTimer.EndTask("Do Windows stuff"); DoRefreshProject(reanalysisRequired); } finally { Application.UseWaitCursor = false; toolStripStatusLabel.Text = prevStatus; } } else { DoRefreshProject(reanalysisRequired); } 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; mDisplayList.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 (mShowAnalyzerOutputDialog != null) { mShowAnalyzerOutputDialog.BodyText = mGenerationLog.WriteToString(); } } mReanalysisTimer.StartTask("Generate DisplayList"); mDisplayList.GenerateAll(); mReanalysisTimer.EndTask("Generate DisplayList"); UpdateSelectionHighlight(); } #endregion Project management #region Main window UI event handlers /// /// Invalidates and forces an update on our various windows. /// private void InvalidateControls(VirtualListViewSelection newSel) { codeListView.BeginUpdate(); ClearCodeListViewCache(); if (mDisplayList != null) { codeListView.VirtualListSize = mDisplayList.Count; } mReanalysisTimer.StartTask("Restore selection"); mRestoringSelection = true; if (newSel != null) { RestoreSelection(newSel); // want to do this between Begin/EndUpdate } else if (mDisplayList != null) { // No selection to restore. This should only happen for the initial // render, when nothing is yet selected. // // Set the length on the code view. mCodeViewSelection.SetLength(mDisplayList.Count); } else { // Just closed the project, nothing to do at all. } mRestoringSelection = false; mReanalysisTimer.EndTask("Restore selection"); UpdateActionMenu(); codeListView.EndUpdate(); InvalidateSymbolListView(); InvalidateNotesListView(); UpdateReferenceView(); UpdateInfoView(); } /// /// Updates menu item enable status; necessary because that determines whether the /// associated keyboard shortcuts are active. /// /// Updates the main form title to show project name and modification status. /// /// This does not handle items that only toggle enabledness when a project is opened /// or closed. /// private void UpdateMenuItemsAndTitle() { undoToolStripMenuItem.Enabled = (mProject != null && mProject.CanUndo); redoToolStripMenuItem.Enabled = (mProject != null && mProject.CanRedo); navigateBackToolStripButton.Enabled = (mProject != null && mNavStack.HasBackward); navigateFwdToolStripButton.Enabled = (mProject != null && mNavStack.HasForward); // Update main window title. StringBuilder sb = new StringBuilder(); sb.Append(Properties.Resources.TITLE_BASE); if (mProject != null) { sb.Append(" - "); if (string.IsNullOrEmpty(mProjectPathName)) { sb.Append(Properties.Resources.TITLE_NEW_PROJECT); } else { sb.Append(Path.GetFileName(mProjectPathName)); } if (mProject.IsDirty) { sb.Append(" "); sb.Append(Properties.Resources.TITLE_MODIFIED); } } Text = sb.ToString(); } /// /// 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(VirtualListViewSelection 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. VirtualListViewSelection 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"); } // This gets the key events for all controls associated with the main form, // regardless of which has focus, making it useful for keyboard shortcuts. // Return true to indicate that we've handled the key. protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { // Handle the Ctrl+H sequence. if (mCtrlHSeen) { if (keyData == (Keys.Control | Keys.C)) { if (hintAsCodeToolStripMenuItem.Enabled) { MarkAsCode_Click(null, null); } } else if (keyData == (Keys.Control | Keys.D)) { if (hintAsDataToolStripMenuItem.Enabled) { MarkAsData_Click(null, null); } } else if (keyData == (Keys.Control | Keys.I)) { if (hintAsInlineDataToolStripMenuItem.Enabled) { MarkAsInlineData_Click(null, null); } } else if (keyData == (Keys.Control | Keys.R)) { if (removeHintToolStripMenuItem.Enabled) { MarkAsNoHint_Click(null, null); } } else { System.Media.SystemSounds.Beep.Play(); } mCtrlHSeen = false; toolStripStatusLabel.Text = Properties.Resources.STATUS_READY; return true; } else if (keyData == (Keys.Control | Keys.H)) { mCtrlHSeen = true; toolStripStatusLabel.Text = Properties.Resources.STATUS_CTRL_H_HIT; return true; } // Ctrl+Shift+Z is an alias for Redo (Ctrl+Y). if (keyData == (Keys.Control | Keys.Shift | Keys.Z)) { if (redoToolStripMenuItem.Enabled) { redoToolStripMenuItem_Click(null, null); } return true; } // Navigation keys. Alt+left/right is intuitive key binding used by Eclipse, // Ctrl+[shift]+minus is weird Visual Studio binding. if (keyData == (Keys.Alt | Keys.Left) || keyData == (Keys.Control | Keys.OemMinus)) { if (navigateBackToolStripButton.Enabled) { navigateBackToolStripButton_Click(null, null); } return true; } if (keyData == (Keys.Alt | Keys.Right) || keyData == (Keys.Control | Keys.Shift | Keys.OemMinus)) { if (navigateFwdToolStripButton.Enabled) { navigateFwdToolStripButton_Click(null, null); } return true; } return base.ProcessCmdKey(ref msg, keyData); } private void navigateBackToolStripButton_Click(object sender, EventArgs e) { if (!mNavStack.HasBackward) { // toolbar button should have been disabled return; } int backOff = mNavStack.Pop(); UpdateMenuItemsAndTitle(); Debug.WriteLine("Nav back: +" + backOff.ToString("x6")); GoToOffset(backOff, false, false); } private void navigateFwdToolStripButton_Click(object sender, EventArgs e) { if (!mNavStack.HasForward) { // toolbar button should have been disabled return; } int fwdOff = mNavStack.PushPrevious(); UpdateMenuItemsAndTitle(); Debug.WriteLine("Nav fwd: +" + fwdOff.ToString("x6")); GoToOffset(fwdOff, false, false); } /// /// Determines whether any part of the specified offset is currently visible in the /// code list view. /// private bool IsOffsetVisible(int offset) { int firstLineIndex = mDisplayList.FindLineIndexByOffset(offset); int lastLineIndex = firstLineIndex + 1; while (lastLineIndex < mDisplayList.Count && mDisplayList[lastLineIndex].FileOffset == offset) { lastLineIndex++; } lastLineIndex--; //Debug.WriteLine("Check vis: first=" + firstLineIndex + " last=" + lastLineIndex); return codeListView.IsItemVisible(codeListView.Items[firstLineIndex]) || codeListView.IsItemVisible(codeListView.Items[lastLineIndex]); } // File > New (Ctrl+N) private void newToolStripMenuItem_Click(object sender, EventArgs e) { DoNew(); } private void newToolStripButton_Click(object sender, EventArgs e) { newToolStripMenuItem_Click(sender, e); } private void newFileLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { DoNew(); } private void DoNew() { if (!DoClose()) { return; } Setup.NewProject dlg = new Setup.NewProject(); dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { bool ok = PrepareNewProject(Path.GetFullPath(dlg.DataFileName), dlg.SystemDef); if (ok) { FinishPrep(); } } dlg.Dispose(); } // File > Open (Ctrl+O) private void openToolStripMenuItem_Click(object sender, EventArgs e) { DoOpen(); } private void openToolStripButton_Click(object sender, EventArgs e) { openToolStripMenuItem_Click(sender, e); } private void recentProjectLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { Debug.Assert(mRecentProjectPaths.Count > 0); if (DoClose()) { DoOpenFile(mRecentProjectPaths[0]); } } private void recentProjectLabel2_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { Debug.Assert(mRecentProjectPaths.Count > 1); if (DoClose()) { DoOpenFile(mRecentProjectPaths[1]); } } private void openExistingLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { DoOpen(); } /// /// Handles opening an existing project by letting the user select the project file. /// private void DoOpen() { if (!DoClose()) { return; } OpenFileDialog fileDlg = new OpenFileDialog(); fileDlg.Filter = ProjectFile.FILENAME_FILTER + "|" + Properties.Resources.FILE_FILTER_ALL; fileDlg.FilterIndex = 1; if (fileDlg.ShowDialog() != DialogResult.OK) { 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(Properties.Resources.ERR_FILE_NOT_FOUND, projPathName); MessageBox.Show(this, msg, Properties.Resources.ERR_FILE_GENERIC_CAPTION, MessageBoxButtons.OK, MessageBoxIcon.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(); dlg.Messages = report.Format(); dlg.CanContinue = false; dlg.ShowDialog(); // ignore dlg.DialogResult dlg.Dispose(); 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(); dlg.Messages = report.Format(); dlg.ShowDialog(); DialogResult result = dlg.DialogResult; dlg.Dispose(); if (result != DialogResult.OK) { 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, string.Format(Properties.Resources.OPEN_DATA_DOESNT_EXIST, dataPathName)); 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(Properties.Resources.OPEN_DATA_WRONG_LENGTH, 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(Properties.Resources.OPEN_DATA_LOAD_FAILED, 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(Properties.Resources.OPEN_DATA_WRONG_CRC, (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(); dlg.PathName = origPath; dlg.Message = errorMsg; dlg.ShowDialog(); DialogResult result = dlg.DialogResult; dlg.Dispose(); if (result != DialogResult.OK) { return null; } OpenFileDialog fileDlg = new OpenFileDialog(); fileDlg.FileName = Path.GetFileName(origPath); fileDlg.Filter = Properties.Resources.FILE_FILTER_ALL; if (fileDlg.ShowDialog() != DialogResult.OK) { return null; } string newPath = Path.GetFullPath(fileDlg.FileName); Debug.WriteLine("User selected data file " + newPath); return newPath; } // File > Save (Ctrl+S) private void saveToolStripMenuItem_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(mProjectPathName)) { saveAsToolStripMenuItem_Click(sender, e); return; } DoSave(mProjectPathName); } private void saveToolStripButton_Click(object sender, EventArgs e) { saveToolStripMenuItem_Click(sender, e); } // File > Save As... private void saveAsToolStripMenuItem_Click(object sender, EventArgs e) { SaveFileDialog fileDlg = new SaveFileDialog(); fileDlg.Filter = ProjectFile.FILENAME_FILTER + "|" + Properties.Resources.FILE_FILTER_ALL; fileDlg.FilterIndex = 1; fileDlg.ValidateNames = true; fileDlg.AddExtension = true; fileDlg.FileName = Path.GetFileName(mDataPathName) + ProjectFile.FILENAME_EXT; if (fileDlg.ShowDialog() == DialogResult.OK) { 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 UpdateMenuItemsAndTitle(); } } } private bool DoSave(string pathName) { Debug.WriteLine("SAVING " + pathName); if (!ProjectFile.SerializeToFile(mProject, pathName, out string errorMessage)) { MessageBox.Show(this, Properties.Resources.ERR_PROJECT_SAVE_FAIL + ": " + errorMessage, Properties.Resources.OPERATION_FAILED, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } mProject.ResetDirtyFlag(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } UpdateMenuItemsAndTitle(); // Update this, in case this was a new project. UpdateRecentProjectList(pathName); // Seems like a good time to save this off too. SaveAppSettings(); return true; } // App is closing. private void ProjectView_FormClosing(object sender, FormClosingEventArgs e) { Debug.WriteLine("Main app form closing (reason=" + e.CloseReason + ")"); if (mProjectControl == null) { // This can be null if something failed during startup, so we're exiting // the application before the UI is fully up. return; } if (!DoClose()) { e.Cancel = true; return; } SaveAppSettings(); } // File > Close private void closeToolStripMenuItem_Click(object sender, EventArgs e) { if (!DoClose()) { Debug.WriteLine("Close canceled"); } } /// /// 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. private bool DoClose() { Debug.WriteLine("ProjectView.DoClose() - dirty=" + (mProject == null ? "N/A" : mProject.IsDirty.ToString())); if (mProject != null && mProject.IsDirty) { DialogResult result = MessageBox.Show(this, Properties.Resources.UNSAVED_CHANGES, Properties.Resources.UNSAVED_CHANGES_CAPTION, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); if (result == DialogResult.Cancel) { return 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(); } // Discard all project state. if (mProject != null) { mProject.Cleanup(); mProject = null; } mDataPathName = null; mProjectPathName = null; mSymbolSubset = new SymbolTableSubset(new SymbolTable()); mCodeViewSelection = new VirtualListViewSelection(); mDisplayList = null; codeListView.VirtualListSize = 0; codeListView.Items.Clear(); ShowNoProject(); InvalidateControls(null); mGenerationLog = null; // Not necessary, but it lets us check the memory monitor to see if we got // rid of everything. GC.Collect(); return true; } // File > Assemble... private void assembleToolStripMenuItem_Click(object sender, EventArgs e) { 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(this, Properties.Resources.SAVE_BEFORE_ASM_TEXT, Properties.Resources.SAVE_BEFORE_ASM_CAPTION, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } AsmGen.GenAndAsm dlg = new AsmGen.GenAndAsm(this, mProject, mProjectPathName); dlg.ShowDialog(); } // File > Exit private void exitToolStripMenuItem_Click(object sender, EventArgs e) { // Unsaved-data check happens in form closing event. Application.Exit(); } // Edit > Undo, Ctrl+Z (may be called with null/null) private void undoToolStripMenuItem_Click(object sender, EventArgs e) { if (!mProject.CanUndo) { Debug.WriteLine("Nothing to undo"); return; } ChangeSet cs = mProject.PopUndoSet(); ApplyChanges(cs, true); UpdateMenuItemsAndTitle(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } } // Edit > Redo, Ctrl+Y (may be called with null/null) private void redoToolStripMenuItem_Click(object sender, EventArgs e) { if (!mProject.CanRedo) { Debug.WriteLine("Nothing to redo"); return; } ChangeSet cs = mProject.PopRedoSet(); ApplyChanges(cs, false); UpdateMenuItemsAndTitle(); // If the debug dialog is visible, update it. if (mShowUndoRedoHistoryDialog != null) { mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory(); } } // Edit > Select All (Ctrl+A) private void selectAllToolStripMenuItem_Click(object sender, EventArgs e) { codeListView.SelectAll(); #if false try { Application.UseWaitCursor = true; Cursor.Current = Cursors.WaitCursor; codeListView.BeginUpdate(); int max = codeListView.VirtualListSize; for (int i = 0; i < max; i++) { //codeListView.Items[i].Selected = true; codeListView.SelectedIndices.Add(i); if ((i % 50000) == 0) { toolStripStatusLabel.Text = string.Format( Properties.Resources.STATUS_SELECTING, (i * 100) / max); //Application.DoEvents(); // <-- this is unwise Refresh(); // <-- updates status line but not mouse } } } finally { codeListView.EndUpdate(); Application.UseWaitCursor = false; toolStripStatusLabel.Text = Properties.Resources.STATUS_READY; } #endif } // Edit > Copy (Ctrl+C) private void copyToolStripMenuItem_Click(object sender, EventArgs e) { const int AssemblerSource = 0; const int Disassembly = 1; const bool addCsv = true; int format = AppSettings.Global.GetInt(AppSettings.CLIP_LINE_FORMAT, AssemblerSource); StringBuilder fullText = new StringBuilder(codeListView.SelectedIndices.Count * 50); StringBuilder csv = new StringBuilder(codeListView.SelectedIndices.Count * 40); StringBuilder sb = new StringBuilder(100); int addrAdj = mProject.CpuDef.HasAddr16 ? 6 : 9; int disAdj = (format != Disassembly) ? 0 : addrAdj + 10; // Walking through the selected indices can be slow for a large file, so we // run through the full list and pick out the selected items with our parallel // structure. (I'm assuming that "select all" will be a common precursor.) for (int i = 0; i < mDisplayList.Count; i++) { if (!mCodeViewSelection[i]) { continue; } DisplayList.Line line = mDisplayList[i]; DisplayList.FormattedParts parts = mDisplayList.GetFormattedParts(i); switch (line.LineType) { case DisplayList.Line.Type.Code: case DisplayList.Line.Type.Data: case DisplayList.Line.Type.EquDirective: case DisplayList.Line.Type.RegWidthDirective: case DisplayList.Line.Type.OrgDirective: if (format == Disassembly) { if (!string.IsNullOrEmpty(parts.Addr)) { sb.Append(parts.Addr); sb.Append(": "); } // shorten the "..." string bytesStr = parts.Bytes; if (bytesStr != null && bytesStr.Length > 8) { bytesStr = bytesStr.Substring(0, 8) + "+"; } TextUtil.AppendPaddedString(sb, bytesStr, disAdj); } TextUtil.AppendPaddedString(sb, parts.Label, disAdj + 9); TextUtil.AppendPaddedString(sb, parts.Opcode, disAdj + 9 + 8); TextUtil.AppendPaddedString(sb, parts.Operand, disAdj + 9 + 8 + 11); if (string.IsNullOrEmpty(parts.Comment)) { // Trim trailing spaces off opcode or operand. TextUtil.TrimEnd(sb); } else { sb.Append(parts.Comment); } sb.Append("\r\n"); break; case DisplayList.Line.Type.LongComment: if (format == Disassembly) { TextUtil.AppendPaddedString(sb, string.Empty, disAdj); } sb.Append(parts.Comment); sb.Append("\r\n"); break; case DisplayList.Line.Type.Note: // don't include notes break; case DisplayList.Line.Type.Blank: sb.Append("\r\n"); break; default: Debug.Assert(false); break; } fullText.Append(sb); if (addCsv) { csv.Append(TextUtil.EscapeCSV(parts.Offset)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Addr)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Bytes)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Flags)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Attr)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Label)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Opcode)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Operand)); csv.Append(','); csv.Append(TextUtil.EscapeCSV(parts.Comment)); csv.Append("\r\n"); } sb.Clear(); } // We want to have both plain text and CSV data on the clipboard. To add both // formats we need to stream it to a DataObject. Complicating matters is Excel's // entirely reasonable desire to have data in UTF-8 rather than UTF-16. // // (I'm not sure pasting assembly bits into Excel is actually useful, so this // should probably be optional.) // // https://stackoverflow.com/a/369219/294248 DataObject dataObject = new DataObject(); dataObject.SetText(fullText.ToString()); if (addCsv) { byte[] csvData = Encoding.UTF8.GetBytes(csv.ToString()); MemoryStream stream = new MemoryStream(csvData); dataObject.SetData(DataFormats.CommaSeparatedValue, stream); } Clipboard.SetDataObject(dataObject, true); } // Edit > Find... (Ctrl+F) private void findToolStripMenuItem_Click(object sender, EventArgs e) { FindBox dlg = new FindBox(); dlg.TextToFind = mFindString; if (dlg.ShowDialog() == DialogResult.OK) { mFindString = dlg.TextToFind; mFindStartIndex = -1; FindText(); } dlg.Dispose(); } // Edit > Find Next (F3) private void findNextToolStripMenuItem_Click(object sender, EventArgs e) { FindText(); } private void FindText() { if (string.IsNullOrEmpty(mFindString)) { return; } int index; if (codeListView.SelectedIndices.Count > 0) { index = codeListView.SelectedIndices[0]; } else { index = 0; } // Start one past the currently-selected item. index++; if (index == mDisplayList.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 = mDisplayList.GetSearchString(index); int matchPos = searchStr.IndexOf(mFindString, StringComparison.InvariantCultureIgnoreCase); if (matchPos >= 0) { //Debug.WriteLine("Match " + index + ": " + searchStr); codeListView.EnsureVisible(index); codeListView.DeselectAll(); codeListView.SelectedIndices.Add(index); return; } index++; if (index == mDisplayList.Count) { index = 0; } } // Announce that we've wrapped around, then clear the start index. MessageBox.Show(this, Properties.Resources.FIND_REACHED_START, Properties.Resources.FIND_REACHED_START_CAPTION, MessageBoxButtons.OK, MessageBoxIcon.Information); mFindStartIndex = -1; } // Edit > Go To... private void gotoToolStripMenuItem_Click(object sender, EventArgs e) { GotoBox dlg = new GotoBox(mProject, mOutputFormatter); if (dlg.ShowDialog() == DialogResult.OK) { GoToOffset(dlg.TargetOffset, false, true); } dlg.Dispose(); } // Edit > Project Properties... private void projectPropertiesToolStripMenuItem_Click(object sender, EventArgs e) { string projectDir = string.Empty; if (!string.IsNullOrEmpty(mProjectPathName)) { projectDir = Path.GetDirectoryName(mProjectPathName); } EditProjectProperties dlg = new EditProjectProperties(projectDir); dlg.SetInitialProps(mProject.ProjectProps); dlg.NumFormatter = mOutputFormatter; dlg.ShowDialog(); ProjectProperties newProps = dlg.NewProps; dlg.Dispose(); if (newProps != null) { UndoableChange uc = UndoableChange.CreateProjectPropertiesChange( mProject.ProjectProps, newProps); ApplyUndoableChanges(new ChangeSet(uc)); } } // Edit > Toggle Data Scan private void toggleDataScanToolStripMenuItem_Click(object sender, EventArgs e) { 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)); } // Edit > Settings... private void settingsToolStripMenuItem_Click(object sender, EventArgs e) { ShowAppSettings(EditAppSettings.Tab.Unknown); } /// /// Opens the app settings dialog. /// /// Tab to present to the user. public void ShowAppSettings(EditAppSettings.Tab initialTab) { EditAppSettings dlg = new EditAppSettings(this, initialTab); dlg.ShowDialog(); dlg.Dispose(); } // Help > View Help... private void viewHelpToolStripMenuItem_Click(object sender, EventArgs e) { HelpAccess.ShowHelp(HelpAccess.Topic.Contents); } private void helpToolStripButton_Click(object sender, EventArgs e) { viewHelpToolStripMenuItem_Click(sender, e); } // Help > About... private void aboutToolStripMenuItem_Click(object sender, EventArgs e) { AboutBox dlg = new AboutBox(); dlg.ShowDialog(); dlg.Dispose(); } private void codeListView_MouseClick(object sender, MouseEventArgs e) { //if (e.Button == MouseButtons.Left) { // Debug.WriteLine("LEFT CLICK"); //} else if (e.Button == MouseButtons.Right) { // Debug.WriteLine("RIGHT CLICK"); // //ShowRightClickMenu(); //} else { // Debug.WriteLine("CLICK " + e.Button); //} } private void codeListView_MouseDoubleClick(object sender, MouseEventArgs e) { ListViewHitTestInfo info = codeListView.HitTest(e.X, e.Y); int row = info.Item.Index; int col = info.Item.SubItems.IndexOf(info.SubItem); // col will be -1 for e.g. blank lines [not anymore?] string value = col < 0 ? "-" : info.Item.SubItems[col].Text; Debug.WriteLine(string.Format("R{0}:C{1} val '{2}'", row, col, value)); // It's possible to select multiple lines with shift-double-click. We // handle that by checking to see what UpdateActionMenu() decided was available. // 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. DisplayList.Line line = mDisplayList[row]; switch (line.LineType) { case DisplayList.Line.Type.EquDirective: // Currently only does something for project symbols; platform symbols // do nothing. if (editProjectSymbolToolStripMenuItem.Enabled) { EditProjectSymbol_Click(sender, e); } break; case DisplayList.Line.Type.OrgDirective: if (setAddressToolStripMenuItem.Enabled) { EditAddress_Click(sender, e); } break; case DisplayList.Line.Type.RegWidthDirective: if (overrideStatusFlagsToolStripMenuItem.Enabled) { EditStatusFlags_Click(sender, e); } break; case DisplayList.Line.Type.LongComment: if (editLongCommentToolStripMenuItem.Enabled) { EditLongComment_Click(sender, e); } break; case DisplayList.Line.Type.Note: if (editNoteToolStripMenuItem.Enabled) { EditNote_Click(sender, e); } break; case DisplayList.Line.Type.Code: case DisplayList.Line.Type.Data: // For code and data, we have to break it down by column. switch ((ColumnIndex)col) { case ColumnIndex.Offset: // does nothing break; case ColumnIndex.Address: // edit address if (setAddressToolStripMenuItem.Enabled) { EditAddress_Click(sender, e); } break; case ColumnIndex.Bytes: if (showHexDumpToolStripMenuItem.Enabled) { ShowHexDump_Click(sender, e); } break; case ColumnIndex.Flags: if (overrideStatusFlagsToolStripMenuItem.Enabled) { EditStatusFlags_Click(sender, e); } break; case ColumnIndex.Attributes: // does nothing break; case ColumnIndex.Label: if (editLabelToolStripMenuItem.Enabled) { EditLabel_Click(sender, e); } break; case ColumnIndex.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); // Does this have an operand with an in-file target offset? if (attr.OperandOffset >= 0) { // Yup, find the line for that offset and jump to it. GoToOffset(attr.OperandOffset, false, true); //int targetIndex = // mDisplayList.FindLineIndexByOffset(attr.OperandOffset); //GoToOffset(mDisplayList[targetIndex].FileOffset); } 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) { GoToOffset(operandOffset, false, true); //int targetIndex = // mDisplayList.FindLineIndexByOffset(operandOffset); //GoToOffset(mDisplayList[targetIndex].FileOffset); } } } break; case ColumnIndex.Operand: if (editOperandToolStripMenuItem.Enabled) { EditInstrDataOperand_Click(sender, e); } break; case ColumnIndex.Comment: if (editCommentToolStripMenuItem.Enabled) { EditComment_Click(sender, e); } break; } break; default: Debug.WriteLine("Double-click: unhandled line type " + line.LineType); break; } } /// /// 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. private void GoToOffset(int gotoOffset, bool jumpToNote, bool doPush) { int curSelIndex = -1; if (codeListView.SelectedIndices.Count > 0) { curSelIndex = codeListView.SelectedIndices[0]; } int topLineIndex = mDisplayList.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 (mDisplayList[topLineIndex].LineType != DisplayList.Line.Type.Note) { topLineIndex++; Debug.Assert(mDisplayList[topLineIndex].FileOffset == gotoOffset); } lastLineIndex = topLineIndex + 1; while (lastLineIndex < mDisplayList.Count && mDisplayList[lastLineIndex].LineType == DisplayList.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 (mDisplayList[topLineIndex].LineType != DisplayList.Line.Type.Code && mDisplayList[topLineIndex].LineType != DisplayList.Line.Type.Data) { topLineIndex++; } lastLineIndex = topLineIndex + 1; } // Make sure the item is visible. For notes, this can span multiple lines. codeListView.EnsureVisible(lastLineIndex - 1); codeListView.EnsureVisible(topLineIndex); // Update the selection. codeListView.DeselectAll(); for (int i = topLineIndex; i < lastLineIndex; i++) { codeListView.Items[i].Selected = true; } if (doPush) { if (curSelIndex >= 0) { // Update the back stack and associated controls. mNavStack.Push(mDisplayList[curSelIndex].FileOffset, gotoOffset); UpdateMenuItemsAndTitle(); } else { // This can happen when the project is first opened and nothing is selected. Debug.WriteLine("no selection to go back to"); } } } // Fires when the selection changes, causing the SelectionIndices list to change. // For multi-select, this seems to be called with an empty list. private void codeListView_SelectedIndexChanged(object sender, EventArgs e) { // Update the "references" and "info" window contents. UpdateReferenceView(); UpdateInfoView(); UpdateSelectionHighlight(); } // Virtual ListView selection tracking private void codeListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) { mCodeViewSelection.ItemSelectionChanged(e); // Don't try to call mCodeViewSelection.DebugValidateSelectionCount here. // Events will fire during RestoreSelection() at a point where the // SelectedIndices don't match up. if (!mRestoringSelection) { UpdateActionMenu(); } } // Virtual ListView selection tracking private void codeListView_VirtualItemsSelectionRangeChanged(object sender, ListViewVirtualItemsSelectionRangeChangedEventArgs e) { mCodeViewSelection.VirtualItemsSelectionRangeChanged(e); if (!mRestoringSelection) { UpdateActionMenu(); } } /// /// Enables or disables the menu items in the Actions menu. /// /// We want to do this whenever the selection changes so that any keyboard shortcuts /// are enabled or disabled appropriately. This does need to be reasonably fast /// for large files. /// /// The outcome of this method -- menu items being enabled or disabled -- is also /// used by the double-click handler. /// private void UpdateActionMenu() { if (mProject == null) { // Disable all actions. foreach (ToolStripItem item in mActionsMenuItems) { item.Enabled = false; } return; } // While restoring the selection, the SelectedIndices won't match up, because // part of the restore process is setting and clearing the control to match // what's in the mCodeViewSelection array. There's no value in repeating // this for every event caused by the restoration, so we don't expect to be // called at all during a restore. (Just make sure to call here after the // restore is complete.) Debug.Assert(!mRestoringSelection); Debug.Assert(mCodeViewSelection.DebugValidateSelectionCount( codeListView.SelectedIndices.Count), "selection count mismatch"); ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; EntityCounts entityCounts; bool editDataOperandEnabled = false; bool editInstrOperandEnabled = false; // 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 (IsSingleItemSelected()) { entityCounts = GatherEntityCounts(sel[0]); DisplayList.Line line = mDisplayList[sel[0]]; DisplayList.Line.Type lineType = line.LineType; bool isCodeOrData = (lineType == DisplayList.Line.Type.Code || lineType == DisplayList.Line.Type.Data); setAddressToolStripMenuItem.Enabled = (isCodeOrData || lineType == DisplayList.Line.Type.OrgDirective); editInstrOperandEnabled = (lineType == DisplayList.Line.Type.Code && mProject.GetAnattrib(line.FileOffset).IsInstructionWithOperand); editDataOperandEnabled = (lineType == DisplayList.Line.Type.Data); editLabelToolStripMenuItem.Enabled = isCodeOrData; editCommentToolStripMenuItem.Enabled = isCodeOrData; editLongCommentToolStripMenuItem.Enabled = (isCodeOrData || lineType == DisplayList.Line.Type.LongComment); editNoteToolStripMenuItem.Enabled = (isCodeOrData || lineType == DisplayList.Line.Type.Note); overrideStatusFlagsToolStripMenuItem.Enabled = (lineType == DisplayList.Line.Type.Code || lineType == DisplayList.Line.Type.RegWidthDirective); deleteNoteCommentToolStripMenuItem.Enabled = (lineType == DisplayList.Line.Type.LongComment || lineType == DisplayList.Line.Type.Note); if (lineType == DisplayList.Line.Type.EquDirective) { // Only enable this for project symbols, not all EQU directives. int symIndex = DisplayList.DefSymIndexFromOffset(line.FileOffset); DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex]; editProjectSymbolToolStripMenuItem.Enabled = (defSym.SymbolSource == Symbol.Source.Project); } else { editProjectSymbolToolStripMenuItem.Enabled = false; } } else { entityCounts = GatherEntityCounts(-1); // Disable all single-target-only items. setAddressToolStripMenuItem.Enabled = false; editInstrOperandEnabled = false; editLabelToolStripMenuItem.Enabled = false; editCommentToolStripMenuItem.Enabled = false; editLongCommentToolStripMenuItem.Enabled = false; editNoteToolStripMenuItem.Enabled = false; overrideStatusFlagsToolStripMenuItem.Enabled = false; deleteNoteCommentToolStripMenuItem.Enabled = false; editProjectSymbolToolStripMenuItem.Enabled = false; if (sel.Count == 0) { // Disable everything else. editDataOperandEnabled = false; hintAsCodeToolStripMenuItem.Enabled = false; hintAsDataToolStripMenuItem.Enabled = false; hintAsInlineDataToolStripMenuItem.Enabled = false; removeHintToolStripMenuItem.Enabled = false; } else { // Must be all data items. Blank lines are okay. Currently allowing // control lines as well. editDataOperandEnabled = (entityCounts.mDataLines > 0 && entityCounts.mCodeLines == 0); } } // Enable the "edit operand" menu item if either instruction or data operand // editing is allowed. editOperandToolStripMenuItem.Enabled = editDataOperandEnabled || editInstrOperandEnabled; formatSplitAddressTableToolStripMenuItem.Enabled = (entityCounts.mDataLines > 0 && entityCounts.mCodeLines == 0); toggleSingleBytesToolStripMenuItem.Enabled = (entityCounts.mDataLines > 0 && entityCounts.mCodeLines == 0); // So long as some code or data is highlighted, allow these. Don't worry about // control lines. Disable options that would have no effect. bool enableHints = (entityCounts.mDataLines > 0 || entityCounts.mCodeLines > 0); hintAsCodeToolStripMenuItem.Enabled = enableHints && (entityCounts.mDataHints != 0 || entityCounts.mInlineDataHints != 0 || entityCounts.mNoHints != 0); hintAsDataToolStripMenuItem.Enabled = enableHints && (entityCounts.mCodeHints != 0 || entityCounts.mInlineDataHints != 0 || entityCounts.mNoHints != 0); hintAsInlineDataToolStripMenuItem.Enabled = enableHints && (entityCounts.mCodeHints != 0 || entityCounts.mDataHints != 0 || entityCounts.mNoHints != 0); removeHintToolStripMenuItem.Enabled = enableHints && (entityCounts.mCodeHints != 0 || entityCounts.mDataHints != 0 || entityCounts.mInlineDataHints != 0); // Just leave this on. If they're in EQU-land or nothing is selected, it'll just // open at the start of the file. showHexDumpToolStripMenuItem.Enabled = true; } /// /// Entity count collection, for GatherEntityCounts. /// private 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 offset hinting. /// /// If a single line is selected, pass the index in. /// Otherwise, pass -1 to traverse the entire line list. /// 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 = mDisplayList.Count - 1; } else { startIndex = endIndex = singleLineIndex; } for (int i = startIndex; i <= endIndex; i++) { if (!mCodeViewSelection[i]) { continue; } DisplayList.Line line = mDisplayList[i]; switch (line.LineType) { case DisplayList.Line.Type.Code: codeLines++; break; case DisplayList.Line.Type.Data: dataLines++; break; case DisplayList.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. 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=" + mCodeViewSelection.Length + ") 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() { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; if (sel.Count == 0) { return false; } else if (sel.Count == 1) { return true; } // The selection is presented in sorted order, so we can just check the // first and last entries to see if they're the same. // // Performance note: SelectedIndices[] appears to be dynamic. In a very // large list (500K+ entries), requesting sel[sel.Count - 1] can take a few // seconds. As an optimization we just give up if the selection spans // more than a few hundred lines. (At worst, this requires clicking a single line // of a comment/note to edit it as an individual item.) // TODO(maybe): iterate over mCodeViewSelection instead? Would need more stuff there. if (sel.Count >= 500) { Debug.WriteLine("Selection very large (" + sel.Count + "), not checking for " + "single-item span"); return false; } DisplayList.Line firstItem = mDisplayList[sel[0]]; DisplayList.Line lastItem = mDisplayList[sel[sel.Count - 1]]; 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) { mTargetHighlightIndex = targetIndex; Debug.WriteLine("Selection highlight now " + targetIndex); // Force a redraw. codeListView.BeginUpdate(); //ClearCodeListViewCache(); // not necessary; only formatting has changed codeListView.EndUpdate(); } } private int FindSelectionHighlight() { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; if (sel.Count != 1) { return -1; } DisplayList.Line line = mDisplayList[codeListView.SelectedIndices[0]]; 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 mDisplayList.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 mDisplayList.FindCodeDataIndexByOffset(operandOffset); } } return -1; } private void editToolStripMenuItem_DropDownOpening(object sender, EventArgs e) { // Set the checkmark on Toggle Data Scan. toggleDataScanToolStripMenuItem.Checked = (mProject != null) && mProject.ProjectProps.AnalysisParams.AnalyzeUncategorizedData; } /// /// Handles an "opening" event for the codeListView's ContextMenuStrip. /// /// This puts all of the Actions menu items in the pop-up context menu. /// private void codeListView_CmsOpening(object sender, CancelEventArgs e) { codeListView.ContextMenuStrip.Items.AddRange(mActionsMenuItems); e.Cancel = false; } /// /// Handles an "opening" event for the ProjectView's Actions menu. /// /// This puts all of the Actions menu items in the Actions menu. We also want to call /// this when the context menu closes, so that Alt-A will open the menu. /// private void ActionsMenuOpening(object sender, EventArgs e) { actionsToolStripMenuItem.DropDownItems.AddRange(mActionsMenuItems); } private void EditAddress_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(IsSingleItemSelected()); int offset = mDisplayList[sel[0]].FileOffset; EditAddress dlg = new EditAddress(); Anattrib attr = mProject.GetAnattrib(offset); dlg.MaxAddressValue = mProject.CpuDef.MaxAddressValue; dlg.SetInitialAddress(attr.Address); dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { if (offset == 0 && dlg.Address < 0) { // Not allowed. The AddressMap will just put it back, which confuses // the undo operation. Debug.WriteLine("Not allowed to remove address at offset +000000"); } else if (attr.Address != dlg.Address) { Debug.WriteLine("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("No change to address"); } } dlg.Dispose(); } // Convert generic "edit operand" request to instruction or data edit call. private void EditInstrDataOperand_Click(object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; if (mDisplayList[sel[0]].LineType == DisplayList.Line.Type.Code) { EditOperand_Click(sender, e); } else { EditData_Click(sender, e); } } private void EditOperand_Click(Object sender, EventArgs e) { Debug.Assert(IsSingleItemSelected()); ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; int offset = mDisplayList[sel[0]].FileOffset; EditOperand dlg = new EditOperand(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 == DialogResult.OK) { ChangeSet cs = new ChangeSet(1); if (dlg.FormatDescriptor != dfd && dlg.ShortcutAction != EditOperand.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 == EditOperand.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 EditOperand.SymbolShortcutAction.CreateLabelInstead: case EditOperand.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 EditOperand.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 EditOperand.SymbolShortcutAction.None: break; } if (cs.Count != 0) { ApplyUndoableChanges(cs); } } dlg.Dispose(); } private void EditData_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(sel.Count > 0); EditData dlg = new EditData(mProject.FileData, mProject.SymbolTable, mOutputFormatter); TypedRangeSet trs = dlg.Selection = GroupedOffsetSetFromSelected(); if (trs.Count == 0) { Debug.Assert(false, "EditData found nothing to edit"); // shouldn't happen dlg.Dispose(); 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; if (mProject.OperandFormats.TryGetValue(firstOffset.Value, out FormatDescriptor dfd)) { dlg.FirstFormatDescriptor = dfd; } dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { // 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"); } } dlg.Dispose(); } private void FormatSplitAddressTable_Click(object sender, EventArgs e) { TypedRangeSet trs = GroupedOffsetSetFromSelected(); if (trs.Count == 0) { // shouldn't happen Debug.Assert(false, "FormatSplitAddressTable found nothing to edit"); return; } FormatSplitAddress dlg = new FormatSplitAddress(mProject, trs, mOutputFormatter); dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { // Start with the format descriptors. ChangeSet cs = mProject.GenerateFormatMergeSet(dlg.NewFormatDescriptors); // Add in the user labels. foreach (KeyValuePair kvp in dlg.NewUserLabels) { Symbol oldUserValue = null; if (mProject.UserLabels.ContainsKey(kvp.Key)) { Debug.Assert(false, "should not be replacing label"); oldUserValue = mProject.UserLabels[kvp.Key]; } UndoableChange uc = UndoableChange.CreateLabelChange(kvp.Key, oldUserValue, kvp.Value); cs.Add(uc); } // Apply code hints. if (dlg.WantCodeHints) { TypedRangeSet newSet = new TypedRangeSet(); TypedRangeSet undoSet = new TypedRangeSet(); foreach (int offset in dlg.AllTargetOffsets) { if (!mProject.GetAnattrib(offset).IsInstruction) { CodeAnalysis.TypeHint oldType = mProject.TypeHints[offset]; if (oldType == CodeAnalysis.TypeHint.Code) { continue; // already set } undoSet.Add(offset, (int)oldType); newSet.Add(offset, (int)CodeAnalysis.TypeHint.Code); } } if (newSet.Count != 0) { cs.Add(UndoableChange.CreateTypeHintChange(undoSet, newSet)); } } // Finally, apply the change. if (cs.Count != 0) { ApplyUndoableChanges(cs); } else { Debug.WriteLine("No changes found"); } } dlg.Dispose(); } private void ToggleSingleBytes_Click(object sender, EventArgs e) { TypedRangeSet trs = GroupedOffsetSetFromSelected(); if (trs.Count == 0) { Debug.Assert(false, "nothing to edit"); // shouldn't happen return; } // Check the format descriptor of the first selected offset. int firstOffset = -1; foreach (TypedRangeSet.Tuple tup in trs) { firstOffset = tup.Value; break; } Debug.Assert(mProject.GetAnattrib(firstOffset).IsDataStart); bool toDefault = false; if (mProject.OperandFormats.TryGetValue(firstOffset, out FormatDescriptor curDfd)) { if (curDfd.FormatType == FormatDescriptor.Type.NumericLE && curDfd.FormatSubType == FormatDescriptor.SubType.None && curDfd.Length == 1) { // Currently single-byte, toggle to default. toDefault = true; } } // Iterate through the selected regions. SortedList newFmts = new SortedList(); IEnumerator rngIter = trs.RangeListIterator; while (rngIter.MoveNext()) { TypedRangeSet.TypedRange rng = rngIter.Current; if (toDefault) { // Create a single REMOVE descriptor that covers the full span. FormatDescriptor newDfd = FormatDescriptor.Create(rng.High - rng.Low + 1, FormatDescriptor.Type.REMOVE, FormatDescriptor.SubType.None); newFmts.Add(rng.Low, newDfd); } else { // Add individual single-byte format descriptors for everything. FormatDescriptor newDfd = FormatDescriptor.Create(1, FormatDescriptor.Type.NumericLE, FormatDescriptor.SubType.None); for (int i = rng.Low; i <= rng.High; i++) { newFmts.Add(i, newDfd); } } } ChangeSet cs = mProject.GenerateFormatMergeSet(newFmts); if (cs.Count != 0) { ApplyUndoableChanges(cs); } } private void DeleteNoteComment_Click(object sender, EventArgs e) { Debug.Assert(IsSingleItemSelected()); ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; DisplayList.Line line = mDisplayList[sel[0]]; int offset = line.FileOffset; UndoableChange uc; if (line.LineType == DisplayList.Line.Type.Note) { if (!mProject.Notes.TryGetValue(offset, out MultiLineComment oldNote)) { Debug.Assert(false); return; } uc = UndoableChange.CreateNoteChange(offset, oldNote, null); } else if (line.LineType == DisplayList.Line.Type.LongComment) { if (!mProject.LongComments.TryGetValue(offset, out MultiLineComment oldComment)) { Debug.Assert(false); return; } uc = UndoableChange.CreateLongCommentChange(offset, oldComment, null); } else { Debug.Assert(false); return; } ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } private void EditLabel_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(IsSingleItemSelected()); int offset = mDisplayList[sel[0]].FileOffset; EditLabel dlg = new EditLabel(mProject.SymbolTable); Anattrib attr = mProject.GetAnattrib(offset); dlg.LabelSym = attr.Symbol; dlg.Address = attr.Address; dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { // 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); } } dlg.Dispose(); } private void EditStatusFlags_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(IsSingleItemSelected()); int offset = mDisplayList[sel[0]].FileOffset; EditStatusFlags dlg = new EditStatusFlags(); dlg.HasEmuFlag = mProject.CpuDef.HasEmuFlag; dlg.FlagValue = mProject.StatusFlagOverrides[offset]; dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { if (dlg.FlagValue != mProject.StatusFlagOverrides[offset]) { UndoableChange uc = UndoableChange.CreateStatusFlagChange(offset, mProject.StatusFlagOverrides[offset], dlg.FlagValue); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } } dlg.Dispose(); } private void EditComment_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(IsSingleItemSelected()); int offset = mDisplayList[sel[0]].FileOffset; EditComment dlg = new EditComment(); string oldComment = dlg.Comment = mProject.Comments[offset]; dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { if (!oldComment.Equals(dlg.Comment)) { Debug.WriteLine("Changing comment at +" + offset.ToString("x6")); UndoableChange uc = UndoableChange.CreateCommentChange(offset, oldComment, dlg.Comment); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } } dlg.Dispose(); } private void EditLongComment_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(IsSingleItemSelected()); EditLongComment(mDisplayList[sel[0]].FileOffset); } private void editHeaderCommentToolStripMenuItem_Click(object sender, EventArgs e) { EditLongComment(DisplayList.Line.HEADER_COMMENT_OFFSET); } private void EditLongComment(int offset) { EditLongComment dlg = new EditLongComment(mOutputFormatter); if (mProject.LongComments.TryGetValue(offset, out MultiLineComment oldComment)) { dlg.LongComment = oldComment; } dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { 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); } } dlg.Dispose(); } private void EditNote_Click(Object sender, EventArgs e) { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; Debug.Assert(IsSingleItemSelected()); int offset = mDisplayList[sel[0]].FileOffset; EditNote dlg = new EditNote(); if (mProject.Notes.TryGetValue(offset, out MultiLineComment oldNote)) { dlg.Note = oldNote; } dlg.ShowDialog(); if (dlg.DialogResult == DialogResult.OK) { MultiLineComment newNote = dlg.Note; if (oldNote != newNote) { Debug.WriteLine("Changing note at +" + offset.ToString("x6")); UndoableChange uc = UndoableChange.CreateNoteChange(offset, oldNote, newNote); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } } dlg.Dispose(); } private void EditProjectSymbol_Click(object sender, EventArgs e) { Debug.Assert(IsSingleItemSelected()); ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; DisplayList.Line line = mDisplayList[sel[0]]; int symIndex = DisplayList.DefSymIndexFromOffset(line.FileOffset); DefSymbol origDefSym = mProject.ActiveDefSymbolList[symIndex]; Debug.Assert(origDefSym.SymbolSource == Symbol.Source.Project); EditDefSymbol dlg = new EditDefSymbol(mOutputFormatter, mProject.ProjectProps.ProjectSyms); dlg.DefSym = origDefSym; if (dlg.ShowDialog() == DialogResult.OK) { ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps); newProps.ProjectSyms.Remove(origDefSym.Label); newProps.ProjectSyms[dlg.DefSym.Label] = dlg.DefSym; UndoableChange uc = UndoableChange.CreateProjectPropertiesChange( mProject.ProjectProps, newProps); ChangeSet cs = new ChangeSet(uc); ApplyUndoableChanges(cs); } dlg.Dispose(); } private void MarkAsCode_Click(Object sender, EventArgs e) { MarkAsType(CodeAnalysis.TypeHint.Code, true); } private void MarkAsData_Click(Object sender, EventArgs e) { MarkAsType(CodeAnalysis.TypeHint.Data, true); } private void MarkAsInlineData_Click(Object sender, EventArgs e) { MarkAsType(CodeAnalysis.TypeHint.InlineData, false); } private void MarkAsNoHint_Click(Object sender, EventArgs e) { MarkAsType(CodeAnalysis.TypeHint.NoHint, false); } private void MarkAsType(CodeAnalysis.TypeHint hint, bool firstByteOnly) { RangeSet sel; if (firstByteOnly) { sel = new RangeSet(); foreach (int index in codeListView.SelectedIndices) { int offset = mDisplayList[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); } private void ShowHexDump_Click(object sender, EventArgs e) { if (mHexDumpDialog == null) { // Create and show modeless dialog. This one is "always on top" by default, // to allow the user to click around to various points. mHexDumpDialog = new Tools.HexDumpViewer(mProject.FileData, mOutputFormatter); mHexDumpDialog.OnWindowClosing += (sender1, e1) => { Debug.WriteLine("Hex dump dialog closed"); //showHexDumpToolStripMenuItem.Checked = false; mHexDumpDialog = null; }; mHexDumpDialog.TopMost = true; mHexDumpDialog.Show(); //showHexDumpToolStripMenuItem.Checked = true; } // Bring it to the front of the window stack. This also transfers focus to the // window. mHexDumpDialog.BringToFront(); // Set the dialog's position. ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; if (sel.Count > 0) { int firstIndex = sel[0]; int lastIndex = sel[sel.Count - 1]; // offsets can be < 0 if they've selected EQU statements int firstOffset = Math.Max(0, mDisplayList[firstIndex].FileOffset); int lastOffset = Math.Max(firstOffset, mDisplayList[lastIndex].FileOffset + mDisplayList[lastIndex].OffsetSpan - 1); mHexDumpDialog.ShowOffsetRange(firstOffset, lastOffset); } } private void aSCIIChartToolStripMenuItem_Click(object sender, EventArgs e) { // Show or hide the modeless dialog. if (mAsciiChartDialog == null) { Tools.AsciiChart dlg = new Tools.AsciiChart(); dlg.OnWindowClosing += (sender1, e1) => { Debug.WriteLine("ASCII chart closed"); aSCIIChartToolStripMenuItem.Checked = false; mAsciiChartDialog = null; }; dlg.Show(); mAsciiChartDialog = dlg; aSCIIChartToolStripMenuItem.Checked = true; } else { // Ask the dialog to close. Do the cleanup in the event. mAsciiChartDialog.Close(); } } /// /// 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. /// /// Boundaries such as labels and address changes are ignored. /// /// RangeSet with all offsets. private RangeSet OffsetSetFromSelected() { RangeSet rs = new RangeSet(); foreach (int index in codeListView.SelectedIndices) { int offset = mDisplayList[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; } /// /// 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; bool thing = false; if (thing) { DateTime nowWhen = DateTime.Now; int selCount = 0; for (int i = 0; i < mDisplayList.Count; i++) { ListViewItem lvi = codeListView.Items[i]; selCount += lvi.Selected ? 1 : 0; } Debug.WriteLine("Sel count (" + selCount + ") took " + (DateTime.Now - nowWhen).TotalMilliseconds + " ms"); } DateTime startWhen = DateTime.Now; int prevOffset = -1; foreach (int index in codeListView.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 (!mDisplayList[index].IsCodeOrData) { continue; } int offset = mDisplayList[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. 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 a selection that skips code/data lines this is // expected. In the later 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 = mDisplayList[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 codeListView OwnerDraw implementation private enum ColumnIndex { Offset = 0, Address, Bytes, Flags, Attributes, Label, Opcode, Operand, Comment } /// /// Handy class for collecting column widths for the code list view. /// public class CodeListColumnWidths { public const int NUM_COLUMNS = (int)ColumnIndex.Comment + 1; /// /// Primary storage for column widths. /// public int[] Width { get; private set; } public int Offset { get { return Width[0]; } set { Width[0] = value; } } public int Address { get { return Width[1]; } set { Width[1] = value; } } public int Bytes { get { return Width[2]; } set { Width[2] = value; } } public int Flags { get { return Width[3]; } set { Width[3] = value; } } public int Attributes { get { return Width[4]; } set { Width[4] = value; } } public int Label { get { return Width[5]; } set { Width[5] = value; } } public int Opcode { get { return Width[6]; } set { Width[6] = value; } } public int Operand { get { return Width[7]; } set { Width[7] = value; } } public int Comment { get { return Width[8]; } set { Width[8] = value; } } public CodeListColumnWidths() { Width = new int[NUM_COLUMNS]; } public string Serialize() { StringBuilder sb = new StringBuilder(64); sb.Append("cw"); for (int i = 0; i < NUM_COLUMNS; i++) { sb.Append(','); sb.Append(Width[i]); } return sb.ToString(); } public static CodeListColumnWidths Deserialize(string cereal) { CodeListColumnWidths widths = new CodeListColumnWidths(); string[] splitted = cereal.Split(','); if (splitted.Length != NUM_COLUMNS + 1) { Debug.WriteLine("Column width parse failed: wrong count: " + splitted.Length); return null; } if (splitted[0] != "cw") { Debug.WriteLine("Column width parse failed: bad magic: " + splitted[0]); return null; } try { for (int i = 0; i < NUM_COLUMNS; i++) { widths.Width[i] = int.Parse(splitted[i + 1]); } } catch (Exception ex) { Debug.WriteLine("Column width parse failed: " + ex.Message); return null; } return widths; } public override string ToString() { return Serialize(); } } /// /// Gets the default column widths for the code list view, based on the currently /// configured font. /// /// Column width set. public CodeListColumnWidths GetDefaultCodeListColumnWidths() { CodeListColumnWidths widths = new CodeListColumnWidths(); Graphics gfx = codeListView.CreateGraphics(); widths.Offset = GetCodeListStringWidth(gfx, "X+000000"); widths.Address = GetCodeListStringWidth(gfx, "X00/0000"); widths.Bytes = GetCodeListStringWidth(gfx, "X00000000"); widths.Flags = GetCodeListStringWidth(gfx, "X00000000 0"); widths.Attributes = GetCodeListStringWidth(gfx, "X######"); widths.Label = GetCodeListStringWidth(gfx, "XMMMMMMMMM"); widths.Opcode = GetCodeListStringWidth(gfx, "XMMMMMMM"); widths.Operand = GetCodeListStringWidth(gfx, "XMMMMMMMMMMMMM"); widths.Comment = GetCodeListStringWidth(gfx, "XMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"); return widths; } private int GetCodeListStringWidth(Graphics gfx, string str) { SizeF size = gfx.MeasureString(str, codeListView.Font); return (int)Math.Round(size.Width); } /// /// Saves the code list column widths into AppSettings. /// /// public void SaveCodeListColumnWidths() { CodeListColumnWidths widths = new CodeListColumnWidths(); for (int i = 0; i < CodeListColumnWidths.NUM_COLUMNS; i++) { widths.Width[i] = codeListView.Columns[i].Width; } string cereal = widths.Serialize(); AppSettings.Global.SetString(AppSettings.CDLV_COL_WIDTHS, cereal); } /// /// Configures the column widths. /// private void SetCodeListHeaderWidths(CodeListColumnWidths widths) { Debug.WriteLine("Setting column widths: " + widths); for (int i = 0; i < CodeListColumnWidths.NUM_COLUMNS; i++) { codeListView.Columns[i].Width = widths.Width[i]; } } private void codeListView_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { //Debug.WriteLine("Column width changed: " + e.ColumnIndex); // This fires during initial setup when things don't have widths. A little // risky to save the widths off now; would be safer to do it right before we write // the settings file. SaveCodeListColumnWidths(); } private void codeListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e) { ListView lv = e.Header.ListView; string text = lv.Columns[e.ColumnIndex].Text; // Adjust rect to match standard control for 10pt fonts, and // reserve a couple pixels at the far right end for the separator. Rectangle rect = new Rectangle(e.Bounds.X + 3, e.Bounds.Y + 4, e.Bounds.Width - 4, e.Bounds.Height - 4); TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine; TextRenderer.DrawText(e.Graphics, text, lv.Font, rect, lv.ForeColor, flags); Pen pen = new Pen(Color.LightGray); //Pen pen = new Pen(Color.Blue); e.Graphics.DrawLine(pen, e.Bounds.X + e.Bounds.Width - 1, e.Bounds.Y, e.Bounds.X + e.Bounds.Width - 1, e.Bounds.Y + e.Bounds.Height); } private void codeListView_DrawItem(object sender, DrawListViewItemEventArgs e) { // Only draw the full-line items here. Do not draw them later. DisplayList.Line line = mDisplayList[e.ItemIndex]; if (line.LineType != DisplayList.Line.Type.LongComment && line.LineType != DisplayList.Line.Type.Note) { return; } // Column 5 is the label. We put long comments and notes there. int leftColsWidth = 0; for (int i = 0; i < (int)ColumnIndex.Label; i++) { leftColsWidth += e.Item.ListView.Columns[i].Width; } // No sub-items, just one long comment. ListView lv = e.Item.ListView; ListViewItem lvi = e.Item; // Set colors based on selection and focus. if (lvi.Selected && lv.Focused) { lvi.BackColor = SystemColors.Highlight; lvi.ForeColor = lv.BackColor; } else if (e.Item.Selected && !lv.Focused) { lvi.BackColor = SystemColors.Control; lvi.ForeColor = lv.ForeColor; } else { lvi.ForeColor = lv.ForeColor; if (line.BackgroundColor.ToArgb() == 0) { lvi.BackColor = lv.BackColor; } else { // Highlight the entire line. lvi.BackColor = line.BackgroundColor; } } e.DrawBackground(); if ((e.State & ListViewItemStates.Selected) != 0) { e.DrawFocusRectangle(); } Rectangle rect = new Rectangle(e.Bounds.X + 3 + leftColsWidth, e.Bounds.Y + 2, e.Bounds.Width - 3 - leftColsWidth, e.Bounds.Height - 2); TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine; Font font = lv.Font; TextRenderer.DrawText(e.Graphics, lvi.Text, font, rect, lvi.ForeColor, flags); } private void codeListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e) { // Draw the multi-column items here. ListView lv = e.Item.ListView; ListViewItem lvi = e.Item; DisplayList.Line.Type lineType = mDisplayList[e.ItemIndex].LineType; if (lineType == DisplayList.Line.Type.LongComment || lineType == DisplayList.Line.Type.Note) { return; } // Set colors based on selection and focus. if (lvi.Selected && lv.Focused) { e.SubItem.BackColor = SystemColors.Highlight; e.SubItem.ForeColor = lv.BackColor; } else if (lvi.Selected && !lv.Focused) { e.SubItem.BackColor = SystemColors.Control; e.SubItem.ForeColor = lv.ForeColor; } else { if (e.ItemIndex == mTargetHighlightIndex && (e.ColumnIndex == (int)ColumnIndex.Address || e.ColumnIndex == (int)ColumnIndex.Label) && !string.IsNullOrEmpty(e.SubItem.Text)) { e.SubItem.BackColor = Color.LightBlue; } else { e.SubItem.BackColor = lv.BackColor; } e.SubItem.ForeColor = lv.ForeColor; } e.DrawBackground(); // Shift the text so it lines up with the standard control at 10pts. // Not strictly necessary, and possibly unwise, since the behavior seems // to change for larger fonts. Rectangle rect = new Rectangle(e.Bounds.X + 3, e.Bounds.Y + 2, e.Bounds.Width - 3, e.Bounds.Height - 2); TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine; Font font = lv.Font; TextRenderer.DrawText(e.Graphics, e.SubItem.Text, font, rect, e.SubItem.ForeColor, flags); // Draw the focus rectangle. It's annoying that we have to draw it for every // sub-item even with FullRowSelect, but DrawItem always happens first, and // there's no equivalent at the end. (It's unclear how useful the focus rect // actually is, but it's part of the standard dialog behavior.) if (lv.FullRowSelect) { e.DrawFocusRectangle(e.Item.Bounds); } else { e.DrawFocusRectangle(e.Bounds); } } #endregion // codeListView OwnerDraw implementation #region codeListView Virtual // For a half-megabyte file, the ListViewItem creation could take 40+ seconds. // Internal array for holding temporary state. Avoids frequent allocations. private ListViewItem.ListViewSubItem[] mSubArray = new ListViewItem.ListViewSubItem[CodeListColumnWidths.NUM_COLUMNS - 1]; // Array of blank sub-items, for entries that span multiple columns. The // virtual mode requires fully populating sub-items. private static ListViewItem.ListViewSubItem[] mBlankArray = new ListViewItem.ListViewSubItem[CodeListColumnWidths.NUM_COLUMNS - 1] { new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem() }; /// /// Cache of previously-constructed ListViewItems. The ListView will request items /// continuously as they are moused-over, so this is fairly important. /// private ListViewItem[] mItemCache; private int mItemCacheFirst; /// /// Clears the contents of the ListViewItem cache. Do this whenever the backing /// store is updated. /// private void ClearCodeListViewCache() { mItemCache = null; mItemCacheFirst = -1; } //private ListViewItem mDummy; private void codeListView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { //Debug.WriteLine("Retrieve " + e.ItemIndex); //if (mDummy == null) { // mDummy = new ListViewItem(); // mDummy.Text = "dummy"; // mDummy.SubItems.AddRange(mBlankArray); //} //e.Item = mDummy; //return; // Is item cached? if (mItemCache != null && e.ItemIndex >= mItemCacheFirst && e.ItemIndex < mItemCacheFirst + mItemCache.Length) { // Yes, return existing item. e.Item = mItemCache[e.ItemIndex - mItemCacheFirst]; } else { // No, create item. e.Item = CreateCodeListViewItem(e.ItemIndex); } } private void codeListView_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) { if (mItemCache != null && e.StartIndex >= mItemCacheFirst && e.EndIndex <= mItemCacheFirst + mItemCache.Length) { // Already have this span cached. //Debug.WriteLine("LVICache " + e.StartIndex + " - " + e.EndIndex + // " already cached"); return; } //Debug.WriteLine("LVICache " + e.StartIndex + " - " + e.EndIndex + " generating"); mItemCacheFirst = e.StartIndex; int len = e.EndIndex - e.StartIndex + 1; // end is inclusive mItemCache = new ListViewItem[len]; for (int i = 0; i < len; i++) { mItemCache[i] = CreateCodeListViewItem(e.StartIndex + i); } } private ListViewItem CreateCodeListViewItem(int index) { DisplayList.Line line = mDisplayList[index]; DisplayList.FormattedParts parts = mDisplayList.GetFormattedParts(index); ListViewItem lvi = new ListViewItem(); if (line.LineType == DisplayList.Line.Type.Blank) { // no sub-items lvi.Text = String.Empty; lvi.SubItems.AddRange(mBlankArray); } else if (line.LineType == DisplayList.Line.Type.LongComment || line.LineType == DisplayList.Line.Type.Note) { lvi.Text = parts.Comment.Replace("&", "&&"); lvi.SubItems.AddRange(mBlankArray); } else { lvi.Text = parts.Offset; mSubArray[0] = new ListViewItem.ListViewSubItem(lvi, parts.Addr); mSubArray[1] = new ListViewItem.ListViewSubItem(lvi, parts.Bytes); mSubArray[2] = new ListViewItem.ListViewSubItem(lvi, parts.Flags); mSubArray[3] = new ListViewItem.ListViewSubItem(lvi, parts.Attr); mSubArray[4] = new ListViewItem.ListViewSubItem(lvi, parts.Label); mSubArray[5] = new ListViewItem.ListViewSubItem(lvi, parts.Opcode); mSubArray[6] = new ListViewItem.ListViewSubItem(lvi, parts.Operand.Replace("&", "&&")); mSubArray[7] = new ListViewItem.ListViewSubItem(lvi, parts.Comment == null ? string.Empty : parts.Comment.Replace("&", "&&")); Debug.Assert(CodeListColumnWidths.NUM_COLUMNS - 1 == 8); lvi.SubItems.AddRange(mSubArray); } return lvi; } #endregion codeListView Virtual #region symbolListView Virtual and UI handling /// /// Cache of previously-constructed ListViewItems. The ListView will request items /// continuously as they are moused-over, so this is fairly important. /// private ListViewItem[] mSymbolItemCache; private int mSymbolItemCacheFirst; // Temporary array, used during ListViewItem creation. private ListViewItem.ListViewSubItem[] mSymbolSubArray = new ListViewItem.ListViewSubItem[2]; private string[] mSymbolColumnHeaderNames; private void InitSymbolListView() { // Save a copy of the column header names as entered in the designer. mSymbolColumnHeaderNames = new string[3]; mSymbolColumnHeaderNames[0] = symbolTypeColumnHeader.Text; mSymbolColumnHeaderNames[1] = symbolValueColumnHeader.Text; mSymbolColumnHeaderNames[2] = symbolNameColumnHeader.Text; SetSymbolColumnHeaders(); } /// /// Clears the contents of the ListViewItem cache. Do this whenever the backing /// store is updated. /// private void ClearSymbolListViewCache() { mSymbolItemCache = null; mSymbolItemCacheFirst = -1; } private void symbolListView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { // Is item cached? if (mSymbolItemCache != null && e.ItemIndex >= mSymbolItemCacheFirst && e.ItemIndex < mSymbolItemCacheFirst + mSymbolItemCache.Length) { // Yes, return existing item. e.Item = mSymbolItemCache[e.ItemIndex - mSymbolItemCacheFirst]; } else { // No, create item. e.Item = CreateSymbolListViewItem(e.ItemIndex); } } private void symbolListView_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) { if (mSymbolItemCache != null && e.StartIndex >= mSymbolItemCacheFirst && e.EndIndex <= mSymbolItemCacheFirst + mSymbolItemCache.Length) { // Already have this span cached. //Debug.WriteLine("LVICache " + e.StartIndex + " - " + e.EndIndex + // " already cached"); return; } //Debug.WriteLine("LVICache " + e.StartIndex + " - " + e.EndIndex + " generating"); mSymbolItemCacheFirst = e.StartIndex; int len = e.EndIndex - e.StartIndex + 1; // end is inclusive mSymbolItemCache = new ListViewItem[len]; for (int i = 0; i < len; i++) { mSymbolItemCache[i] = CreateSymbolListViewItem(e.StartIndex + i); } } private ListViewItem CreateSymbolListViewItem(int index) { Symbol sym = mSymbolSubset.GetSubsetItem(index); ListViewItem lvi = new ListViewItem(); lvi.Text = sym.SourceTypeString; mSymbolSubArray[0] = new ListViewItem.ListViewSubItem(lvi, mOutputFormatter.FormatHexValue(sym.Value, 0)); mSymbolSubArray[1] = new ListViewItem.ListViewSubItem(lvi, sym.Label); lvi.SubItems.AddRange(mSymbolSubArray); return lvi; } private void InvalidateSymbolListView() { symbolListView.BeginUpdate(); ClearSymbolListViewCache(); symbolListView.VirtualListSize = mSymbolSubset.GetSubsetCount(); symbolListView.EndUpdate(); } // Column header click. Update sort. private void symbolListView_ColumnClick(object sender, ColumnClickEventArgs e) { //Debug.WriteLine("Click on " + e.Column); SymbolTableSubset.SortCol prevCol = mSymbolSubset.SortColumn; // SortCol happens to match the ListView column numbers, so just cast it SymbolTableSubset.SortCol clickCol = (SymbolTableSubset.SortCol)e.Column; if (prevCol == clickCol) { mSymbolSubset.SortAscending = !mSymbolSubset.SortAscending; } else { mSymbolSubset.SortColumn = clickCol; } SetSymbolColumnHeaders(); InvalidateSymbolListView(); } private void symbolListView_MouseDoubleClick(object sender, MouseEventArgs e) { ListViewHitTestInfo info = symbolListView.HitTest(e.X, e.Y); int row = info.Item.Index; Symbol sym = mSymbolSubset.GetSubsetItem(row); if (sym.SymbolSource == Symbol.Source.Auto || sym.SymbolSource == Symbol.Source.User) { int offset = mProject.FindLabelByName(sym.Label); if (offset >= 0) { GoToOffset(offset, false, true); codeListView.Focus(); } else { Debug.WriteLine("DClick symbol: " + sym + ": label not found"); } } else { Debug.WriteLine("DClick symbol: " + sym + ": not label"); } } /// /// Sets the ListView column headers, adding a glyph to show sort direction. /// Sadly, this is significantly easier than adding a graphic. /// private void SetSymbolColumnHeaders() { SymbolTableSubset.SortCol sortCol = mSymbolSubset.SortColumn; // Pick a pair of symbols. string sortStr = mSymbolSubset.SortAscending ? "\u25b2" : "\u25bc"; // BLACK UP-POINTING TRIANGLE and DOWN- //"\u2191" : "\u2193"; // UPWARDS ARROW and DOWNWARDS ARROW //"\u2b06" : "\u2b07"; // UPWARDS BLACK ARROW and DOWNWARDS BLACK ARROW //"\u234d" : "\u2354"; // APL FUNCTIONAL SYMBOL QUAD DELTA and ...QUAD DEL symbolTypeColumnHeader.Text = (sortCol == SymbolTableSubset.SortCol.Type ? sortStr : "") + mSymbolColumnHeaderNames[0]; symbolValueColumnHeader.Text = (sortCol == SymbolTableSubset.SortCol.Value ? sortStr : "") + mSymbolColumnHeaderNames[1]; symbolNameColumnHeader.Text = (sortCol == SymbolTableSubset.SortCol.Name ? sortStr : "") + mSymbolColumnHeaderNames[2]; } private void symbolListView_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { UpdateLastSymbolColumnWidth(); } private void symbolListView_SizeChanged(object sender, EventArgs e) { UpdateLastSymbolColumnWidth(); } private void UpdateLastSymbolColumnWidth() { #if false const int ADJ = 4; // fudge factor needed to prevent horizontal scrollbar int leftWidths = symbolListView.Columns[0].Width + symbolListView.Columns[1].Width; int lastWidth = symbolListView.Size.Width - leftWidths - ADJ; if (lastWidth < 0) { lastWidth = 0; } symbolListView.Columns[2].Width = lastWidth; #endif AppSettings.Global.Dirty = true; } private void SerializeSymbolColumnWidths() { int[] values = new int[] { symbolListView.Columns[0].Width, symbolListView.Columns[1].Width, symbolListView.Columns[2].Width }; AppSettings.Global.SetString(AppSettings.SYMWIN_COL_WIDTHS, TextUtil.SerializeIntArray(values)); } private void DeserializeSymbolColumnWidths() { string str = AppSettings.Global.GetString(AppSettings.SYMWIN_COL_WIDTHS, null); if (!string.IsNullOrEmpty(str)) { int[] values = TextUtil.DeserializeIntArray(str); if (values.Length == symbolListView.Columns.Count) { for (int i = 0; i < values.Length; i++) { symbolListView.Columns[i].Width = values[i]; } } } // The updates should automatically trigger the last-column-width adjuster. } private void symbolUserCheckBox_CheckedChanged(object sender, EventArgs e) { mSymbolSubset.IncludeUserLabels = symbolUserCheckBox.Checked; InvalidateSymbolListView(); } private void symbolProjectCheckBox_CheckedChanged(object sender, EventArgs e) { mSymbolSubset.IncludeProjectSymbols = symbolProjectCheckBox.Checked; InvalidateSymbolListView(); } private void symbolPlatformCheckBox_CheckedChanged(object sender, EventArgs e) { mSymbolSubset.IncludePlatformSymbols = symbolPlatformCheckBox.Checked; InvalidateSymbolListView(); } private void symbolAutoCheckBox_CheckedChanged(object sender, EventArgs e) { mSymbolSubset.IncludeAutoLabels = symbolAutoCheckBox.Checked; InvalidateSymbolListView(); } private void symbolAddrCheckBox_CheckedChanged(object sender, EventArgs e) { mSymbolSubset.IncludeAddresses = symbolAddressCheckBox.Checked; InvalidateSymbolListView(); } private void symbolConstantCheckBox_CheckedChanged(object sender, EventArgs e) { mSymbolSubset.IncludeConstants = symbolConstantCheckBox.Checked; InvalidateSymbolListView(); } #endregion symbolListView Virtual and UI handling #region referencesListView stuff private ListViewItem.ListViewSubItem[] mXrefSubArray = new ListViewItem.ListViewSubItem[2]; /// /// Updates the "references" view 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 there's no need for virtual items /// or output caching. /// private void UpdateReferenceView() { referencesListView.BeginUpdate(); try { referencesListView.Items.Clear(); // Determine which line is selected. If it's not code, data, or an EQU // directive, there's no data to populate. ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; if (sel.Count != 1) { return; } DisplayList.Line.Type type = mDisplayList[sel[0]].LineType; if (type != DisplayList.Line.Type.Code && type != DisplayList.Line.Type.Data && type != DisplayList.Line.Type.EquDirective) { // Code, data, and platform symbol EQUs have xrefs. return; } // Find the appropriate xref set. int offset = mDisplayList[sel[0]].FileOffset; XrefSet xrefs; if (offset < 0) { int index = DisplayList.DefSymIndexFromOffset(offset); DefSymbol defSym = mProject.ActiveDefSymbolList[index]; xrefs = defSym.Xrefs; } else { xrefs = mProject.GetXrefSet(offset); } if (xrefs == null || xrefs.Count == 0) { return; } Asm65.Formatter formatter = mOutputFormatter; bool showBank = !mProject.CpuDef.HasAddr16; for (int i = 0; i < xrefs.Count; i++) { XrefSet.Xref xr = xrefs[i]; ListViewItem lvi = new ListViewItem(); string typeStr; switch (xr.Type) { case XrefSet.XrefType.BranchOperand: typeStr = "branch "; break; case XrefSet.XrefType.InstrOperand: typeStr = "instr "; break; case XrefSet.XrefType.DataOperand: typeStr = "data "; break; default: Debug.Assert(false); typeStr = "??? "; break; } lvi.Text = formatter.FormatOffset24(xr.Offset); mXrefSubArray[0] = new ListViewItem.ListViewSubItem(lvi, formatter.FormatAddress(mProject.GetAnattrib(xr.Offset).Address, showBank)); mXrefSubArray[1] = new ListViewItem.ListViewSubItem(lvi, (xr.IsSymbolic ? "Sym " : "Num ") + typeStr + formatter.FormatAdjustment(-xr.Adjustment)); lvi.SubItems.AddRange(mXrefSubArray); referencesListView.Items.Add(lvi); } } finally { referencesListView.EndUpdate(); } } private void referencesListView_MouseDoubleClick(object sender, MouseEventArgs e) { ListViewHitTestInfo info = referencesListView.HitTest(e.X, e.Y); ListViewItem item = info.Item; // The easiest way to do this is to just parse it back out of the ListViewItem. int offset; try { offset = Convert.ToInt32(item.Text.Substring(1), 16); } catch (Exception ex) { Debug.Assert(false, "Bad ref offset '" + item.Text + "': " + ex.Message); return; } Debug.WriteLine("DClick refs, offset=+" + offset.ToString("x6")); // Jump to the note, and shift the focus back to the code view. GoToOffset(offset, false, true); codeListView.Focus(); } private void referencesListView_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { //Debug.WriteLine("CH: " + e.ColumnIndex + " " + // referencesListView.Columns[e.ColumnIndex].Width); UpdateLastReferencesColumnWidth(); } private void referencesListView_SizeChanged(object sender, EventArgs e) { UpdateLastReferencesColumnWidth(); } private void UpdateLastReferencesColumnWidth() { #if false const int ADJ = 4; // fudge factor needed to prevent horizontal scrollbar int leftWidths = referencesListView.Columns[0].Width + referencesListView.Columns[1].Width; int lastWidth = referencesListView.Size.Width - leftWidths - ADJ; if (lastWidth < 0) { lastWidth = 0; } referencesListView.Columns[2].Width = lastWidth; #endif AppSettings.Global.Dirty = true; } private void SerializeReferencesColumnWidths() { int[] values = new int[] { referencesListView.Columns[0].Width, referencesListView.Columns[1].Width, referencesListView.Columns[2].Width }; AppSettings.Global.SetString(AppSettings.REFWIN_COL_WIDTHS, TextUtil.SerializeIntArray(values)); } private void DeserializeReferencesColumnWidths() { string str = AppSettings.Global.GetString(AppSettings.REFWIN_COL_WIDTHS, null); if (!string.IsNullOrEmpty(str)) { int[] values = TextUtil.DeserializeIntArray(str); if (values.Length == referencesListView.Columns.Count) { for (int i = 0; i < values.Length; i++) { referencesListView.Columns[i].Width = values[i]; } } } // The updates should automatically trigger the last-column-width adjuster. } #endregion referencesListView stuff #region notesListView Virtual and UI handling // I'm not expecting there to be a lot of notes, but making this virtual avoids // having to update the item set when things change. (We could also just rebuild // the item list after any change is applied... but if we do have a lot of notes, // that could be much worse.) Only updating the list when the notes object changes // would be optimal, but requires probing ChangeSets. This was easier. /// /// Cache of previously-constructed ListViewItems. The ListView will request items /// continuously as they are moused-over, so this is fairly important. /// private ListViewItem[] mNotesItemCache; private int mNotesItemCacheFirst; // Temporary array, used during ListViewItem creation. private ListViewItem.ListViewSubItem[] mNotesSubArray = new ListViewItem.ListViewSubItem[1]; /// /// Clears the contents of the ListViewItem cache. Do this whenever the backing /// store is updated. /// private void ClearNotesListViewCache() { mNotesItemCache = null; mNotesItemCacheFirst = -1; } private void notesListView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { // Is item cached? if (mNotesItemCache != null && e.ItemIndex >= mNotesItemCacheFirst && e.ItemIndex < mNotesItemCacheFirst + mNotesItemCache.Length) { // Yes, return existing item. e.Item = mNotesItemCache[e.ItemIndex - mNotesItemCacheFirst]; } else { // No, create item. e.Item = CreateNotesListViewItem(e.ItemIndex); } } private void notesListView_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) { if (mNotesItemCache != null && e.StartIndex >= mNotesItemCacheFirst && e.EndIndex <= mNotesItemCacheFirst + mNotesItemCache.Length) { // Already have this span cached. return; } mNotesItemCacheFirst = e.StartIndex; int len = e.EndIndex - e.StartIndex + 1; // end is inclusive mNotesItemCache = new ListViewItem[len]; for (int i = 0; i < len; i++) { mNotesItemCache[i] = CreateNotesListViewItem(e.StartIndex + i); } } private ListViewItem CreateNotesListViewItem(int index) { int offset = mProject.Notes.Keys[index]; MultiLineComment mlc = mProject.Notes.Values[index]; ListViewItem lvi = new ListViewItem(); lvi.Text = mOutputFormatter.FormatOffset24(offset); lvi.Tag = mlc; mNotesSubArray[0] = new ListViewItem.ListViewSubItem(lvi, mlc.Text.Replace("\r\n", " \u2022 ")); lvi.SubItems.AddRange(mNotesSubArray); return lvi; } private void InvalidateNotesListView() { notesListView.BeginUpdate(); ClearNotesListViewCache(); if (mProject == null) { notesListView.VirtualListSize = 0; } else { notesListView.VirtualListSize = mProject.Notes.Count; } notesListView.EndUpdate(); } private void notesListView_MouseDoubleClick(object sender, MouseEventArgs e) { ListViewHitTestInfo info = notesListView.HitTest(e.X, e.Y); int row = info.Item.Index; int offset = mProject.Notes.Keys[row]; Debug.WriteLine("DClick Notes row=" + row + " offset=+" + offset.ToString("x6")); // Jump to the note, and shift the focus back to the code view. GoToOffset(offset, true, true); codeListView.Focus(); } private void notesListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e) { ListView lv = e.Header.ListView; string text = lv.Columns[e.ColumnIndex].Text; // Adjust rect to match standard control for 10pt fonts, and // reserve a couple pixels at the far right end for the separator. Rectangle rect = new Rectangle(e.Bounds.X + 3, e.Bounds.Y + 4, e.Bounds.Width - 4, e.Bounds.Height - 4); TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine; TextRenderer.DrawText(e.Graphics, text, lv.Font, rect, lv.ForeColor, flags); Pen pen = new Pen(Color.LightGray); e.Graphics.DrawLine(pen, e.Bounds.X + e.Bounds.Width - 1, e.Bounds.Y, e.Bounds.X + e.Bounds.Width - 1, e.Bounds.Y + e.Bounds.Height); } private void notesListView_DrawItem(object sender, DrawListViewItemEventArgs e) { // We have no full-width items. } private void notesListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e) { // Draw the multi-column items here. ListView lv = e.Item.ListView; ListViewItem lvi = e.Item; MultiLineComment mlc = (MultiLineComment) lvi.Tag; // Set colors based on selection and focus. if (lvi.Selected && lv.Focused && e.ColumnIndex == 0) { e.SubItem.BackColor = SystemColors.Highlight; e.SubItem.ForeColor = codeListView.BackColor; } else { if (e.ColumnIndex == 1 && mlc.BackgroundColor.ToArgb() != 0) { e.SubItem.BackColor = mlc.BackgroundColor; } else { e.SubItem.BackColor = lv.BackColor; } e.SubItem.ForeColor = lv.ForeColor; } e.DrawBackground(); // Shift the text so it lines up with the standard control at 10pts. // Not strictly necessary, and possibly unwise, since the behavior seems // to change for larger fonts. Rectangle rect = new Rectangle(e.Bounds.X + 3, e.Bounds.Y + 2, e.Bounds.Width - 3, e.Bounds.Height - 2); TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine; Font font = lv.Font; // TODO(maybe): consider drawing the note text with a proportional font. We // don't need multi-line stuff to line up, and it'll let us show more of // the text in a narrow window. TextRenderer.DrawText(e.Graphics, e.SubItem.Text, font, rect, e.SubItem.ForeColor, flags); // Draw the focus rectangle. It's annoying that we have to draw it for every // sub-item even with FullRowSelect, but DrawItem always happens first, and // there's no equivalent at the end. (It's unclear how useful the focus rect // actually is, but it's part of the standard dialog behavior.) if (lv.FullRowSelect) { e.DrawFocusRectangle(e.Item.Bounds); } else { e.DrawFocusRectangle(e.Bounds); } } private void notesListView_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { // We don't auto-adjust column widths, but we do want to make sure that the // width adjustment causes the settings to be saved. AppSettings.Global.Dirty = true; } private void SerializeNotesColumnWidths() { int[] values = new int[] { notesListView.Columns[0].Width, notesListView.Columns[1].Width }; AppSettings.Global.SetString(AppSettings.NOTEWIN_COL_WIDTHS, TextUtil.SerializeIntArray(values)); } private void DeserializeNotesColumnWidths() { string str = AppSettings.Global.GetString(AppSettings.NOTEWIN_COL_WIDTHS, null); if (!string.IsNullOrEmpty(str)) { int[] values = TextUtil.DeserializeIntArray(str); if (values.Length == notesListView.Columns.Count) { for (int i = 0; i < values.Length; i++) { notesListView.Columns[i].Width = values[i]; } } } } #endregion // notesListView Virtual and UI handling #region Info view /// /// Updates the Info window for the current selection. /// private void UpdateInfoView() { // I'm seeing weird behavior where, if you do a bunch of up-arrow/down-arrow // movement in the main window, the info window will eventually freeze. You can // refresh individual lines by dragging across them with the mouse. Resizing // the splitter pane leaves the window apparently blank, but moving or resizing // the application window causes an immediate redraw. // // Simply calling Invalidate() didn't help, which shouldn't be too surprising // since I expect assignment to the Text field to do something similar. Refresh() // feels a bit heavy-handed but it gets the job done. // // I see similar behavior from RichTextBox. The References ListView, which is // also updated when the selection changes, seems to work correctly though. // // This is on Win10 Pro x64, as of 2018/07/28. DoUpdateInfoView(); infoTextBox.Refresh(); } private void DoUpdateInfoView() { ListView.SelectedIndexCollection sel = codeListView.SelectedIndices; if (sel.Count != 1) { // Nothing selected, or multiple lines selected. // Calling here from SelectedIndexChanged events works fine for single-item // selections, but the SelectedIndices list is always empty for multi-select. // If we add this to other selection events it ends up getting called 3-4 // times when you arrow around. //infoRichTextBox.Text = "(" + // string.Format(Properties.Resources.FMT_LINES_SELECTED, sel.Count) + ")"; infoTextBox.Text = string.Empty; return; } int lineIndex = sel[0]; DisplayList.Line line = mDisplayList[lineIndex]; StringBuilder sb = new StringBuilder(250); // NOTE: this should be made easier to localize string lineTypeStr; string extraStr = string.Empty; switch (line.LineType) { case DisplayList.Line.Type.Code: lineTypeStr = "code"; break; case DisplayList.Line.Type.Data: if (mProject.GetAnattrib(line.FileOffset).IsInlineData) { lineTypeStr = "inline data"; } else { lineTypeStr = "data"; } break; case DisplayList.Line.Type.LongComment: lineTypeStr = "comment"; break; case DisplayList.Line.Type.Note: lineTypeStr = "note"; break; case DisplayList.Line.Type.Blank: lineTypeStr = "blank line"; //lineTypeStr = "blank line (+" + // mOutputFormatter.FormatOffset24(line.FileOffset) + ")"; break; case DisplayList.Line.Type.OrgDirective: lineTypeStr = "address directive"; break; case DisplayList.Line.Type.RegWidthDirective: lineTypeStr = "register width directive"; break; case DisplayList.Line.Type.EquDirective: { lineTypeStr = "equate"; int symIndex = DisplayList.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(Properties.Resources.FMT_INFO_LINE_SUM_NON, lineIndex, lineTypeStr); if (!string.IsNullOrEmpty(extraStr)) { sb.Append("\r\n\r\n"); sb.Append(extraStr); } infoTextBox.Text = 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(Properties.Resources.FMT_INFO_LINE_SUM_SINGULAR, lineIndex, line.OffsetSpan, lineTypeStr); } else { sb.AppendFormat(Properties.Resources.FMT_INFO_LINE_SUM_PLURAL, 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(Properties.Resources.FMT_INFO_FD_SUM, Properties.Resources.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(Properties.Resources.FMT_INFO_FD_SUM, 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("\u23e9[*] "); } else { sb.Append("\u23e9 "); } 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 infoTextBox.Text = sb.ToString(); } #endregion // Info view #region Tools items private void hexDumpToolStripMenuItem_Click(object sender, EventArgs e) { OpenFileDialog fileDlg = new OpenFileDialog(); fileDlg.Filter = Properties.Resources.FILE_FILTER_ALL; fileDlg.FilterIndex = 1; if (fileDlg.ShowDialog() != DialogResult.OK) { return; } string fileName = fileDlg.FileName; FileInfo fi = new FileInfo(fileName); if (fi.Length > Tools.HexDumpViewer.MAX_LENGTH) { string msg = string.Format(Properties.Resources.ERR_FILE_TOO_LARGE, Tools.HexDumpViewer.MAX_LENGTH); MessageBox.Show(this, msg, Properties.Resources.OPEN_DATA_FAIL_CAPTION, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } byte[] data; try { data = File.ReadAllBytes(fileName); } catch (Exception ex) { MessageBox.Show(this, ex.Message); return; } // Fire and forget. Tools.HexDumpViewer dlg = new Tools.HexDumpViewer(data, mOutputFormatter); dlg.Text = Path.GetFileName(fileName); dlg.Show(); } #endregion Tools items #region Debug menu items private void dEBUGToolStripMenuItem_DropDownOpened(object sender, EventArgs e) { toggleOwnerDrawToolStripMenuItem.Checked = codeListView.OwnerDraw; toggleCommentRulersToolStripMenuItem.Checked = MultiLineComment.DebugShowRuler; useKeepAliveHackToolStripMenuItem.Checked = ScriptManager.UseKeepAliveHack; } private void reanalyzeToolStripMenuItem_Click(object sender, EventArgs e) { Debug.WriteLine("Reanalyzing..."); // Call through ApplyChanges so we update the timer task output. UndoableChange uc = UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.CodeAndData); ApplyChanges(new ChangeSet(uc), false); UpdateMenuItemsAndTitle(); // in case something changed } private Tools.ShowText mShowUndoRedoHistoryDialog; private void showUndoRedoHistoryToolStripMenuItem_Click(object sender, EventArgs e) { // Show or hide the modeless dialog. if (mShowUndoRedoHistoryDialog == null) { Tools.ShowText dlg = new Tools.ShowText(); dlg.Title = "Undo/Redo History"; dlg.BodyText = mProject.DebugGetUndoRedoHistory(); dlg.OnWindowClosing += (sender1, e1) => { Debug.WriteLine("Undo/redo dialog closed"); showUndoRedoHistoryToolStripMenuItem.Checked = false; mShowUndoRedoHistoryDialog = null; }; dlg.Show(); mShowUndoRedoHistoryDialog = dlg; showUndoRedoHistoryToolStripMenuItem.Checked = true; } else { // Ask the dialog to close. Do the cleanup in the event. mShowUndoRedoHistoryDialog.Close(); } } private Tools.ShowText mShowAnalyzerOutputDialog; private void showAnalyzerOutputToolStripMenuItem_Click(object sender, EventArgs e) { // Show or hide the modeless dialog. if (mShowAnalyzerOutputDialog == null) { Tools.ShowText dlg = new Tools.ShowText(); dlg.Title = "Analyzer Output"; if (mGenerationLog == null) { dlg.BodyText = "(no data yet)"; } else { dlg.BodyText = mGenerationLog.WriteToString(); } dlg.OnWindowClosing += (sender1, e1) => { Debug.WriteLine("Analyzer output dialog closed"); showAnalyzerOutputToolStripMenuItem.Checked = false; mShowAnalyzerOutputDialog = null; }; dlg.Show(); mShowAnalyzerOutputDialog = dlg; showAnalyzerOutputToolStripMenuItem.Checked = true; } else { // Ask the dialog to close. Do the cleanup in the event. mShowAnalyzerOutputDialog.Close(); } } private Tools.ShowText mShowAnalysisTimersDialog; private void showAnalysisTimersToolStripMenuItem_Click(object sender, EventArgs e) { // Show or hide the modeless dialog. if (mShowAnalysisTimersDialog == null) { Tools.ShowText dlg = new Tools.ShowText(); dlg.Title = "Analysis Timers"; dlg.BodyText = "(no data yet)"; dlg.OnWindowClosing += (sender1, e1) => { Debug.WriteLine("Analysis timers dialog closed"); showAnalysisTimersToolStripMenuItem.Checked = false; mShowAnalysisTimersDialog = null; }; dlg.Show(); mShowAnalysisTimersDialog = dlg; showAnalysisTimersToolStripMenuItem.Checked = true; } else { // Ask the dialog to close. Do the cleanup in the event. mShowAnalysisTimersDialog.Close(); } } private void toggleOwnerDrawToolStripMenuItem_Click(object sender, EventArgs e) { Debug.WriteLine("TOGGLE OWNERDRAW"); bool newState = !codeListView.OwnerDraw; codeListView.OwnerDraw = newState; notesListView.OwnerDraw = newState; } private void toggleCommentRulersToolStripMenuItem_Click(object sender, EventArgs e) { MultiLineComment.DebugShowRuler = !MultiLineComment.DebugShowRuler; // Don't need to repeat the analysis, but we do want to save/restore the // selection and top position when the comment fields change size. UndoableChange uc = UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DataOnly); ApplyChanges(new ChangeSet(uc), false); } private void useKeepAliveHackToolStripMenuItem_Click(object sender, EventArgs e) { ScriptManager.UseKeepAliveHack = !ScriptManager.UseKeepAliveHack; } private void sourceGenTestsToolStripMenuItem_Click(object sender, EventArgs e) { Tests.GenTestRunner dlg = new Tests.GenTestRunner(); dlg.ShowDialog(); dlg.Dispose(); } private void extensionScriptInfoToolStripMenuItem_Click(object sender, EventArgs e) { string info = mProject.DebugGetLoadedScriptInfo(); Tools.ShowText dlg = new Tools.ShowText(); dlg.Title = "Loaded Extension Script Info"; dlg.BodyText = info; dlg.ShowDialog(); } #endregion Debug menu items } }