1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-08-07 20:28:55 +00:00
6502bench/SourceGen/AppForms/ProjectView.cs
Andy McFadden 60aa252352 Improve chances of running under Mono
Updated the RuntimeData directory finder to work, and made the
stuff that crashed when the directory wasn't found crash in less
obvious ways.  Under Mono+Linux it still falls over with some
complaints about ListViews.  This will need some work.
2018-10-01 10:28:03 -07:00

4374 lines
189 KiB
C#

/*
* 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 {
/// <summary>
/// Main application form. This is the top-level application object.
/// </summary>
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;
/// <summary>
/// Symbol subset, used to supply data to the symbol ListView. Initialized with
/// an empty symbol table.
/// </summary>
private SymbolTableSubset mSymbolSubset = new SymbolTableSubset(new SymbolTable());
/// <summary>
/// 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.
/// </summary>
private VirtualListViewSelection mCodeViewSelection = new VirtualListViewSelection();
/// <summary>
/// Data backing the codeListView.
/// </summary>
private DisplayList mDisplayList;
#endregion Project state
/// <summary>
/// Returns the font currently in use by the code ListView.
/// </summary>
public Font CodeListViewFont { get { return codeListView.Font; } }
/// <summary>
/// List of recently-opened projects.
/// </summary>
private List<string> mRecentProjectPaths = new List<string>(MAX_RECENT_PROJECTS);
private const int MAX_RECENT_PROJECTS = 6;
/// <summary>
/// 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.
/// </summary>
private ToolStripItem[] mActionsMenuItems;
/// <summary>
/// Activity log generated by the code and data analyzers. Displayed in window.
/// </summary>
private DebugLog mGenerationLog;
/// <summary>
/// Timing data generated during analysis.
/// </summary>
TaskTimer mReanalysisTimer = new TaskTimer();
/// <summary>
/// Base control to show when no project is open.
/// </summary>
private Control mNoProjectControl;
/// <summary>
/// Base control to show when project is open.
/// </summary>
private Control mProjectControl;
/// <summary>
/// Performance hack.
/// </summary>
private bool mRestoringSelection;
/// <summary>
/// Stack for navigate forward/backward.
/// </summary>
private NavStack mNavStack = new NavStack();
/// <summary>
/// Output format configuration.
/// </summary>
private Formatter.FormatConfig mFormatterConfig;
/// <summary>
/// Output format controller.
///
/// This is shared with the DisplayList.
/// </summary>
private Formatter mOutputFormatter;
/// <summary>
/// Pseudo-op names.
///
/// This is shared with the DisplayList.
/// </summary>
private PseudoOp.PseudoOpNames mPseudoOpNames;
/// <summary>
/// String we most recently searched for.
/// </summary>
private string mFindString = string.Empty;
/// <summary>
/// Initial start point of most recent search.
/// </summary>
private int mFindStartIndex = -1;
/// <summary>
/// Used to highlight the line that is the target of the selected line.
/// </summary>
private int mTargetHighlightIndex = -1;
/// <summary>
/// CPU definition used when the Formatter was created. If the CPU choice or
/// inclusion of undocumented opcodes changes, we need to wipe the formatter.
/// </summary>
private CpuDef mOutputFormatterCpuDef;
/// <summary>
/// Instruction description object. Used for Info window.
/// </summary>
private OpDescription mOpDesc = OpDescription.GetOpDescription(null);
/// <summary>
/// If true, plugins will execute in the main application's AppDomain instead of
/// the sandbox.
/// </summary>
private bool mUseMainAppDomainForPlugins = false;
/// <summary>
/// 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).
/// </summary>
private Tools.HexDumpViewer mHexDumpDialog;
/// <summary>
/// Floating ASCII chart dialog. Not tied to the project.
/// </summary>
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(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(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];
}
// Init primary ListView (virtual, ownerdraw)
InitCodeListView();
// Init Symbols ListView (virtual, non-ownerdraw)
symbolListView.SetDoubleBuffered(true);
InitSymbolListView();
LoadAppSettings();
ApplyAppSettings();
// Init References ListView (non-virtual, non-ownerdraw)
referencesListView.SetDoubleBuffered(true);
UpdateActionMenu();
UpdateMenuItemsAndTitle();
UpdateRecentLinks();
ShowNoProject();
}
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;
// 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);
}
/// <summary>
/// Loads settings from the settings file into AppSettings.Global. Does not apply
/// them to the ProjectView.
/// </summary>
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);
}
}
/// <summary>
/// Saves AppSettings to a file.
/// </summary>
private void SaveAppSettings() {
if (!AppSettings.Global.Dirty) {
Debug.WriteLine("Settings not dirty, not saving");
return;
}
// Collect some window and column widths. Don't grab the window size if we're
// maximized or minimized.
if (this.WindowState == FormWindowState.Normal) {
AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_WIDTH, this.Size.Width);
AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_HEIGHT, this.Size.Height);
AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_SPLITTER_DIST,
mainSplitterLeft.SplitterDistance);
AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_SPLITTER_DIST,
mainSplitterRight.SplitterDistance);
AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_SIDE_SPLITTER_DIST,
leftPanelSplitter.SplitterDistance);
AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_SIDE_SPLITTER_DIST,
rightPanelSplitter.SplitterDistance);
}
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);
}
}
/// <summary>
/// Replaces the contents of the global settings object with the new settings,
/// then applies them to the ProjectView.
/// </summary>
/// <param name="settings"></param>
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();
}
/// <summary>
/// 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.
/// </summary>
private void ApplyAppSettings() {
Debug.WriteLine("ApplyAppSettings...");
AppSettings settings = AppSettings.Global;
// Main window size.
this.Size = new Size(
settings.GetInt(AppSettings.MAIN_WINDOW_WIDTH, 1280),
settings.GetInt(AppSettings.MAIN_WINDOW_HEIGHT, 720));
// Left splitter with is distance from left edge of window.
mainSplitterLeft.SplitterDistance =
settings.GetInt(AppSettings.MAIN_LEFT_SPLITTER_DIST, 250);
// Right splitter posn is distance from right edge of left splitter.
mainSplitterRight.SplitterDistance =
settings.GetInt(AppSettings.MAIN_RIGHT_SPLITTER_DIST, (1280 - 250) - 250);
leftPanelSplitter.SplitterDistance =
settings.GetInt(AppSettings.MAIN_LEFT_SIDE_SPLITTER_DIST, 350);
rightPanelSplitter.SplitterDistance =
settings.GetInt(AppSettings.MAIN_RIGHT_SIDE_SPLITTER_DIST, 400);
// 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();
// 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. We don't catch size-chage events
// for the left/right splitter widths because that should be picked up by the sub-windows.
private void ProjectView_SizeChanged(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;
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;
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<List<string>>(cereal);
} catch (Exception ex) {
Debug.WriteLine("Failed deserializing recent projects: " + ex.Message);
return;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="projectPath"></param>
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();
}
/// <summary>
/// Updates the links on the no-project control.
/// </summary>
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(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);
}
/// <summary>
/// Loads the data file, reading it entirely into memory.
///
/// All errors are reported as exceptions.
/// </summary>
/// <param name="dataFileName">Full pathname.</param>
/// <returns>Data file contents.</returns>
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;
}
/// <summary>
/// Applies the changes to the project, adds them to the undo stack, and updates
/// the display.
/// </summary>
/// <param name="cs">Set of changes to apply.</param>
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();
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="cs">Set of changes to apply.</param>
/// <param name="backward">If set, undo the changes instead.</param>
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;
}
}
/// <summary>
/// Refreshes the project after something of substance has changed. Some
/// re-analysis will be done, followed by a complete rebuild of the DisplayList.
/// </summary>
/// <param name="reanalysisRequired">Indicates whether reanalysis is required, and
/// what level.</param>
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 +
"%)");
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="offsetSet"></param>
private void RefreshCodeListViewEntries(RangeSet offsetSet) {
IEnumerator<RangeSet.Range> 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");
}
#endregion Project management
#region Main window UI event handlers
/// <summary>
/// Invalidates and forces an update on our various windows.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="curSel">Selection bits for the current display list.</param>
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) {
// 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);
}
/// <summary>
/// Determines whether any part of the specified offset is currently visible in the
/// code list view.
/// </summary>
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();
}
/// <summary>
/// Handles opening an existing project by letting the user select the project file.
/// </summary>
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);
}
/// <summary>
/// Handles opening an existing project, given a pathname to the project file.
/// </summary>
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(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();
}
/// <summary>
/// Finds and loads the specified data file. The file's length and CRC must match
/// the project's expectations.
/// </summary>
/// <param name="dataPathName">Full path to file.</param>
/// <param name="proj">Project object.</param>
/// <param name="cancel">Returns true if we want to cancel the attempt.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Displays a "do you want to pick a different file" message, then (on OK) allows the
/// user to select a file.
/// </summary>
/// <param name="origPath">Pathname of original file.</param>
/// <param name="errorMsg">Message to display in the message box.</param>
/// <returns>Full path of file to open.</returns>
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(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");
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>True if the project was closed, false if the user chose to cancel.</returns>
private bool DoClose() {
Debug.WriteLine("ProjectView.DoClose() - dirty=" +
(mProject == null ? "N/A" : mProject.IsDirty.ToString()));
if (mProject != null && mProject.IsDirty) {
DialogResult result = MessageBox.Show(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(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(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 > Settings...
private void settingsToolStripMenuItem_Click(object sender, EventArgs e) {
ShowAppSettings(EditAppSettings.Tab.Unknown);
}
/// <summary>
/// Opens the app settings dialog.
/// </summary>
/// <param name="initialTab">Tab to present to the user.</param>
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) {
EditOperand_Click(sender, e);
} else if (editDataFormatToolStripMenuItem.Enabled) {
EditData_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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="gotoOffset">Offset to jump to.</param>
/// <param name="doPush">If set, push new offset onto navigation stack.</param>
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();
}
}
/// <summary>
/// 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.
/// </summary>
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;
// 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);
editOperandToolStripMenuItem.Enabled =
(lineType == DisplayList.Line.Type.Code &&
mProject.GetAnattrib(line.FileOffset).IsInstructionWithOperand);
editDataFormatToolStripMenuItem.Enabled =
(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;
editOperandToolStripMenuItem.Enabled = 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.
editDataFormatToolStripMenuItem.Enabled = 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.
editDataFormatToolStripMenuItem.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;
}
/// <summary>
/// Entity count collection, for GatherEntityCounts.
/// </summary>
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;
};
/// <summary>
/// Gathers a count of different line types and offset hinting.
/// </summary>
/// <param name="singleLineIndex">If a single line is selected, pass the index in.
/// Otherwise, pass -1 to traverse the entire line list.</param>
/// <returns></returns>
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
};
}
/// <summary>
/// Determines whether the current selection spans a single item. This could be a
/// single-line item or a multi-line item.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Handles an "opening" event for the codeListView's ContextMenuStrip.
///
/// This puts all of the Actions menu items in the pop-up context menu.
/// </summary>
private void codeListView_CmsOpening(object sender, CancelEventArgs e) {
codeListView.ContextMenuStrip.Items.AddRange(mActionsMenuItems);
e.Cancel = false;
}
/// <summary>
/// Handles an "opening" event for the ProjectView's Actions menu.
///
/// This puts all of the Actions menu items in the Actions menu.
/// </summary>
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();
}
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<TypedRangeSet.Tuple> iter =
(IEnumerator<TypedRangeSet.Tuple>) 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 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<int, FormatDescriptor> newFmts = new SortedList<int, FormatDescriptor>();
IEnumerator<TypedRangeSet.TypedRange> 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);
}
private void MarkAsData_Click(Object sender, EventArgs e) {
MarkAsType(CodeAnalysis.TypeHint.Data);
}
private void MarkAsInlineData_Click(Object sender, EventArgs e) {
MarkAsType(CodeAnalysis.TypeHint.InlineData);
}
private void MarkAsNoHint_Click(Object sender, EventArgs e) {
MarkAsType(CodeAnalysis.TypeHint.NoHint);
}
private void MarkAsType(CodeAnalysis.TypeHint hint) {
RangeSet 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();
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>RangeSet with all offsets.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>TypedRangeSet with all offsets.</returns>
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
}
/// <summary>
/// Handy class for collecting column widths for the code list view.
/// </summary>
public class CodeListColumnWidths {
public const int NUM_COLUMNS = (int)ColumnIndex.Comment + 1;
/// <summary>
/// Primary storage for column widths.
/// </summary>
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();
}
}
/// <summary>
/// Gets the default column widths for the code list view, based on the currently
/// configured font.
/// </summary>
/// <returns>Column width set.</returns>
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);
}
/// <summary>
/// Saves the code list column widths into AppSettings.
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Configures the column widths.
/// </summary>
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()
};
/// <summary>
/// Cache of previously-constructed ListViewItems. The ListView will request items
/// continuously as they are moused-over, so this is fairly important.
/// </summary>
private ListViewItem[] mItemCache;
private int mItemCacheFirst;
/// <summary>
/// Clears the contents of the ListViewItem cache. Do this whenever the backing
/// store is updated.
/// </summary>
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
/// <summary>
/// Cache of previously-constructed ListViewItems. The ListView will request items
/// continuously as they are moused-over, so this is fairly important.
/// </summary>
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] = symbolNameColumnHeader.Text;
mSymbolColumnHeaderNames[2] = symbolValueColumnHeader.Text;
SetSymbolColumnHeaders();
}
/// <summary>
/// Clears the contents of the ListViewItem cache. Do this whenever the backing
/// store is updated.
/// </summary>
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, sym.Label);
mSymbolSubArray[1] = new ListViewItem.ListViewSubItem(lvi,
mOutputFormatter.FormatHexValue(sym.Value, 0));
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");
}
}
/// <summary>
/// Sets the ListView column headers, adding a glyph to show sort direction.
/// Sadly, this is significantly easier than adding a graphic.
/// </summary>
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];
symbolNameColumnHeader.Text =
(sortCol == SymbolTableSubset.SortCol.Name ? sortStr : "") +
mSymbolColumnHeaderNames[1];
symbolValueColumnHeader.Text =
(sortCol == SymbolTableSubset.SortCol.Value ? 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];
/// <summary>
/// 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.
/// </summary>
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.
/// <summary>
/// Cache of previously-constructed ListViewItems. The ListView will request items
/// continuously as they are moused-over, so this is fairly important.
/// </summary>
private ListViewItem[] mNotesItemCache;
private int mNotesItemCacheFirst;
// Temporary array, used during ListViewItem creation.
private ListViewItem.ListViewSubItem[] mNotesSubArray =
new ListViewItem.ListViewSubItem[1];
/// <summary>
/// Clears the contents of the ListViewItem cache. Do this whenever the backing
/// store is updated.
/// </summary>
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
/// <summary>
/// Updates the Info window for the current selection.
/// </summary>
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(msg, Properties.Resources.OPEN_DATA_FAIL_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
byte[] data;
try {
data = File.ReadAllBytes(fileName);
} catch (Exception ex) {
MessageBox.Show(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
}
}