1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-19 06:31:02 +00:00
6502bench/SourceGen/AppForms/ProjectView.cs
Andy McFadden 4f9af30455 Progress toward new assembler configuration
Rather than have each assembler get its own app config string for
the cross-assembler executable, we now have a collection of per-
assembler config items, of which the executable path name is one
member.  The Asm Config tab has an auto-generated pop-up to select
the assembler.

The per-assembler settings block is serialized with the rather
unpleasant JSON-in-JSON approach, but nobody should have to look
at it.

This also adds assembler-specific column widths to the settings
dialog, though they aren't actually used yet.
2018-10-20 20:35:32 -07:00

4630 lines
201 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;
/// <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>
/// Set to true if the last key hit was Ctrl+H.
/// </summary>
private bool mCtrlHSeen;
/// <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) {
// It's *really* unstable right now, so actively discourage its use.
if (Type.GetType("Mono.Runtime") != null) {
MessageBox.Show(this,
"WARNING: SourceGen is currently unstable under Mono. " +
"Many features are badly broken. Proceed at your own risk.",
"Danger! Danger!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
if (RuntimeDataAccess.GetDirectory() == null) {
MessageBox.Show(this, Properties.Resources.RUNTIME_DIR_NOT_FOUND,
Properties.Resources.RUNTIME_DIR_NOT_FOUND_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
return;
}
try {
PluginDllCache.PreparePluginDir();
} catch (Exception ex) {
string pluginPath = PluginDllCache.GetPluginDirPath();
if (pluginPath == null) {
pluginPath = "<???>";
}
string msg = string.Format(Properties.Resources.PLUGIN_DIR_FAIL,
pluginPath + ": " + ex.Message);
MessageBox.Show(this, msg, Properties.Resources.PLUGIN_DIR_FAIL_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
return;
}
logoPictureBox.ImageLocation = RuntimeDataAccess.GetPathName(LOGO_FILE_NAME);
versionLabel.Text = string.Format(Properties.Resources.VERSION_FMT,
Program.ProgramVersion);
toolStripStatusLabel.Text = Properties.Resources.STATUS_READY;
mProjectControl = this.codeListView;
mNoProjectControl = this.noProjectPanel;
// Clone the menu structure from the designer. The same items are used for
// both Edit > Actions and the right-click context menu in codeListView.
mActionsMenuItems = new ToolStripItem[actionsToolStripMenuItem.DropDownItems.Count];
for (int i = 0; i < actionsToolStripMenuItem.DropDownItems.Count; i++) {
mActionsMenuItems[i] = actionsToolStripMenuItem.DropDownItems[i];
}
// Load the settings from the file. Some things (like the symbol subset) need
// these. The general "apply settings" doesn't happen until a bit later, after
// the sub-windows have been initialized.
LoadAppSettings();
// Init primary ListView (virtual, ownerdraw)
InitCodeListView();
// Init Symbols ListView (virtual, non-ownerdraw)
mSymbolSubset = new SymbolTableSubset(new SymbolTable());
symbolListView.SetDoubleBuffered(true);
InitSymbolListView();
// Init References ListView (non-virtual, non-ownerdraw)
referencesListView.SetDoubleBuffered(true);
// Place the main window and apply the various settings.
SetAppWindowLocation();
ApplyAppSettings();
UpdateActionMenu();
UpdateMenuItemsAndTitle();
UpdateRecentLinks();
ShowNoProject();
ProcessCommandLine();
}
private void ProcessCommandLine() {
string[] args = Environment.GetCommandLineArgs();
if (args.Length == 2) {
DoOpenFile(Path.GetFullPath(args[1]));
}
}
private void InitCodeListView() {
ListView cv = codeListView;
//cv.View = View.Details;
// Create an empty place-holder for the context menu.
codeListView.ContextMenuStrip = new ContextMenuStrip();
// When the Actions or context menu are opened, all menu items get transferred over.
codeListView.ContextMenuStrip.Opening += codeListView_CmsOpening;
// When the context menu closes, move all the menu items back to the Actions menu,
// so that you can open it with Alt-A.
codeListView.ContextMenuStrip.Closed += ActionsMenuOpening;
// Set default widths, in case we don't have a value for this in AppSettings.
CodeListColumnWidths widths = GetDefaultCodeListColumnWidths();
SetCodeListHeaderWidths(widths);
// This gets invoked when the user starts typing characters while the ListView
// has focus. Useful in a file viewer to find something by the first few
// characters. Less useful for us, though we could potentially use it as a
// shortcut to jump to an offset or label.
//codeListView.SearchForVirtualItem +=
// new SearchForVirtualItemEventHandler(codeListView_SearchForVirtualItem);
// Can't simply set this to "true", because the member is protected.
codeListView.SetDoubleBuffered(true);
}
/// <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 widths. Don't grab the main window size if we're
// maximized or minimized.
if (this.WindowState == FormWindowState.Normal ||
this.WindowState == FormWindowState.Maximized) {
if (this.WindowState == FormWindowState.Normal) {
AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_LOC_X, this.Location.X);
AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_LOC_Y, this.Location.Y);
AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_WIDTH, this.Size.Width);
AppSettings.Global.SetInt(AppSettings.MAIN_WINDOW_HEIGHT, this.Size.Height);
}
AppSettings.Global.SetBool(AppSettings.MAIN_WINDOW_MAXIMIZED,
this.WindowState == FormWindowState.Maximized);
// Horizontal splitters. We want to record the panel widths, rather than the
// splitter distance, because otherwise the right panel doesn't keep its size
// when the middle section changes during maximization.
AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH,
mainSplitterLeft.SplitterDistance);
AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH,
this.Size.Width - (mainSplitterLeft.SplitterDistance +
mainSplitterRight.SplitterDistance));
// Vertical splitters.
AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_SIDE_SPLITTER_DIST,
leftPanelSplitter.SplitterDistance);
AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_SIDE_SPLITTER_DIST,
rightPanelSplitter.SplitterDistance);
} else {
// VisualStudio appears to un-minimize its window before closing it. We
// could probably do that and grab the size values on the way out.
AppSettings.Global.SetBool(AppSettings.MAIN_WINDOW_MAXIMIZED, false);
}
SerializeCodeListColumnWidths();
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>
/// Sets the app window's location and size.
/// </summary>
private void SetAppWindowLocation() {
const int DEFAULT_WIDTH = 1280;
const int DEFAULT_HEIGHT = 720;
const int DEFAULT_SPLIT = 250;
AppSettings settings = AppSettings.Global;
// Main window size.
this.Size = new Size(
settings.GetInt(AppSettings.MAIN_WINDOW_WIDTH, DEFAULT_WIDTH),
settings.GetInt(AppSettings.MAIN_WINDOW_HEIGHT, DEFAULT_HEIGHT));
// Left splitter width is distance from left edge of window.
mainSplitterLeft.SplitterDistance =
settings.GetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH, DEFAULT_SPLIT);
// Right splitter distance is distance from right edge of left splitter, i.e.
// the width of the middle section, *not* the width of the right panel.
// --> splitter_distance = main_width - (left_width + right_width)
int rightSplitWidth = settings.GetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH,
DEFAULT_SPLIT);
mainSplitterRight.SplitterDistance =
this.Size.Width - (mainSplitterLeft.SplitterDistance + rightSplitWidth);
// Vertical splits, e.g. References vs. Notes.
leftPanelSplitter.SplitterDistance =
settings.GetInt(AppSettings.MAIN_LEFT_SIDE_SPLITTER_DIST, 350);
rightPanelSplitter.SplitterDistance =
settings.GetInt(AppSettings.MAIN_RIGHT_SIDE_SPLITTER_DIST, 400);
// Get working area of screen the system is going to display us on. Note that,
// with multiple displays, the X/Y coordinates for the display's upper left may
// be negative.
Screen myScreen = Screen.FromControl(this);
Rectangle scrArea = myScreen.WorkingArea;
// Upper-left corner of our window that positions us in the center.
Point centered = new Point(scrArea.X + (scrArea.Width - this.Size.Width) / 2,
scrArea.Y + (scrArea.Height - this.Size.Height) / 2);
// Get requested location (if any).
Point loc = new Point();
loc.X = settings.GetInt(AppSettings.MAIN_WINDOW_LOC_X, int.MinValue);
loc.Y = settings.GetInt(AppSettings.MAIN_WINDOW_LOC_Y, int.MinValue);
if (loc.X == int.MinValue || loc.Y == int.MinValue) {
// No setting exists; center it on the screen.
loc = centered;
} else {
// See if this location makes sense. As a quick sanity check we test to see
// if the top-center part of the window is visible. If not, it may not be
// possible to reposition the window because the title bar won't be visible.
//
// Win10 seems to be okay with dragging a window below the icon bar at the
// bottom, which makes it hard to get at. We offset the Y position by a few
// pixels to give us some wiggle room.
Point checkPoint = new Point(loc.X + this.Size.Width / 2, loc.Y + 8);
if (!scrArea.Contains(checkPoint)) {
Debug.WriteLine("Titlebar " + checkPoint + " not inside " + scrArea);
loc = centered;
}
}
this.Location = loc;
if (settings.GetBool(AppSettings.MAIN_WINDOW_MAXIMIZED, false)) {
this.WindowState = FormWindowState.Maximized;
}
// Configure column widths.
string widthStr = settings.GetString(AppSettings.CDLV_COL_WIDTHS, null);
if (!string.IsNullOrEmpty(widthStr)) {
CodeListColumnWidths widths = CodeListColumnWidths.Deserialize(widthStr);
if (widths != null) {
SetCodeListHeaderWidths(widths);
}
}
DeserializeReferencesColumnWidths();
DeserializeNotesColumnWidths();
DeserializeSymbolColumnWidths();
}
/// <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;
// Set up the formatter.
mFormatterConfig = new Formatter.FormatConfig();
AsmGen.GenCommon.ConfigureFormatterFromSettings(AppSettings.Global,
ref mFormatterConfig);
mFormatterConfig.mEndOfLineCommentDelimiter = ";";
mFormatterConfig.mFullLineCommentDelimiterBase = ";";
mFormatterConfig.mBoxLineCommentDelimiter = string.Empty;
mFormatterConfig.mAllowHighAsciiCharConst = true;
mOutputFormatter = new Formatter(mFormatterConfig);
mOutputFormatterCpuDef = null;
// Set pseudo-op names. Entries aren't allowed to be blank, so we start with the
// default values and merge in whatever the user has configured.
mPseudoOpNames = PseudoOp.sDefaultPseudoOpNames.GetCopy();
string pseudoCereal = settings.GetString(AppSettings.FMT_PSEUDO_OP_NAMES, null);
if (!string.IsNullOrEmpty(pseudoCereal)) {
PseudoOp.PseudoOpNames deser = PseudoOp.PseudoOpNames.Deserialize(pseudoCereal);
if (deser != null) {
mPseudoOpNames.Merge(deser);
}
}
// Configure the Symbols window.
symbolUserCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_USER, false);
symbolAutoCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_AUTO, false);
symbolProjectCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_PROJECT, false);
symbolPlatformCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false);
symbolConstantCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_CONST, false);
symbolAddressCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR, false);
// Set the code list view font.
string fontStr = settings.GetString(AppSettings.CDLV_FONT, null);
if (!string.IsNullOrEmpty(fontStr)) {
FontConverter cvt = new FontConverter();
try {
Font font = cvt.ConvertFromInvariantString(fontStr) as Font;
codeListView.Font = font;
Debug.WriteLine("Set font to " + font.ToString());
} catch (Exception ex) {
Debug.WriteLine("Font convert failed: " + ex.Message);
}
}
// Unpack the recent-project list.
UnpackRecentProjectList();
// Enable the DEBUG menu if configured.
bool showDebugMenu = AppSettings.Global.GetBool(AppSettings.DEBUG_MENU_ENABLED, false);
if (dEBUGToolStripMenuItem.Visible != showDebugMenu) {
dEBUGToolStripMenuItem.Visible = showDebugMenu;
mainMenuStrip.Refresh();
}
// Finally, update the display list with all the fancy settings.
if (mDisplayList != null) {
// Regenerate the display list with the latest formatter config and
// pseudo-op definition. (These are set as part of the refresh.)
UndoableChange uc =
UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DisplayOnly);
ApplyChanges(new ChangeSet(uc), false);
}
}
// Make sure we pick up changes to the window size or position. We don't catch
// size-chage events for the left/right splitter widths because that should be picked
// up by events on sub-windows.
private void ProjectView_SizeOrLocChanged(object sender, EventArgs e) {
AppSettings.Global.Dirty = true;
}
// This handles the splitters in the side panels, e.g. between References and Notes.
private void SidePanelSplitter_SplitterMoved(object sender, SplitterEventArgs e) {
AppSettings.Global.Dirty = true;
}
public void ShowProject() {
mProjectControl.Show();
mNoProjectControl.Hide();
codeListView.Focus();
saveToolStripMenuItem.Enabled = true;
saveToolStripButton.Enabled = true;
saveAsToolStripMenuItem.Enabled = true;
closeToolStripMenuItem.Enabled = true;
assembleToolStripMenuItem.Enabled = true;
printToolStripMenuItem.Enabled = true;
copyToolStripMenuItem.Enabled = true;
copyToolStripButton.Enabled = true;
selectAllToolStripMenuItem.Enabled = true;
findToolStripMenuItem.Enabled = true;
findNextToolStripMenuItem.Enabled = true;
gotoToolStripMenuItem.Enabled = true;
editHeaderCommentToolStripMenuItem.Enabled = true;
projectPropertiesToolStripMenuItem.Enabled = true;
toggleDataScanToolStripMenuItem.Enabled = true;
showUndoRedoHistoryToolStripMenuItem.Enabled = true;
showAnalysisTimersToolStripMenuItem.Enabled = true;
showAnalyzerOutputToolStripMenuItem.Enabled = true;
toggleOwnerDrawToolStripMenuItem.Enabled = true;
reanalyzeToolStripMenuItem.Enabled = true;
toggleCommentRulersToolStripMenuItem.Enabled = true;
extensionScriptInfoToolStripMenuItem.Enabled = true;
mNavStack.Clear();
UpdateMenuItemsAndTitle();
}
public void ShowNoProject() {
mProjectControl.Hide();
mNoProjectControl.Show();
saveToolStripMenuItem.Enabled = false;
saveToolStripButton.Enabled = false;
saveAsToolStripMenuItem.Enabled = false;
closeToolStripMenuItem.Enabled = false;
assembleToolStripMenuItem.Enabled = false;
printToolStripMenuItem.Enabled = false;
copyToolStripMenuItem.Enabled = false;
copyToolStripButton.Enabled = false;
selectAllToolStripMenuItem.Enabled = false;
findToolStripMenuItem.Enabled = false;
findNextToolStripMenuItem.Enabled = false;
gotoToolStripMenuItem.Enabled = false;
editHeaderCommentToolStripMenuItem.Enabled = false;
projectPropertiesToolStripMenuItem.Enabled = false;
toggleDataScanToolStripMenuItem.Enabled = false;
showUndoRedoHistoryToolStripMenuItem.Enabled = false;
showAnalysisTimersToolStripMenuItem.Enabled = false;
showAnalyzerOutputToolStripMenuItem.Enabled = false;
toggleOwnerDrawToolStripMenuItem.Enabled = false;
reanalyzeToolStripMenuItem.Enabled = false;
toggleCommentRulersToolStripMenuItem.Enabled = false;
extensionScriptInfoToolStripMenuItem.Enabled = false;
UpdateMenuItemsAndTitle();
}
private void UnpackRecentProjectList() {
mRecentProjectPaths.Clear();
string cereal = AppSettings.Global.GetString(
AppSettings.PRVW_RECENT_PROJECT_LIST, null);
if (string.IsNullOrEmpty(cereal)) {
return;
}
try {
JavaScriptSerializer ser = new JavaScriptSerializer();
mRecentProjectPaths = ser.Deserialize<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(this, caption, message, MessageBoxButtons.OK,
MessageBoxIcon.Error);
return false;
}
proj.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins;
proj.Initialize(fileData.Length);
proj.PrepForNew(fileData, Path.GetFileName(dataPathName));
proj.LongComments.Add(DisplayList.Line.HEADER_COMMENT_OFFSET,
new MultiLineComment("6502bench SourceGen v" + Program.ProgramVersion));
// The system definition provides a set of defaults that can be overridden.
// We pull everything of interest out and then discard the object.
proj.ApplySystemDef(sysDef);
mProject = proj;
return true;
}
private void FinishPrep() {
string messages = mProject.LoadExternalFiles();
if (messages.Length != 0) {
// ProjectLoadIssues isn't quite the right dialog, but it'll do.
ProjectLoadIssues dlg = new ProjectLoadIssues();
dlg.CanCancel = false;
dlg.Messages = messages;
dlg.ShowDialog();
dlg.Dispose();
}
mDisplayList = new DisplayList(mProject, mOutputFormatter, mPseudoOpNames);
// Prep the symbol table subset object. Replace the old one with a new one.
mSymbolSubset = new SymbolTableSubset(mProject.SymbolTable);
RefreshProject(UndoableChange.ReanalysisScope.CodeAndData);
ShowProject();
InvalidateControls(null);
// Want to do this after ShowProject() or we see a weird glitch.
UpdateRecentProjectList(mProjectPathName);
}
/// <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;
}
// Lines may have moved around. Update the selection highlight. It's important
// we do it here, and not down in DoRefreshProject(), because at that point the
// ListView's selection index could be referencing a line off the end.
UpdateSelectionHighlight();
}
/// <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) {
// Handle the Ctrl+H sequence.
if (mCtrlHSeen) {
if (keyData == (Keys.Control | Keys.C)) {
if (hintAsCodeToolStripMenuItem.Enabled) {
MarkAsCode_Click(null, null);
}
} else if (keyData == (Keys.Control | Keys.D)) {
if (hintAsDataToolStripMenuItem.Enabled) {
MarkAsData_Click(null, null);
}
} else if (keyData == (Keys.Control | Keys.I)) {
if (hintAsInlineDataToolStripMenuItem.Enabled) {
MarkAsInlineData_Click(null, null);
}
} else if (keyData == (Keys.Control | Keys.R)) {
if (removeHintToolStripMenuItem.Enabled) {
MarkAsNoHint_Click(null, null);
}
} else {
System.Media.SystemSounds.Beep.Play();
}
mCtrlHSeen = false;
toolStripStatusLabel.Text = Properties.Resources.STATUS_READY;
return true;
} else if (keyData == (Keys.Control | Keys.H)) {
mCtrlHSeen = true;
toolStripStatusLabel.Text = Properties.Resources.STATUS_CTRL_H_HIT;
return true;
}
// Ctrl+Shift+Z is an alias for Redo (Ctrl+Y).
if (keyData == (Keys.Control | Keys.Shift | Keys.Z)) {
if (redoToolStripMenuItem.Enabled) {
redoToolStripMenuItem_Click(null, null);
}
return true;
}
// Navigation keys. Alt+left/right is intuitive key binding used by Eclipse,
// Ctrl+[shift]+minus is weird Visual Studio binding.
if (keyData == (Keys.Alt | Keys.Left) ||
keyData == (Keys.Control | Keys.OemMinus)) {
if (navigateBackToolStripButton.Enabled) {
navigateBackToolStripButton_Click(null, null);
}
return true;
}
if (keyData == (Keys.Alt | Keys.Right) ||
keyData == (Keys.Control | Keys.Shift | Keys.OemMinus)) {
if (navigateFwdToolStripButton.Enabled) {
navigateFwdToolStripButton_Click(null, null);
}
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void navigateBackToolStripButton_Click(object sender, EventArgs e) {
if (!mNavStack.HasBackward) {
// toolbar button should have been disabled
return;
}
int backOff = mNavStack.Pop();
UpdateMenuItemsAndTitle();
Debug.WriteLine("Nav back: +" + backOff.ToString("x6"));
GoToOffset(backOff, false, false);
}
private void navigateFwdToolStripButton_Click(object sender, EventArgs e) {
if (!mNavStack.HasForward) {
// toolbar button should have been disabled
return;
}
int fwdOff = mNavStack.PushPrevious();
UpdateMenuItemsAndTitle();
Debug.WriteLine("Nav fwd: +" + fwdOff.ToString("x6"));
GoToOffset(fwdOff, false, false);
}
/// <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(this, msg, Properties.Resources.ERR_FILE_GENERIC_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
DisasmProject newProject = new DisasmProject();
newProject.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins;
// Deserialize the project file. I want to do this before loading the data file
// in case we decide to store the data file name in the project (e.g. the data
// file is a disk image or zip archive, and we need to know which part(s) to
// extract).
if (!ProjectFile.DeserializeFromFile(projPathName, newProject,
out FileLoadReport report)) {
// Should probably use a less-busy dialog for something simple like
// "permission denied", but the open file dialog handles most simple
// stuff directly.
ProjectLoadIssues dlg = new ProjectLoadIssues();
dlg.Messages = report.Format();
dlg.CanContinue = false;
dlg.ShowDialog();
// ignore dlg.DialogResult
dlg.Dispose();
return;
}
// Now open the data file, generating the pathname by stripping off the ".dis65"
// extension. If we can't find the file, show a message box and offer the option to
// locate it manually, repeating the process until successful or canceled.
const string UNKNOWN_FILE = "UNKNOWN";
string dataPathName;
if (projPathName.Length <= ProjectFile.FILENAME_EXT.Length) {
dataPathName = UNKNOWN_FILE;
} else {
dataPathName = projPathName.Substring(0,
projPathName.Length - ProjectFile.FILENAME_EXT.Length);
}
byte[] fileData;
while ((fileData = FindValidDataFile(ref dataPathName, newProject,
out bool cancel)) == null) {
if (cancel) {
// give up
Debug.WriteLine("Abandoning attempt to open project");
return;
}
}
// If there were warnings, notify the user and give the a chance to cancel.
if (report.Count != 0) {
ProjectLoadIssues dlg = new ProjectLoadIssues();
dlg.Messages = report.Format();
dlg.ShowDialog();
DialogResult result = dlg.DialogResult;
dlg.Dispose();
if (result != DialogResult.OK) {
return;
}
}
mProject = newProject;
mProjectPathName = mProject.ProjectPathName = projPathName;
mProject.SetFileData(fileData, Path.GetFileName(dataPathName));
FinishPrep();
}
/// <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(this,
Properties.Resources.ERR_PROJECT_SAVE_FAIL + ": " + errorMessage,
Properties.Resources.OPERATION_FAILED,
MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
mProject.ResetDirtyFlag();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
UpdateMenuItemsAndTitle();
// Update this, in case this was a new project.
UpdateRecentProjectList(pathName);
// Seems like a good time to save this off too.
SaveAppSettings();
return true;
}
// App is closing.
private void ProjectView_FormClosing(object sender, FormClosingEventArgs e) {
Debug.WriteLine("Main app form closing (reason=" + e.CloseReason + ")");
if (mProjectControl == null) {
// This can be null if something failed during startup, so we're exiting
// the application before the UI is fully up.
return;
}
if (!DoClose()) {
e.Cancel = true;
return;
}
SaveAppSettings();
}
// File > Close
private void closeToolStripMenuItem_Click(object sender, EventArgs e) {
if (!DoClose()) {
Debug.WriteLine("Close canceled");
}
}
/// <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(this, Properties.Resources.UNSAVED_CHANGES,
Properties.Resources.UNSAVED_CHANGES_CAPTION, MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
if (result == DialogResult.Cancel) {
return false;
}
}
// Close modeless dialogs that depend on project.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.Close();
}
if (mShowAnalysisTimersDialog != null) {
mShowAnalysisTimersDialog.Close();
}
if (mShowAnalyzerOutputDialog != null) {
mShowAnalyzerOutputDialog.Close();
}
if (mHexDumpDialog != null) {
mHexDumpDialog.Close();
}
// Discard all project state.
if (mProject != null) {
mProject.Cleanup();
mProject = null;
}
mDataPathName = null;
mProjectPathName = null;
mSymbolSubset = new SymbolTableSubset(new SymbolTable());
mCodeViewSelection = new VirtualListViewSelection();
mDisplayList = null;
codeListView.VirtualListSize = 0;
//codeListView.Items.Clear();
ShowNoProject();
InvalidateControls(null);
mGenerationLog = null;
// Not necessary, but it lets us check the memory monitor to see if we got
// rid of everything.
GC.Collect();
return true;
}
// File > Assemble...
private void assembleToolStripMenuItem_Click(object sender, EventArgs e) {
if (string.IsNullOrEmpty(mProjectPathName)) {
// We need a project pathname so we know where to write the assembler
// source files, and what to call the output files. We could just pop up the
// Save As dialog, but that seems confusing unless we do a custom dialog with
// an explanation, or have some annoying click-through.
//
// This only appears for never-saved projects, not projects with unsaved data.
MessageBox.Show(this, Properties.Resources.SAVE_BEFORE_ASM_TEXT,
Properties.Resources.SAVE_BEFORE_ASM_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
AsmGen.GenAndAsm dlg = new AsmGen.GenAndAsm(this, mProject, mProjectPathName);
dlg.ShowDialog();
}
// File > Exit
private void exitToolStripMenuItem_Click(object sender, EventArgs e) {
// Unsaved-data check happens in form closing event.
Application.Exit();
}
// Edit > Undo, Ctrl+Z (may be called with null/null)
private void undoToolStripMenuItem_Click(object sender, EventArgs e) {
if (!mProject.CanUndo) {
Debug.WriteLine("Nothing to undo");
return;
}
ChangeSet cs = mProject.PopUndoSet();
ApplyChanges(cs, true);
UpdateMenuItemsAndTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
}
// Edit > Redo, Ctrl+Y (may be called with null/null)
private void redoToolStripMenuItem_Click(object sender, EventArgs e) {
if (!mProject.CanRedo) {
Debug.WriteLine("Nothing to redo");
return;
}
ChangeSet cs = mProject.PopRedoSet();
ApplyChanges(cs, false);
UpdateMenuItemsAndTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
}
// Edit > Select All (Ctrl+A)
private void selectAllToolStripMenuItem_Click(object sender, EventArgs e) {
codeListView.SelectAll();
#if false
try {
Application.UseWaitCursor = true;
Cursor.Current = Cursors.WaitCursor;
codeListView.BeginUpdate();
int max = codeListView.VirtualListSize;
for (int i = 0; i < max; i++) {
//codeListView.Items[i].Selected = true;
codeListView.SelectedIndices.Add(i);
if ((i % 50000) == 0) {
toolStripStatusLabel.Text = string.Format(
Properties.Resources.STATUS_SELECTING, (i * 100) / max);
//Application.DoEvents(); // <-- this is unwise
Refresh(); // <-- updates status line but not mouse
}
}
} finally {
codeListView.EndUpdate();
Application.UseWaitCursor = false;
toolStripStatusLabel.Text = Properties.Resources.STATUS_READY;
}
#endif
}
// Edit > Copy (Ctrl+C)
private void copyToolStripMenuItem_Click(object sender, EventArgs e) {
const int AssemblerSource = 0;
const int Disassembly = 1;
const bool addCsv = true;
int format = AppSettings.Global.GetInt(AppSettings.CLIP_LINE_FORMAT, AssemblerSource);
StringBuilder fullText = new StringBuilder(codeListView.SelectedIndices.Count * 50);
StringBuilder csv = new StringBuilder(codeListView.SelectedIndices.Count * 40);
StringBuilder sb = new StringBuilder(100);
int addrAdj = mProject.CpuDef.HasAddr16 ? 6 : 9;
int disAdj = (format != Disassembly) ? 0 : addrAdj + 10;
// Walking through the selected indices can be slow for a large file, so we
// run through the full list and pick out the selected items with our parallel
// structure. (I'm assuming that "select all" will be a common precursor.)
for (int i = 0; i < mDisplayList.Count; i++) {
if (!mCodeViewSelection[i]) {
continue;
}
DisplayList.Line line = mDisplayList[i];
DisplayList.FormattedParts parts = mDisplayList.GetFormattedParts(i);
switch (line.LineType) {
case DisplayList.Line.Type.Code:
case DisplayList.Line.Type.Data:
case DisplayList.Line.Type.EquDirective:
case DisplayList.Line.Type.RegWidthDirective:
case DisplayList.Line.Type.OrgDirective:
if (format == Disassembly) {
if (!string.IsNullOrEmpty(parts.Addr)) {
sb.Append(parts.Addr);
sb.Append(": ");
}
// shorten the "..."
string bytesStr = parts.Bytes;
if (bytesStr != null && bytesStr.Length > 8) {
bytesStr = bytesStr.Substring(0, 8) + "+";
}
TextUtil.AppendPaddedString(sb, bytesStr, disAdj);
}
TextUtil.AppendPaddedString(sb, parts.Label, disAdj + 9);
TextUtil.AppendPaddedString(sb, parts.Opcode, disAdj + 9 + 8);
TextUtil.AppendPaddedString(sb, parts.Operand, disAdj + 9 + 8 + 11);
if (string.IsNullOrEmpty(parts.Comment)) {
// Trim trailing spaces off opcode or operand.
TextUtil.TrimEnd(sb);
} else {
sb.Append(parts.Comment);
}
sb.Append("\r\n");
break;
case DisplayList.Line.Type.LongComment:
if (format == Disassembly) {
TextUtil.AppendPaddedString(sb, string.Empty, disAdj);
}
sb.Append(parts.Comment);
sb.Append("\r\n");
break;
case DisplayList.Line.Type.Note:
// don't include notes
break;
case DisplayList.Line.Type.Blank:
sb.Append("\r\n");
break;
default:
Debug.Assert(false);
break;
}
fullText.Append(sb);
if (addCsv) {
csv.Append(TextUtil.EscapeCSV(parts.Offset)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Addr)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Bytes)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Flags)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Attr)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Label)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Opcode)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Operand)); csv.Append(',');
csv.Append(TextUtil.EscapeCSV(parts.Comment));
csv.Append("\r\n");
}
sb.Clear();
}
// We want to have both plain text and CSV data on the clipboard. To add both
// formats we need to stream it to a DataObject. Complicating matters is Excel's
// entirely reasonable desire to have data in UTF-8 rather than UTF-16.
//
// (I'm not sure pasting assembly bits into Excel is actually useful, so this
// should probably be optional.)
//
// https://stackoverflow.com/a/369219/294248
DataObject dataObject = new DataObject();
dataObject.SetText(fullText.ToString());
if (addCsv) {
byte[] csvData = Encoding.UTF8.GetBytes(csv.ToString());
MemoryStream stream = new MemoryStream(csvData);
dataObject.SetData(DataFormats.CommaSeparatedValue, stream);
}
Clipboard.SetDataObject(dataObject, true);
}
// Edit > Find... (Ctrl+F)
private void findToolStripMenuItem_Click(object sender, EventArgs e) {
FindBox dlg = new FindBox();
dlg.TextToFind = mFindString;
if (dlg.ShowDialog() == DialogResult.OK) {
mFindString = dlg.TextToFind;
mFindStartIndex = -1;
FindText();
}
dlg.Dispose();
}
// Edit > Find Next (F3)
private void findNextToolStripMenuItem_Click(object sender, EventArgs e) {
FindText();
}
private void FindText() {
if (string.IsNullOrEmpty(mFindString)) {
return;
}
int index;
if (codeListView.SelectedIndices.Count > 0) {
index = codeListView.SelectedIndices[0];
} else {
index = 0;
}
// Start one past the currently-selected item.
index++;
if (index == mDisplayList.Count) {
index = 0;
}
//Debug.WriteLine("FindText index=" + index + " start=" + mFindStartIndex +
// " str=" + mFindString);
while (index != mFindStartIndex) {
if (mFindStartIndex < 0) {
// need to latch this inside the loop so the initial test doesn't fail
mFindStartIndex = index;
}
string searchStr = mDisplayList.GetSearchString(index);
int matchPos = searchStr.IndexOf(mFindString,
StringComparison.InvariantCultureIgnoreCase);
if (matchPos >= 0) {
//Debug.WriteLine("Match " + index + ": " + searchStr);
codeListView.EnsureVisible(index);
codeListView.DeselectAll();
codeListView.SelectedIndices.Add(index);
return;
}
index++;
if (index == mDisplayList.Count) {
index = 0;
}
}
// Announce that we've wrapped around, then clear the start index.
MessageBox.Show(this, Properties.Resources.FIND_REACHED_START,
Properties.Resources.FIND_REACHED_START_CAPTION, MessageBoxButtons.OK,
MessageBoxIcon.Information);
mFindStartIndex = -1;
}
// Edit > Go To...
private void gotoToolStripMenuItem_Click(object sender, EventArgs e) {
GotoBox dlg = new GotoBox(mProject, mOutputFormatter);
if (dlg.ShowDialog() == DialogResult.OK) {
GoToOffset(dlg.TargetOffset, false, true);
}
dlg.Dispose();
}
// Edit > Project Properties...
private void projectPropertiesToolStripMenuItem_Click(object sender, EventArgs e) {
string projectDir = string.Empty;
if (!string.IsNullOrEmpty(mProjectPathName)) {
projectDir = Path.GetDirectoryName(mProjectPathName);
}
EditProjectProperties dlg = new EditProjectProperties(projectDir);
dlg.SetInitialProps(mProject.ProjectProps);
dlg.NumFormatter = mOutputFormatter;
dlg.ShowDialog();
ProjectProperties newProps = dlg.NewProps;
dlg.Dispose();
if (newProps != null) {
UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(
mProject.ProjectProps, newProps);
ApplyUndoableChanges(new ChangeSet(uc));
}
}
// Edit > Toggle Data Scan
private void toggleDataScanToolStripMenuItem_Click(object sender, EventArgs e) {
ProjectProperties oldProps = mProject.ProjectProps;
ProjectProperties newProps = new ProjectProperties(oldProps);
newProps.AnalysisParams.AnalyzeUncategorizedData =
!newProps.AnalysisParams.AnalyzeUncategorizedData;
UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(oldProps, newProps);
ApplyUndoableChanges(new ChangeSet(uc));
}
// Edit > Settings...
private void settingsToolStripMenuItem_Click(object sender, EventArgs e) {
ShowAppSettings(EditAppSettings.Tab.Unknown, AsmGen.AssemblerInfo.Id.Unknown);
}
/// <summary>
/// Opens the app settings dialog.
/// </summary>
/// <param name="initialTab">Tab to present to the user.</param>
public void ShowAppSettings(EditAppSettings.Tab initialTab,
AsmGen.AssemblerInfo.Id initialAsmId) {
EditAppSettings dlg = new EditAppSettings(this, initialTab, initialAsmId);
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_MouseDown(object sender, MouseEventArgs e) {
// MouseClick only fires for certain buttons and certain locations. MouseDown
// fires for all buttons so long as the pointer is in the codeListView area.
if (e.Button == MouseButtons.XButton1 && navigateBackToolStripButton.Enabled) {
navigateBackToolStripButton_Click(sender, e);
}
}
private void codeListView_MouseDoubleClick(object sender, MouseEventArgs e) {
ListViewHitTestInfo info = codeListView.HitTest(e.X, e.Y);
int row = info.Item.Index;
int col = info.Item.SubItems.IndexOf(info.SubItem);
// col will be -1 for e.g. blank lines [not anymore?]
string value = col < 0 ? "-" : info.Item.SubItems[col].Text;
Debug.WriteLine(string.Format("R{0}:C{1} val '{2}'", row, col, value));
// It's possible to select multiple lines with shift-double-click. We
// handle that by checking to see what UpdateActionMenu() decided was available.
// Clicking on some types of lines, such as ORG directives, results in
// specific behavior regardless of which column you click in. We're just
// checking the clicked-on line to decide what action to take. If it doesn't
// make sense to do for a multi-line selection, the action will have been
// disabled.
DisplayList.Line line = mDisplayList[row];
switch (line.LineType) {
case DisplayList.Line.Type.EquDirective:
// Currently only does something for project symbols; platform symbols
// do nothing.
if (editProjectSymbolToolStripMenuItem.Enabled) {
EditProjectSymbol_Click(sender, e);
}
break;
case DisplayList.Line.Type.OrgDirective:
if (setAddressToolStripMenuItem.Enabled) {
EditAddress_Click(sender, e);
}
break;
case DisplayList.Line.Type.RegWidthDirective:
if (overrideStatusFlagsToolStripMenuItem.Enabled) {
EditStatusFlags_Click(sender, e);
}
break;
case DisplayList.Line.Type.LongComment:
if (editLongCommentToolStripMenuItem.Enabled) {
EditLongComment_Click(sender, e);
}
break;
case DisplayList.Line.Type.Note:
if (editNoteToolStripMenuItem.Enabled) {
EditNote_Click(sender, e);
}
break;
case DisplayList.Line.Type.Code:
case DisplayList.Line.Type.Data:
// For code and data, we have to break it down by column.
switch ((ColumnIndex)col) {
case ColumnIndex.Offset:
// does nothing
break;
case ColumnIndex.Address:
// edit address
if (setAddressToolStripMenuItem.Enabled) {
EditAddress_Click(sender, e);
}
break;
case ColumnIndex.Bytes:
if (showHexDumpToolStripMenuItem.Enabled) {
ShowHexDump_Click(sender, e);
}
break;
case ColumnIndex.Flags:
if (overrideStatusFlagsToolStripMenuItem.Enabled) {
EditStatusFlags_Click(sender, e);
}
break;
case ColumnIndex.Attributes:
// does nothing
break;
case ColumnIndex.Label:
if (editLabelToolStripMenuItem.Enabled) {
EditLabel_Click(sender, e);
}
break;
case ColumnIndex.Opcode:
// File offset should always be valid, since we excluded the EQU
// statements and header comment earlier.
if (line.FileOffset >= 0) {
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
// Does this have an operand with an in-file target offset?
if (attr.OperandOffset >= 0) {
// Yup, find the line for that offset and jump to it.
GoToOffset(attr.OperandOffset, false, true);
//int targetIndex =
// mDisplayList.FindLineIndexByOffset(attr.OperandOffset);
//GoToOffset(mDisplayList[targetIndex].FileOffset);
} else if (attr.IsDataStart || attr.IsInlineDataStart) {
// If it's an Address or Symbol, we can try to resolve
// the value.
int operandOffset = DataAnalysis.GetDataOperandOffset(
mProject, line.FileOffset);
if (operandOffset >= 0) {
GoToOffset(operandOffset, false, true);
//int targetIndex =
// mDisplayList.FindLineIndexByOffset(operandOffset);
//GoToOffset(mDisplayList[targetIndex].FileOffset);
}
}
}
break;
case ColumnIndex.Operand:
if (editOperandToolStripMenuItem.Enabled) {
EditInstrDataOperand_Click(sender, e);
}
break;
case ColumnIndex.Comment:
if (editCommentToolStripMenuItem.Enabled) {
EditComment_Click(sender, e);
}
break;
}
break;
default:
Debug.WriteLine("Double-click: unhandled line type " + line.LineType);
break;
}
}
/// <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;
bool editDataOperandEnabled = false;
bool editInstrOperandEnabled = false;
// Use IsSingleItemSelected(), rather than just checking sel.Count, because we
// want the user to be able to e.g. EditData on a multi-line string even if all
// lines in the string are selected.
if (IsSingleItemSelected()) {
entityCounts = GatherEntityCounts(sel[0]);
DisplayList.Line line = mDisplayList[sel[0]];
DisplayList.Line.Type lineType = line.LineType;
bool isCodeOrData = (lineType == DisplayList.Line.Type.Code ||
lineType == DisplayList.Line.Type.Data);
setAddressToolStripMenuItem.Enabled =
(isCodeOrData || lineType == DisplayList.Line.Type.OrgDirective);
editInstrOperandEnabled = (lineType == DisplayList.Line.Type.Code &&
mProject.GetAnattrib(line.FileOffset).IsInstructionWithOperand);
editDataOperandEnabled = (lineType == DisplayList.Line.Type.Data);
editLabelToolStripMenuItem.Enabled = isCodeOrData;
editCommentToolStripMenuItem.Enabled = isCodeOrData;
editLongCommentToolStripMenuItem.Enabled =
(isCodeOrData || lineType == DisplayList.Line.Type.LongComment);
editNoteToolStripMenuItem.Enabled =
(isCodeOrData || lineType == DisplayList.Line.Type.Note);
overrideStatusFlagsToolStripMenuItem.Enabled =
(lineType == DisplayList.Line.Type.Code ||
lineType == DisplayList.Line.Type.RegWidthDirective);
deleteNoteCommentToolStripMenuItem.Enabled =
(lineType == DisplayList.Line.Type.LongComment ||
lineType == DisplayList.Line.Type.Note);
if (lineType == DisplayList.Line.Type.EquDirective) {
// Only enable this for project symbols, not all EQU directives.
int symIndex = DisplayList.DefSymIndexFromOffset(line.FileOffset);
DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex];
editProjectSymbolToolStripMenuItem.Enabled =
(defSym.SymbolSource == Symbol.Source.Project);
} else {
editProjectSymbolToolStripMenuItem.Enabled = false;
}
} else {
entityCounts = GatherEntityCounts(-1);
// Disable all single-target-only items.
setAddressToolStripMenuItem.Enabled = false;
editInstrOperandEnabled = false;
editLabelToolStripMenuItem.Enabled = false;
editCommentToolStripMenuItem.Enabled = false;
editLongCommentToolStripMenuItem.Enabled = false;
editNoteToolStripMenuItem.Enabled = false;
overrideStatusFlagsToolStripMenuItem.Enabled = false;
deleteNoteCommentToolStripMenuItem.Enabled = false;
editProjectSymbolToolStripMenuItem.Enabled = false;
if (sel.Count == 0) {
// Disable everything else.
editDataOperandEnabled = false;
hintAsCodeToolStripMenuItem.Enabled = false;
hintAsDataToolStripMenuItem.Enabled = false;
hintAsInlineDataToolStripMenuItem.Enabled = false;
removeHintToolStripMenuItem.Enabled = false;
} else {
// Must be all data items. Blank lines are okay. Currently allowing
// control lines as well.
editDataOperandEnabled =
(entityCounts.mDataLines > 0 && entityCounts.mCodeLines == 0);
}
}
// Enable the "edit operand" menu item if either instruction or data operand
// editing is allowed.
editOperandToolStripMenuItem.Enabled =
editDataOperandEnabled || editInstrOperandEnabled;
formatSplitAddressTableToolStripMenuItem.Enabled =
(entityCounts.mDataLines > 0 && entityCounts.mCodeLines == 0);
toggleSingleBytesToolStripMenuItem.Enabled =
(entityCounts.mDataLines > 0 && entityCounts.mCodeLines == 0);
// So long as some code or data is highlighted, allow these. Don't worry about
// control lines. Disable options that would have no effect.
bool enableHints = (entityCounts.mDataLines > 0 || entityCounts.mCodeLines > 0);
hintAsCodeToolStripMenuItem.Enabled = enableHints &&
(entityCounts.mDataHints != 0 ||
entityCounts.mInlineDataHints != 0 ||
entityCounts.mNoHints != 0);
hintAsDataToolStripMenuItem.Enabled = enableHints &&
(entityCounts.mCodeHints != 0 ||
entityCounts.mInlineDataHints != 0 ||
entityCounts.mNoHints != 0);
hintAsInlineDataToolStripMenuItem.Enabled = enableHints &&
(entityCounts.mCodeHints != 0 ||
entityCounts.mDataHints != 0 ||
entityCounts.mNoHints != 0);
removeHintToolStripMenuItem.Enabled = enableHints &&
(entityCounts.mCodeHints != 0 ||
entityCounts.mDataHints != 0 ||
entityCounts.mInlineDataHints != 0);
// Just leave this on. If they're in EQU-land or nothing is selected, it'll just
// open at the start of the file.
showHexDumpToolStripMenuItem.Enabled = true;
}
/// <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;
}
private void editToolStripMenuItem_DropDownOpening(object sender, EventArgs e) {
// Set the checkmark on Toggle Data Scan.
toggleDataScanToolStripMenuItem.Checked = (mProject != null) &&
mProject.ProjectProps.AnalysisParams.AnalyzeUncategorizedData;
}
/// <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. We also want to call
/// this when the context menu closes, so that Alt-A will open the 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();
}
// Convert generic "edit operand" request to instruction or data edit call.
private void EditInstrDataOperand_Click(object sender, EventArgs e) {
ListView.SelectedIndexCollection sel = codeListView.SelectedIndices;
if (mDisplayList[sel[0]].LineType == DisplayList.Line.Type.Code) {
EditOperand_Click(sender, e);
} else {
EditData_Click(sender, e);
}
}
private void EditOperand_Click(Object sender, EventArgs e) {
Debug.Assert(IsSingleItemSelected());
ListView.SelectedIndexCollection sel = codeListView.SelectedIndices;
int offset = mDisplayList[sel[0]].FileOffset;
EditOperand dlg = new EditOperand(offset, mProject, mOutputFormatter);
// We'd really like to pass in an indication of what the "default" format actually
// resolved to, but we don't always know. If this offset has a FormatDescriptor,
// we might not have auto-generated the label that would have been used otherwise.
// We're editing the FormatDescriptor from OperandFormats, not Anattribs;
// the latter may have auto-generated stuff.
if (mProject.OperandFormats.TryGetValue(offset, out FormatDescriptor dfd)) {
dlg.FormatDescriptor = dfd;
}
dlg.ShowDialog();
if (dlg.DialogResult == DialogResult.OK) {
ChangeSet cs = new ChangeSet(1);
if (dlg.FormatDescriptor != dfd && dlg.ShortcutAction !=
EditOperand.SymbolShortcutAction.CreateLabelInstead) {
// Note EditOperand returns a null descriptor when the user selects Default.
// This is different from how EditData works, since that has to deal with
// multiple regions.
Debug.WriteLine("Changing " + dfd + " to " + dlg.FormatDescriptor);
UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset,
dfd, dlg.FormatDescriptor);
cs.Add(uc);
} else if (dfd != null && dlg.ShortcutAction ==
EditOperand.SymbolShortcutAction.CreateLabelInstead) {
Debug.WriteLine("Removing existing label for CreateLabelInstead");
UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset,
dfd, null);
cs.Add(uc);
} else {
Debug.WriteLine("No change to format descriptor");
}
switch (dlg.ShortcutAction) {
case EditOperand.SymbolShortcutAction.CreateLabelInstead:
case EditOperand.SymbolShortcutAction.CreateLabelAlso:
Debug.Assert(!mProject.UserLabels.ContainsKey(dlg.ShortcutArg));
Anattrib targetAttr = mProject.GetAnattrib(dlg.ShortcutArg);
Symbol newLabel = new Symbol(dlg.FormatDescriptor.SymbolRef.Label,
targetAttr.Address, Symbol.Source.User, Symbol.Type.LocalOrGlobalAddr);
UndoableChange uc = UndoableChange.CreateLabelChange(dlg.ShortcutArg,
null, newLabel);
cs.Add(uc);
break;
case EditOperand.SymbolShortcutAction.CreateProjectSymbolAlso:
Debug.Assert(!mProject.ProjectProps.ProjectSyms.ContainsKey(
dlg.FormatDescriptor.SymbolRef.Label));
DefSymbol defSym = new DefSymbol(dlg.FormatDescriptor.SymbolRef.Label,
dlg.ShortcutArg, Symbol.Source.Project, Symbol.Type.ExternalAddr,
FormatDescriptor.SubType.Hex, string.Empty, string.Empty);
ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps);
newProps.ProjectSyms.Add(defSym.Label, defSym);
uc = UndoableChange.CreateProjectPropertiesChange(
mProject.ProjectProps, newProps);
cs.Add(uc);
break;
case EditOperand.SymbolShortcutAction.None:
break;
}
if (cs.Count != 0) {
ApplyUndoableChanges(cs);
}
}
dlg.Dispose();
}
private void EditData_Click(Object sender, EventArgs e) {
ListView.SelectedIndexCollection sel = codeListView.SelectedIndices;
Debug.Assert(sel.Count > 0);
EditData dlg = new EditData(mProject.FileData, mProject.SymbolTable, mOutputFormatter);
TypedRangeSet trs = dlg.Selection = GroupedOffsetSetFromSelected();
if (trs.Count == 0) {
Debug.Assert(false, "EditData found nothing to edit"); // shouldn't happen
dlg.Dispose();
return;
}
// If the first offset has a FormatDescriptor, pass that in as a recommendation
// for the default value in the dialog. This allows single-item editing to work
// as expected. If the format can't be applied to the full selection (which
// would disable that radio button), the dialog will have to pick something
// that does work.
//
// We could pull this out of Anattribs, which would let the dialog reflect the
// auto-format that the user was just looking at. However, I think it's better
// if the dialog shows what's actually there, i.e. no formatting at all.
IEnumerator<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 FormatSplitAddressTable_Click(object sender, EventArgs e) {
TypedRangeSet trs = GroupedOffsetSetFromSelected();
if (trs.Count == 0) {
// shouldn't happen
Debug.Assert(false, "FormatSplitAddressTable found nothing to edit");
return;
}
FormatSplitAddress dlg = new FormatSplitAddress(mProject, trs, mOutputFormatter);
dlg.ShowDialog();
if (dlg.DialogResult == DialogResult.OK) {
// Start with the format descriptors.
ChangeSet cs = mProject.GenerateFormatMergeSet(dlg.NewFormatDescriptors);
// Add in the user labels.
foreach (KeyValuePair<int, Symbol> kvp in dlg.NewUserLabels) {
Symbol oldUserValue = null;
if (mProject.UserLabels.ContainsKey(kvp.Key)) {
Debug.Assert(false, "should not be replacing label");
oldUserValue = mProject.UserLabels[kvp.Key];
}
UndoableChange uc = UndoableChange.CreateLabelChange(kvp.Key,
oldUserValue, kvp.Value);
cs.Add(uc);
}
// Apply code hints.
if (dlg.WantCodeHints) {
TypedRangeSet newSet = new TypedRangeSet();
TypedRangeSet undoSet = new TypedRangeSet();
foreach (int offset in dlg.AllTargetOffsets) {
if (!mProject.GetAnattrib(offset).IsInstruction) {
CodeAnalysis.TypeHint oldType = mProject.TypeHints[offset];
if (oldType == CodeAnalysis.TypeHint.Code) {
continue; // already set
}
undoSet.Add(offset, (int)oldType);
newSet.Add(offset, (int)CodeAnalysis.TypeHint.Code);
}
}
if (newSet.Count != 0) {
cs.Add(UndoableChange.CreateTypeHintChange(undoSet, newSet));
}
}
// Finally, apply the change.
if (cs.Count != 0) {
ApplyUndoableChanges(cs);
} else {
Debug.WriteLine("No changes found");
}
}
dlg.Dispose();
}
private void ToggleSingleBytes_Click(object sender, EventArgs e) {
TypedRangeSet trs = GroupedOffsetSetFromSelected();
if (trs.Count == 0) {
Debug.Assert(false, "nothing to edit"); // shouldn't happen
return;
}
// Check the format descriptor of the first selected offset.
int firstOffset = -1;
foreach (TypedRangeSet.Tuple tup in trs) {
firstOffset = tup.Value;
break;
}
Debug.Assert(mProject.GetAnattrib(firstOffset).IsDataStart);
bool toDefault = false;
if (mProject.OperandFormats.TryGetValue(firstOffset, out FormatDescriptor curDfd)) {
if (curDfd.FormatType == FormatDescriptor.Type.NumericLE &&
curDfd.FormatSubType == FormatDescriptor.SubType.None &&
curDfd.Length == 1) {
// Currently single-byte, toggle to default.
toDefault = true;
}
}
// Iterate through the selected regions.
SortedList<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, true);
}
private void MarkAsData_Click(Object sender, EventArgs e) {
MarkAsType(CodeAnalysis.TypeHint.Data, true);
}
private void MarkAsInlineData_Click(Object sender, EventArgs e) {
MarkAsType(CodeAnalysis.TypeHint.InlineData, false);
}
private void MarkAsNoHint_Click(Object sender, EventArgs e) {
MarkAsType(CodeAnalysis.TypeHint.NoHint, false);
}
private void MarkAsType(CodeAnalysis.TypeHint hint, bool firstByteOnly) {
RangeSet sel;
if (firstByteOnly) {
sel = new RangeSet();
foreach (int index in codeListView.SelectedIndices) {
int offset = mDisplayList[index].FileOffset;
if (offset >= 0) {
// Not interested in the header stuff for hinting.
sel.Add(offset);
}
}
} else {
sel = OffsetSetFromSelected();
}
TypedRangeSet newSet = new TypedRangeSet();
TypedRangeSet undoSet = new TypedRangeSet();
foreach (int offset in sel) {
if (offset < 0) {
// header comment
continue;
}
CodeAnalysis.TypeHint oldType = mProject.TypeHints[offset];
if (oldType == hint) {
// no change, don't add to set
continue;
}
undoSet.Add(offset, (int)oldType);
newSet.Add(offset, (int)hint);
}
if (newSet.Count == 0) {
Debug.WriteLine("No changes found (" + hint + ", " + sel.Count + " offsets)");
return;
}
UndoableChange uc = UndoableChange.CreateTypeHintChange(undoSet, newSet);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
private void ShowHexDump_Click(object sender, EventArgs e) {
if (mHexDumpDialog == null) {
// Create and show modeless dialog. This one is "always on top" by default,
// to allow the user to click around to various points.
mHexDumpDialog = new Tools.HexDumpViewer(mProject.FileData, mOutputFormatter);
mHexDumpDialog.OnWindowClosing += (sender1, e1) => {
Debug.WriteLine("Hex dump dialog closed");
//showHexDumpToolStripMenuItem.Checked = false;
mHexDumpDialog = null;
};
mHexDumpDialog.TopMost = true;
mHexDumpDialog.Show();
//showHexDumpToolStripMenuItem.Checked = true;
}
// Bring it to the front of the window stack. This also transfers focus to the
// window.
mHexDumpDialog.BringToFront();
// Set the dialog's position.
ListView.SelectedIndexCollection sel = codeListView.SelectedIndices;
if (sel.Count > 0) {
int firstIndex = sel[0];
int lastIndex = sel[sel.Count - 1];
// offsets can be < 0 if they've selected EQU statements
int firstOffset = Math.Max(0, mDisplayList[firstIndex].FileOffset);
int lastOffset = Math.Max(firstOffset, mDisplayList[lastIndex].FileOffset +
mDisplayList[lastIndex].OffsetSpan - 1);
mHexDumpDialog.ShowOffsetRange(firstOffset, lastOffset);
}
}
private void aSCIIChartToolStripMenuItem_Click(object sender, EventArgs e) {
// Show or hide the modeless dialog.
if (mAsciiChartDialog == null) {
Tools.AsciiChart dlg = new Tools.AsciiChart();
dlg.OnWindowClosing += (sender1, e1) => {
Debug.WriteLine("ASCII chart closed");
aSCIIChartToolStripMenuItem.Checked = false;
mAsciiChartDialog = null;
};
dlg.Show();
mAsciiChartDialog = dlg;
aSCIIChartToolStripMenuItem.Checked = true;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mAsciiChartDialog.Close();
}
}
/// <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 SerializeCodeListColumnWidths() {
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) {
AppSettings.Global.Dirty = true;
}
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] = symbolValueColumnHeader.Text;
mSymbolColumnHeaderNames[2] = symbolNameColumnHeader.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,
mOutputFormatter.FormatHexValue(sym.Value, 0));
mSymbolSubArray[1] = new ListViewItem.ListViewSubItem(lvi, sym.Label);
lvi.SubItems.AddRange(mSymbolSubArray);
return lvi;
}
private void InvalidateSymbolListView() {
symbolListView.BeginUpdate();
ClearSymbolListViewCache();
symbolListView.VirtualListSize = mSymbolSubset.GetSubsetCount();
symbolListView.EndUpdate();
}
// Column header click. Update sort.
private void symbolListView_ColumnClick(object sender, ColumnClickEventArgs e) {
//Debug.WriteLine("Click on " + e.Column);
SymbolTableSubset.SortCol prevCol = mSymbolSubset.SortColumn;
// SortCol happens to match the ListView column numbers, so just cast it
SymbolTableSubset.SortCol clickCol = (SymbolTableSubset.SortCol)e.Column;
if (prevCol == clickCol) {
mSymbolSubset.SortAscending = !mSymbolSubset.SortAscending;
} else {
mSymbolSubset.SortColumn = clickCol;
}
SetSymbolColumnHeaders();
InvalidateSymbolListView();
}
private void symbolListView_MouseDoubleClick(object sender, MouseEventArgs e) {
ListViewHitTestInfo info = symbolListView.HitTest(e.X, e.Y);
int row = info.Item.Index;
Symbol sym = mSymbolSubset.GetSubsetItem(row);
if (sym.SymbolSource == Symbol.Source.Auto || sym.SymbolSource == Symbol.Source.User) {
int offset = mProject.FindLabelByName(sym.Label);
if (offset >= 0) {
GoToOffset(offset, false, true);
codeListView.Focus();
} else {
Debug.WriteLine("DClick symbol: " + sym + ": label not found");
}
} else {
Debug.WriteLine("DClick symbol: " + sym + ": not label");
}
}
/// <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];
symbolValueColumnHeader.Text =
(sortCol == SymbolTableSubset.SortCol.Value ? sortStr : "") +
mSymbolColumnHeaderNames[1];
symbolNameColumnHeader.Text =
(sortCol == SymbolTableSubset.SortCol.Name ? sortStr : "") +
mSymbolColumnHeaderNames[2];
}
private void symbolListView_ColumnWidthChanged(object sender,
ColumnWidthChangedEventArgs e) {
UpdateLastSymbolColumnWidth();
}
private void symbolListView_SizeChanged(object sender, EventArgs e) {
UpdateLastSymbolColumnWidth();
}
private void UpdateLastSymbolColumnWidth() {
#if false
const int ADJ = 4; // fudge factor needed to prevent horizontal scrollbar
int leftWidths = symbolListView.Columns[0].Width + symbolListView.Columns[1].Width;
int lastWidth = symbolListView.Size.Width - leftWidths - ADJ;
if (lastWidth < 0) {
lastWidth = 0;
}
symbolListView.Columns[2].Width = lastWidth;
#endif
AppSettings.Global.Dirty = true;
}
private void SerializeSymbolColumnWidths() {
int[] values = new int[] {
symbolListView.Columns[0].Width,
symbolListView.Columns[1].Width,
symbolListView.Columns[2].Width
};
AppSettings.Global.SetString(AppSettings.SYMWIN_COL_WIDTHS,
TextUtil.SerializeIntArray(values));
}
private void DeserializeSymbolColumnWidths() {
string str = AppSettings.Global.GetString(AppSettings.SYMWIN_COL_WIDTHS, null);
if (!string.IsNullOrEmpty(str)) {
int[] values = TextUtil.DeserializeIntArray(str);
if (values.Length == symbolListView.Columns.Count) {
for (int i = 0; i < values.Length; i++) {
symbolListView.Columns[i].Width = values[i];
}
}
}
// The updates should automatically trigger the last-column-width adjuster.
}
private void symbolUserCheckBox_CheckedChanged(object sender, EventArgs e) {
mSymbolSubset.IncludeUserLabels = symbolUserCheckBox.Checked;
InvalidateSymbolListView();
}
private void symbolProjectCheckBox_CheckedChanged(object sender, EventArgs e) {
mSymbolSubset.IncludeProjectSymbols = symbolProjectCheckBox.Checked;
InvalidateSymbolListView();
}
private void symbolPlatformCheckBox_CheckedChanged(object sender, EventArgs e) {
mSymbolSubset.IncludePlatformSymbols = symbolPlatformCheckBox.Checked;
InvalidateSymbolListView();
}
private void symbolAutoCheckBox_CheckedChanged(object sender, EventArgs e) {
mSymbolSubset.IncludeAutoLabels = symbolAutoCheckBox.Checked;
InvalidateSymbolListView();
}
private void symbolAddrCheckBox_CheckedChanged(object sender, EventArgs e) {
mSymbolSubset.IncludeAddresses = symbolAddressCheckBox.Checked;
InvalidateSymbolListView();
}
private void symbolConstantCheckBox_CheckedChanged(object sender, EventArgs e) {
mSymbolSubset.IncludeConstants = symbolConstantCheckBox.Checked;
InvalidateSymbolListView();
}
#endregion symbolListView Virtual and UI handling
#region referencesListView stuff
private ListViewItem.ListViewSubItem[] mXrefSubArray =
new ListViewItem.ListViewSubItem[2];
/// <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;
// Replace line break with bullet. If there's a single CRLF at the end, strip it.
string nocrlfStr;
if (mlc.Text.EndsWith("\r\n")) {
nocrlfStr = mlc.Text.Substring(0, mlc.Text.Length - 1).Replace("\r\n", " \u2022 ");
} else {
nocrlfStr = mlc.Text.Replace("\r\n", " \u2022 ");
}
mNotesSubArray[0] = new ListViewItem.ListViewSubItem(lvi, nocrlfStr);
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 DEBUG
sb.Append(" [offset=+" + line.FileOffset.ToString("x6") + "]");
#endif
if (!string.IsNullOrEmpty(extraStr)) {
sb.Append("\r\n\r\n");
sb.Append(extraStr);
}
infoTextBox.Text = sb.ToString();
return;
}
Debug.Assert(line.IsCodeOrData);
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
// Show number of bytes of code/data.
if (line.OffsetSpan == 1) {
sb.AppendFormat(Properties.Resources.FMT_INFO_LINE_SUM_SINGULAR,
lineIndex, line.OffsetSpan, lineTypeStr);
} else {
sb.AppendFormat(Properties.Resources.FMT_INFO_LINE_SUM_PLURAL,
lineIndex, line.OffsetSpan, lineTypeStr);
}
sb.Append("\r\n");
if (!mProject.OperandFormats.TryGetValue(line.FileOffset, out FormatDescriptor dfd)) {
// No user-specified format, but there may be a generated format.
sb.AppendFormat(Properties.Resources.FMT_INFO_FD_SUM,
Properties.Resources.DEFAULT_VALUE);
if (attr.DataDescriptor != null) {
sb.Append(" [");
sb.Append(attr.DataDescriptor.ToUiString());
sb.Append("]");
}
} else {
// User-specified operand format.
// If the descriptor has a weak reference to an unknown symbol, should we
// call that out here?
sb.AppendFormat(Properties.Resources.FMT_INFO_FD_SUM, dfd.ToUiString());
}
sb.Append("\r\n");
// Debug only
//sb.Append("DEBUG: opAddr=" + attr.OperandAddress.ToString("x4") +
// " opOff=" + attr.OperandOffset.ToString("x4") + "\r\n");
sb.Append("\u2022Attributes:");
if (attr.IsHinted) {
sb.Append(" Hinted(");
for (int i = 0; i < line.OffsetSpan; i++) {
switch (mProject.TypeHints[line.FileOffset + i]) {
case CodeAnalysis.TypeHint.Code:
sb.Append("C");
break;
case CodeAnalysis.TypeHint.Data:
sb.Append("D");
break;
case CodeAnalysis.TypeHint.InlineData:
sb.Append("I");
break;
default:
break;
}
if (i > 8) {
sb.Append("...");
break;
}
}
sb.Append(')');
}
if (attr.IsEntryPoint) {
sb.Append(" EntryPoint");
}
if (attr.IsBranchTarget) {
sb.Append(" BranchTarget");
}
if (attr.DoesNotContinue) {
sb.Append(" NoContinue");
}
if (attr.DoesNotBranch) {
sb.Append(" NoBranch");
}
if (mProject.StatusFlagOverrides[line.FileOffset].AsInt != 0) {
sb.Append(" StatusFlags");
}
sb.Append("\r\n\r\n");
if (attr.IsInstruction) {
Asm65.OpDef op = mProject.CpuDef.GetOpDef(mProject.FileData[line.FileOffset]);
string shortDesc = mOpDesc.GetShortDescription(op.Mnemonic);
if (!string.IsNullOrEmpty(shortDesc)) {
if (op.IsUndocumented) {
sb.Append("\u23e9[*] ");
} else {
sb.Append("\u23e9 ");
}
sb.Append(shortDesc);
string addrStr = mOpDesc.GetAddressModeDescription(op.AddrMode);
if (!string.IsNullOrEmpty(addrStr)) {
sb.Append(", ");
sb.Append(addrStr);
}
sb.Append("\r\n");
}
sb.Append("\u2022Cycles: ");
int cycles = op.Cycles;
Asm65.OpDef.CycleMod cycMods = op.CycleMods;
sb.Append(cycles.ToString());
if (cycMods != 0) {
sb.Append(" (");
int workBits = (int)cycMods;
while (workBits != 0) {
// Isolate rightmost bit.
int firstBit = (~workBits + 1) & workBits;
sb.Append(mOpDesc.GetCycleModDescription((OpDef.CycleMod)firstBit));
// Remove from set.
workBits &= ~firstBit;
if (workBits != 0) {
// more to come
sb.Append(", ");
}
}
sb.Append(")");
}
sb.Append("\r\n");
const string FLAGS = "NVMXDIZC";
sb.Append("\u2022Flags affected: ");
Asm65.StatusFlags affectedFlags = op.FlagsAffected;
for (int i = 0; i < 8; i++) {
if (affectedFlags.GetBit((StatusFlags.FlagBits)(7 - i)) >= 0) {
sb.Append(' ');
sb.Append(FLAGS[i]);
} else {
sb.Append(" -");
}
}
sb.Append("\r\n");
string longDesc = mOpDesc.GetLongDescription(op.Mnemonic);
if (!string.IsNullOrEmpty(longDesc)) {
sb.Append("\r\n");
sb.Append(longDesc);
sb.Append("\r\n");
}
} else {
// do we want descriptions of the pseudo-ops?
}
// Publish
infoTextBox.Text = sb.ToString();
}
#endregion // Info view
#region Tools items
private void hexDumpToolStripMenuItem_Click(object sender, EventArgs e) {
OpenFileDialog fileDlg = new OpenFileDialog();
fileDlg.Filter = Properties.Resources.FILE_FILTER_ALL;
fileDlg.FilterIndex = 1;
if (fileDlg.ShowDialog() != DialogResult.OK) {
return;
}
string fileName = fileDlg.FileName;
FileInfo fi = new FileInfo(fileName);
if (fi.Length > Tools.HexDumpViewer.MAX_LENGTH) {
string msg = string.Format(Properties.Resources.ERR_FILE_TOO_LARGE,
Tools.HexDumpViewer.MAX_LENGTH);
MessageBox.Show(this, msg, Properties.Resources.OPEN_DATA_FAIL_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
byte[] data;
try {
data = File.ReadAllBytes(fileName);
} catch (Exception ex) {
MessageBox.Show(this, ex.Message);
return;
}
// Fire and forget.
Tools.HexDumpViewer dlg = new Tools.HexDumpViewer(data, mOutputFormatter);
dlg.Text = Path.GetFileName(fileName);
dlg.Show();
}
#endregion Tools items
#region Debug menu items
private void dEBUGToolStripMenuItem_DropDownOpened(object sender, EventArgs e) {
toggleOwnerDrawToolStripMenuItem.Checked = codeListView.OwnerDraw;
toggleCommentRulersToolStripMenuItem.Checked = MultiLineComment.DebugShowRuler;
useKeepAliveHackToolStripMenuItem.Checked = ScriptManager.UseKeepAliveHack;
}
private void reanalyzeToolStripMenuItem_Click(object sender, EventArgs e) {
Debug.WriteLine("Reanalyzing...");
// Call through ApplyChanges so we update the timer task output.
UndoableChange uc =
UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.CodeAndData);
ApplyChanges(new ChangeSet(uc), false);
UpdateMenuItemsAndTitle(); // in case something changed
}
private Tools.ShowText mShowUndoRedoHistoryDialog;
private void showUndoRedoHistoryToolStripMenuItem_Click(object sender, EventArgs e) {
// Show or hide the modeless dialog.
if (mShowUndoRedoHistoryDialog == null) {
Tools.ShowText dlg = new Tools.ShowText();
dlg.Title = "Undo/Redo History";
dlg.BodyText = mProject.DebugGetUndoRedoHistory();
dlg.OnWindowClosing += (sender1, e1) => {
Debug.WriteLine("Undo/redo dialog closed");
showUndoRedoHistoryToolStripMenuItem.Checked = false;
mShowUndoRedoHistoryDialog = null;
};
dlg.Show();
mShowUndoRedoHistoryDialog = dlg;
showUndoRedoHistoryToolStripMenuItem.Checked = true;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mShowUndoRedoHistoryDialog.Close();
}
}
private Tools.ShowText mShowAnalyzerOutputDialog;
private void showAnalyzerOutputToolStripMenuItem_Click(object sender, EventArgs e) {
// Show or hide the modeless dialog.
if (mShowAnalyzerOutputDialog == null) {
Tools.ShowText dlg = new Tools.ShowText();
dlg.Title = "Analyzer Output";
if (mGenerationLog == null) {
dlg.BodyText = "(no data yet)";
} else {
dlg.BodyText = mGenerationLog.WriteToString();
}
dlg.OnWindowClosing += (sender1, e1) => {
Debug.WriteLine("Analyzer output dialog closed");
showAnalyzerOutputToolStripMenuItem.Checked = false;
mShowAnalyzerOutputDialog = null;
};
dlg.Show();
mShowAnalyzerOutputDialog = dlg;
showAnalyzerOutputToolStripMenuItem.Checked = true;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mShowAnalyzerOutputDialog.Close();
}
}
private Tools.ShowText mShowAnalysisTimersDialog;
private void showAnalysisTimersToolStripMenuItem_Click(object sender, EventArgs e) {
// Show or hide the modeless dialog.
if (mShowAnalysisTimersDialog == null) {
Tools.ShowText dlg = new Tools.ShowText();
dlg.Title = "Analysis Timers";
dlg.BodyText = "(no data yet)";
dlg.OnWindowClosing += (sender1, e1) => {
Debug.WriteLine("Analysis timers dialog closed");
showAnalysisTimersToolStripMenuItem.Checked = false;
mShowAnalysisTimersDialog = null;
};
dlg.Show();
mShowAnalysisTimersDialog = dlg;
showAnalysisTimersToolStripMenuItem.Checked = true;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mShowAnalysisTimersDialog.Close();
}
}
private void toggleOwnerDrawToolStripMenuItem_Click(object sender, EventArgs e) {
Debug.WriteLine("TOGGLE OWNERDRAW");
bool newState = !codeListView.OwnerDraw;
codeListView.OwnerDraw = newState;
notesListView.OwnerDraw = newState;
}
private void toggleCommentRulersToolStripMenuItem_Click(object sender, EventArgs e) {
MultiLineComment.DebugShowRuler = !MultiLineComment.DebugShowRuler;
// Don't need to repeat the analysis, but we do want to save/restore the
// selection and top position when the comment fields change size.
UndoableChange uc =
UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DataOnly);
ApplyChanges(new ChangeSet(uc), false);
}
private void useKeepAliveHackToolStripMenuItem_Click(object sender, EventArgs e) {
ScriptManager.UseKeepAliveHack = !ScriptManager.UseKeepAliveHack;
}
private void sourceGenTestsToolStripMenuItem_Click(object sender, EventArgs e) {
Tests.GenTestRunner dlg = new Tests.GenTestRunner();
dlg.ShowDialog();
dlg.Dispose();
}
private void extensionScriptInfoToolStripMenuItem_Click(object sender, EventArgs e) {
string info = mProject.DebugGetLoadedScriptInfo();
Tools.ShowText dlg = new Tools.ShowText();
dlg.Title = "Loaded Extension Script Info";
dlg.BodyText = info;
dlg.ShowDialog();
}
#endregion Debug menu items
}
}