1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-29 10:50:28 +00:00
6502bench/SourceGen/MainController.cs
Andy McFadden 354d106665 Documentation update
Also, make "spaces between numbers in bytes column" default to
enabled.
2019-08-19 14:16:35 -07:00

3407 lines
145 KiB
C#

/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Web.Script.Serialization;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using Asm65;
using CommonUtil;
using CommonWPF;
using SourceGen.Sandbox;
using SourceGen.WpfGui;
namespace SourceGen {
/// <summary>
/// This class manages user interaction. The goal is for this to be relatively
/// GUI-toolkit-agnostic, with all the WPF stuff tucked into the code-behind files. An
/// instance of this class is created by MainWindow when the app starts.
///
/// There is some Windows-specific stuff, like MessageBox and OpenFileDialog.
/// </summary>
public class MainController {
private const string SETTINGS_FILE_NAME = "SourceGen-settings";
#region Project state
// Currently open project, or null if none.
private DisasmProject mProject;
// Pathname to 65xx data file.
private string mDataPathName;
// Pathname of .dis65 file. This will be empty for a new project.
private string mProjectPathName;
/// <summary>
/// Data backing the code list.
/// </summary>
public LineListGen CodeLineList { get; private set; }
#endregion Project state
/// <summary>
/// Reference back to MainWindow object.
/// </summary>
private MainWindow mMainWin;
/// <summary>
/// Hex dump viewer window. This is used for the currently open project.
/// </summary>
private Tools.WpfGui.HexDumpViewer mHexDumpDialog;
// Debug windows.
private Tools.WpfGui.ShowText mShowAnalysisTimersDialog;
public bool IsDebugAnalysisTimersOpen { get { return mShowAnalysisTimersDialog != null; } }
private Tools.WpfGui.ShowText mShowAnalyzerOutputDialog;
public bool IsDebugAnalyzerOutputOpen { get { return mShowAnalyzerOutputDialog != null; } }
private Tools.WpfGui.ShowText mShowUndoRedoHistoryDialog;
public bool IsDebugUndoRedoHistoryOpen { get { return mShowUndoRedoHistoryDialog != null; } }
/// <summary>
/// This holds any un-owned Windows that we don't otherwise track. It's used for
/// hex dump windows of arbitrary files. We need to close them when the main window
/// is closed.
/// </summary>
private List<Window> mUnownedWindows = new List<Window>();
/// <summary>
/// ASCII chart reference window. Not tied to the project.
/// </summary>
private Tools.WpfGui.AsciiChart mAsciiChartDialog;
/// <summary>
/// Returns true if the ASCII chart window is currently open.
/// </summary>
public bool IsAsciiChartOpen { get { return mAsciiChartDialog != null; } }
/// <summary>
/// List of recently-opened projects.
/// </summary>
public List<string> RecentProjectPaths = new List<string>(MAX_RECENT_PROJECTS);
public const int MAX_RECENT_PROJECTS = 6;
/// <summary>
/// Analyzed selection state, updated whenever the selection changes.
/// </summary>
public SelectionState SelectionAnalysis { get; set; }
/// <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>
/// Stack for navigate forward/backward.
/// </summary>
private NavStack mNavStack = new NavStack();
/// <summary>
/// Output format configuration.
/// </summary>
private Formatter.FormatConfig mFormatterConfig;
/// <summary>
/// Output format controller.
///
/// This is shared with the DisplayList.
/// </summary>
private Formatter mOutputFormatter;
/// <summary>
/// Pseudo-op names.
///
/// This is shared with the DisplayList.
/// </summary>
private PseudoOp.PseudoOpNames mPseudoOpNames;
/// <summary>
/// String we most recently searched for.
/// </summary>
private string mFindString = string.Empty;
/// <summary>
/// Initial start point of most recent search.
/// </summary>
private int mFindStartIndex = -1;
/// <summary>
/// Used to highlight the line that is the target of the selected line.
/// </summary>
private int mTargetHighlightIndex = -1;
/// <summary>
/// CPU definition used when the Formatter was created. If the CPU choice or
/// inclusion of undocumented opcodes changes, we need to wipe the formatter.
/// </summary>
private CpuDef mOutputFormatterCpuDef;
/// <summary>
/// Instruction description object. Used for Info window.
/// </summary>
private OpDescription mOpDesc = OpDescription.GetOpDescription(null);
/// <summary>
/// If true, plugins will execute in the main application's AppDomain instead of
/// the sandbox.
/// </summary>
private bool mUseMainAppDomainForPlugins = false;
/// <summary>
/// Code list column numbers.
/// </summary>
public enum CodeListColumn {
Offset = 0, Address, Bytes, Flags, Attributes, Label, Opcode, Operand, Comment,
COUNT // must be last; must equal number of columns
}
/// <summary>
/// Clipboard format enumeration.
/// </summary>
public enum ClipLineFormat {
Unknown = -1,
AssemblerSource = 0,
Disassembly = 1
}
/// <summary>
/// True if a project is open and AnalyzeUncategorizedData is enabled.
/// </summary>
public bool IsAnalyzeUncategorizedDataEnabled {
get {
if (mProject == null) {
return false;
}
return mProject.ProjectProps.AnalysisParams.AnalyzeUncategorizedData;
}
}
#region Init and settings
public MainController(MainWindow win) {
mMainWin = win;
ScriptManager.UseKeepAliveHack = true;
}
/// <summary>
/// Early initialization, before the window is visible. Notably, we want to get the
/// window placement data, so we can position and size the window before it's first
/// drawn (avoids a blink).
/// </summary>
public void WindowSourceInitialized() {
// Load the settings from the file. If this fails we have no way to tell the user,
// so just keep going.
LoadAppSettings();
SetAppWindowLocation();
}
/// <summary>
/// Perform one-time initialization after the Window has finished loading. We defer
/// to this point so we can report fatal errors directly to the user.
/// </summary>
public void WindowLoaded() {
if (RuntimeDataAccess.GetDirectory() == null) {
MessageBox.Show(Res.Strings.RUNTIME_DIR_NOT_FOUND,
Res.Strings.RUNTIME_DIR_NOT_FOUND_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
try {
PluginDllCache.PreparePluginDir();
} catch (Exception ex) {
string pluginPath = PluginDllCache.GetPluginDirPath();
if (pluginPath == null) {
pluginPath = "<???>";
}
string msg = string.Format(Res.Strings.PLUGIN_DIR_FAIL_FMT,
pluginPath + ": " + ex.Message);
MessageBox.Show(msg, Res.Strings.PLUGIN_DIR_FAIL_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
// Place the main window and apply the various settings.
ApplyAppSettings();
UpdateTitle();
mMainWin.UpdateRecentLinks();
ProcessCommandLine();
// Create an initial value.
SelectionAnalysis = UpdateSelectionState();
}
private void ProcessCommandLine() {
string[] args = Environment.GetCommandLineArgs();
if (args.Length == 2) {
DoOpenFile(Path.GetFullPath(args[1]));
}
}
/// <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
// actually used is expected to do something reasonable by default.
settings.SetBool(AppSettings.SYMWIN_SHOW_USER, true);
settings.SetBool(AppSettings.SYMWIN_SHOW_PROJECT, true);
settings.SetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false);
settings.SetBool(AppSettings.SYMWIN_SHOW_AUTO, false);
settings.SetBool(AppSettings.SYMWIN_SHOW_CONST, true);
settings.SetBool(AppSettings.SYMWIN_SHOW_ADDR, true);
settings.SetBool(AppSettings.SYMWIN_SORT_ASCENDING, true);
settings.SetInt(AppSettings.SYMWIN_SORT_COL, (int)Symbol.SymbolSortField.Name);
settings.SetBool(AppSettings.FMT_UPPER_OPERAND_A, true);
settings.SetBool(AppSettings.FMT_UPPER_OPERAND_S, true);
settings.SetBool(AppSettings.FMT_ADD_SPACE_FULL_COMMENT, true);
settings.SetBool(AppSettings.FMT_SPACES_BETWEEN_BYTES, true);
settings.SetString(AppSettings.FMT_OPCODE_SUFFIX_LONG, "l");
settings.SetString(AppSettings.FMT_OPERAND_PREFIX_ABS, "a:");
settings.SetString(AppSettings.FMT_OPERAND_PREFIX_LONG, "f:");
settings.SetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, true);
settings.SetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, true);
#if DEBUG
settings.SetBool(AppSettings.DEBUG_MENU_ENABLED, true);
#else
settings.SetBool(AppSettings.DEBUG_MENU_ENABLED, false);
#endif
// Make sure we have entries for these.
settings.SetString(AppSettings.CDLV_FONT_FAMILY,
mMainWin.CodeListFontFamily.ToString());
settings.SetInt(AppSettings.CDLV_FONT_SIZE, (int)mMainWin.CodeListFontSize);
// Character and string delimiters.
Formatter.DelimiterSet chrDel = Formatter.DelimiterSet.GetDefaultCharDelimiters();
string chrSer = chrDel.Serialize();
settings.SetString(AppSettings.FMT_CHAR_DELIM, chrSer);
Formatter.DelimiterSet strDel = Formatter.DelimiterSet.GetDefaultStringDelimiters();
string strSer = strDel.Serialize();
settings.SetString(AppSettings.FMT_STRING_DELIM, strSer);
// Load the settings file, and merge it into the globals.
string runtimeDataDir = RuntimeDataAccess.GetDirectory();
if (runtimeDataDir == null) {
Debug.WriteLine("Unable to load settings file");
return;
}
string settingsDir = Path.GetDirectoryName(runtimeDataDir);
string settingsPath = Path.Combine(settingsDir, SETTINGS_FILE_NAME);
try {
string text = File.ReadAllText(settingsPath);
AppSettings fileSettings = AppSettings.Deserialize(text);
AppSettings.Global.MergeSettings(fileSettings);
Debug.WriteLine("Settings file loaded and merged");
} catch (Exception ex) {
Debug.WriteLine("Unable to read settings file: " + ex.Message);
}
}
/// <summary>
/// Saves AppSettings to a file.
/// </summary>
private void SaveAppSettings() {
if (!AppSettings.Global.Dirty) {
Debug.WriteLine("Settings not dirty, not saving");
return;
}
// Main window position and size.
AppSettings.Global.SetString(AppSettings.MAIN_WINDOW_PLACEMENT,
mMainWin.GetPlacement());
// Horizontal splitters.
AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH,
(int)mMainWin.LeftPanelWidth);
AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH,
(int)mMainWin.RightPanelWidth);
// Vertical splitters.
AppSettings.Global.SetInt(AppSettings.MAIN_REFERENCES_HEIGHT,
(int)mMainWin.ReferencesPanelHeight);
AppSettings.Global.SetInt(AppSettings.MAIN_SYMBOLS_HEIGHT,
(int)mMainWin.SymbolsPanelHeight);
mMainWin.CaptureColumnWidths();
string runtimeDataDir = RuntimeDataAccess.GetDirectory();
if (runtimeDataDir == null) {
Debug.WriteLine("Unable to save settings file");
return;
}
string settingsDir = Path.GetDirectoryName(runtimeDataDir);
string settingsPath = Path.Combine(settingsDir, SETTINGS_FILE_NAME);
try {
string cereal = AppSettings.Global.Serialize();
File.WriteAllText(settingsPath, cereal);
AppSettings.Global.Dirty = false;
Debug.WriteLine("Saved settings (" + settingsPath + ")");
} catch (Exception ex) {
Debug.WriteLine("Failed to save settings: " + ex.Message);
}
}
/// <summary>
/// Replaces the contents of the global settings object with the new settings,
/// then applies them to the project.
/// </summary>
/// <param name="settings">New 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. This should be called before the window has
/// finished initialization.
/// </summary>
private void SetAppWindowLocation() {
const int DEFAULT_SPLIT = 250;
AppSettings settings = AppSettings.Global;
string placement = settings.GetString(AppSettings.MAIN_WINDOW_PLACEMENT, null);
if (placement != null) {
mMainWin.SetPlacement(placement);
}
mMainWin.LeftPanelWidth =
settings.GetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH, DEFAULT_SPLIT);
mMainWin.RightPanelWidth =
settings.GetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH, DEFAULT_SPLIT);
mMainWin.ReferencesPanelHeight =
settings.GetInt(AppSettings.MAIN_REFERENCES_HEIGHT, 350);
mMainWin.SymbolsPanelHeight =
settings.GetInt(AppSettings.MAIN_SYMBOLS_HEIGHT, 400);
mMainWin.RestoreColumnWidths();
}
/// <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;
string chrDelCereal = settings.GetString(AppSettings.FMT_CHAR_DELIM, null);
if (chrDelCereal != null) {
mFormatterConfig.mCharDelimiters =
Formatter.DelimiterSet.Deserialize(chrDelCereal);
}
string strDelCereal = settings.GetString(AppSettings.FMT_STRING_DELIM, null);
if (strDelCereal != null) {
mFormatterConfig.mStringDelimiters =
Formatter.DelimiterSet.Deserialize(strDelCereal);
}
// Update the formatter, and null out mOutputFormatterCpuDef to force a refresh
// of related items.
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.DefaultPseudoOpNames;
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 = PseudoOp.PseudoOpNames.Merge(mPseudoOpNames, deser);
}
}
// Configure the Symbols window.
mMainWin.SymFilterUserLabels =
settings.GetBool(AppSettings.SYMWIN_SHOW_USER, false);
mMainWin.SymFilterAutoLabels =
settings.GetBool(AppSettings.SYMWIN_SHOW_AUTO, false);
mMainWin.SymFilterProjectSymbols =
settings.GetBool(AppSettings.SYMWIN_SHOW_PROJECT, false);
mMainWin.SymFilterPlatformSymbols =
settings.GetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false);
mMainWin.SymFilterConstants =
settings.GetBool(AppSettings.SYMWIN_SHOW_CONST, false);
mMainWin.SymFilterAddresses =
settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR, false);
// Get the configured font info. If nothing is configured, use whatever the
// code list happens to be using now.
string fontFamilyName = settings.GetString(AppSettings.CDLV_FONT_FAMILY, null);
if (fontFamilyName == null) {
fontFamilyName = mMainWin.CodeListFontFamily.ToString();
}
int size = settings.GetInt(AppSettings.CDLV_FONT_SIZE, -1);
if (size <= 0) {
size = (int)mMainWin.CodeListFontSize;
}
mMainWin.SetCodeListFont(fontFamilyName, size);
// Update the column widths. This was done earlier during init, but may need to be
// repeated if the show/hide buttons were used in Settings.
mMainWin.RestoreColumnWidths();
// Unpack the recent-project list.
UnpackRecentProjectList();
// Enable the DEBUG menu if configured.
mMainWin.ShowDebugMenu =
AppSettings.Global.GetBool(AppSettings.DEBUG_MENU_ENABLED, false);
// Finally, update the display list generator with all the fancy settings.
if (CodeLineList != null) {
// Regenerate the display list with the latest formatter config and
// pseudo-op definition. (These are set as part of the refresh.)
UndoableChange uc =
UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DisplayOnly);
ApplyChanges(new ChangeSet(uc), false);
}
}
private void UnpackRecentProjectList() {
RecentProjectPaths.Clear();
string cereal = AppSettings.Global.GetString(
AppSettings.PRVW_RECENT_PROJECT_LIST, null);
if (string.IsNullOrEmpty(cereal)) {
return;
}
try {
JavaScriptSerializer ser = new JavaScriptSerializer();
RecentProjectPaths = ser.Deserialize<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 = RecentProjectPaths.IndexOf(projectPath);
if (index == 0) {
// Already in the list, nothing changes. No need to update anything else.
return;
}
if (index > 0) {
RecentProjectPaths.RemoveAt(index);
}
RecentProjectPaths.Insert(0, projectPath);
// Trim the list to the max allowed.
while (RecentProjectPaths.Count > MAX_RECENT_PROJECTS) {
Debug.WriteLine("Recent projects: dropping " +
RecentProjectPaths[MAX_RECENT_PROJECTS]);
RecentProjectPaths.RemoveAt(MAX_RECENT_PROJECTS);
}
// Store updated list in app settings. JSON-in-JSON is ugly and inefficient,
// but it'll do for now.
JavaScriptSerializer ser = new JavaScriptSerializer();
string cereal = ser.Serialize(RecentProjectPaths);
AppSettings.Global.SetString(AppSettings.PRVW_RECENT_PROJECT_LIST, cereal);
mMainWin.UpdateRecentLinks();
}
/// <summary>
/// Updates the main form title to show project name and modification status.
/// </summary>
private void UpdateTitle() {
// Update main window title.
StringBuilder sb = new StringBuilder();
sb.Append(Res.Strings.TITLE_BASE);
if (mProject != null) {
sb.Append(" - ");
if (string.IsNullOrEmpty(mProjectPathName)) {
sb.Append(Res.Strings.TITLE_NEW_PROJECT);
} else {
sb.Append(Path.GetFileName(mProjectPathName));
}
if (mProject.IsDirty) {
sb.Append(" ");
sb.Append(Res.Strings.TITLE_MODIFIED);
}
}
mMainWin.Title = sb.ToString();
}
#endregion Init and settings
#region Project management
private bool PrepareNewProject(string dataPathName, SystemDef sysDef) {
DisasmProject proj = new DisasmProject();
mDataPathName = dataPathName;
mProjectPathName = string.Empty;
byte[] fileData = null;
try {
fileData = LoadDataFile(dataPathName);
} catch (Exception ex) {
Debug.WriteLine("PrepareNewProject exception: " + ex);
string message = Res.Strings.OPEN_DATA_FAIL_CAPTION;
string caption = Res.Strings.OPEN_DATA_FAIL_MESSAGE + ": " + ex.Message;
MessageBox.Show(caption, message, MessageBoxButton.OK,
MessageBoxImage.Error);
return false;
}
proj.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins;
proj.Initialize(fileData.Length);
proj.PrepForNew(fileData, Path.GetFileName(dataPathName));
// Initial header comment is the program name and version.
string cmt = string.Format(Res.Strings.DEFAULT_HEADER_COMMENT_FMT, App.ProgramVersion);
proj.LongComments.Add(LineListGen.Line.HEADER_COMMENT_OFFSET,
new MultiLineComment(cmt));
// 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;
}
#if false
private class FinishPrepProgress : WorkProgress.IWorker {
public string ExtMessages { get; private set; }
private MainController mMainCtrl;
public FinishPrepProgress(MainController mainCtrl) {
mMainCtrl = mainCtrl;
}
public object DoWork(BackgroundWorker worker) {
string messages = mMainCtrl.mProject.LoadExternalFiles();
mMainCtrl.DoRefreshProject(UndoableChange.ReanalysisScope.CodeAndData);
return messages;
}
public void RunWorkerCompleted(object results) {
ExtMessages = (string)results;
}
}
#endif
private void FinishPrep() {
CodeLineList = new LineListGen(mProject, mMainWin.CodeDisplayList,
mOutputFormatter, mPseudoOpNames);
string messages = mProject.LoadExternalFiles();
if (messages.Length != 0) {
// ProjectLoadIssues isn't quite the right dialog, but it'll do. This is
// purely informative; no decision needs to be made.
ProjectLoadIssues dlg = new ProjectLoadIssues(mMainWin, messages,
ProjectLoadIssues.Buttons.Continue);
dlg.ShowDialog();
}
// Ideally we'd call DoRefreshProject (and LoadExternalFiles) from a progress
// dialog, but we're not allowed to update the DisplayList from a different thread.
RefreshProject(UndoableChange.ReanalysisScope.CodeAndData);
// Populate the Symbols list.
PopulateSymbolsList();
// Load initial contents of Notes panel.
PopulateNotesList();
mMainWin.ShowCodeListView = true;
mNavStack.Clear();
UpdateRecentProjectList(mProjectPathName);
UpdateTitle();
}
/// <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(Res.Strings.OPEN_DATA_TOO_LARGE_FMT,
fs.Length / 1024, DisasmProject.MAX_DATA_FILE_SIZE / 1024));
} else if (fs.Length == 0) {
throw new InvalidDataException(Res.Strings.OPEN_DATA_EMPTY);
}
fileData = new byte[fs.Length];
int actual = fs.Read(fileData, 0, (int)fs.Length);
if (actual != fs.Length) {
// Not expected -- should be able to read the entire file in one shot.
throw new Exception(Res.Strings.OPEN_DATA_PARTIAL_READ);
}
}
return fileData;
}
/// <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);
UpdateTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.DisplayText = 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");
mMainWin.CodeListView_DebugValidateSelectionCount();
int topItemIndex = mMainWin.CodeListView_GetTopIndex();
LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate(
CodeLineList, mMainWin.CodeDisplayList.SelectedIndices, topItemIndex);
//savedSel.DebugDump();
// Clear this so we don't try to fiddle with it later.
mTargetHighlightIndex = -1;
mReanalysisTimer.EndTask("Save selection");
mReanalysisTimer.StartTask("Apply changes");
UndoableChange.ReanalysisScope needReanalysis = mProject.ApplyChanges(cs, backward,
out RangeSet affectedOffsets);
mReanalysisTimer.EndTask("Apply changes");
string refreshTaskStr = "Refresh w/reanalysis=" + needReanalysis;
mReanalysisTimer.StartTask(refreshTaskStr);
if (needReanalysis != UndoableChange.ReanalysisScope.None) {
Debug.WriteLine("Refreshing project (" + needReanalysis + ")");
RefreshProject(needReanalysis);
} else {
Debug.WriteLine("Refreshing " + affectedOffsets.Count + " offsets");
RefreshCodeListViewEntries(affectedOffsets);
mProject.Validate(); // shouldn't matter w/o reanalysis, but do it anyway
}
mReanalysisTimer.EndTask(refreshTaskStr);
DisplayListSelection newSel = savedSel.Restore(CodeLineList, out topItemIndex);
//newSel.DebugDump();
// Restore the selection. The selection-changed event will cause updates to the
// references, notes, and info panels.
mReanalysisTimer.StartTask("Restore selection and top position");
mMainWin.CodeListView_SetSelection(newSel);
mMainWin.CodeListView_SetTopIndex(topItemIndex);
mReanalysisTimer.EndTask("Restore selection and top position");
// Update the Notes and Symbols windows. References should refresh automatically
// when the selection is restored.
PopulateNotesList();
PopulateSymbolsList();
mReanalysisTimer.EndTask("ProjectView.ApplyChanges()");
//mReanalysisTimer.DumpTimes("ProjectView timers:", mGenerationLog);
if (mShowAnalysisTimersDialog != null) {
string timerStr = mReanalysisTimer.DumpToString("ProjectView timers:");
mShowAnalysisTimersDialog.DisplayText = 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.
// (This may not be necessary with WPF, because the way highlights work changed.)
UpdateSelectionHighlight();
}
/// <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;
CodeLineList.GenerateRange(range.Low, range.High);
}
}
/// <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.
if (mProject.FileDataLength > 65536) {
try {
Mouse.OverrideCursor = Cursors.Wait;
DoRefreshProject(reanalysisRequired);
} finally {
Mouse.OverrideCursor = null;
}
} else {
DoRefreshProject(reanalysisRequired);
}
if (mGenerationLog != null) {
//mReanalysisTimer.StartTask("Save _log");
//mGenerationLog.WriteToFile(@"C:\Src\WorkBench\SourceGen\TestData\_log.txt");
//mReanalysisTimer.EndTask("Save _log");
if (mShowAnalyzerOutputDialog != null) {
mShowAnalyzerOutputDialog.DisplayText = mGenerationLog.WriteToString();
}
}
if (FormatDescriptor.DebugCreateCount != 0) {
Debug.WriteLine("FormatDescriptor total=" + FormatDescriptor.DebugCreateCount +
" prefab=" + FormatDescriptor.DebugPrefabCount + " (" +
(FormatDescriptor.DebugPrefabCount * 100) / FormatDescriptor.DebugCreateCount +
"%)");
}
}
/// <summary>
/// Refreshes the project after something of substance has changed.
/// </summary>
/// <remarks>
/// Ideally from this point on we can run on a background thread. The tricky part
/// is the close relationship between LineListGen and DisplayList -- we can't update
/// DisplayList from a background thread. Until that's fixed, putting up a "working..."
/// dialog or other UI will be awkward.
/// </remarks>
/// <param name="reanalysisRequired">Indicates whether reanalysis is required, and
/// what level.</param>
private void DoRefreshProject(UndoableChange.ReanalysisScope reanalysisRequired) {
// Changing the CPU type or whether undocumented instructions are supported
// invalidates the Formatter's mnemonic cache. We can change these values
// through undo/redo, so we need to check it here.
if (mOutputFormatterCpuDef != mProject.CpuDef) { // reference equality is fine
Debug.WriteLine("CpuDef has changed, resetting formatter (now " +
mProject.CpuDef + ")");
mOutputFormatter = new Formatter(mFormatterConfig);
CodeLineList.SetFormatter(mOutputFormatter);
CodeLineList.SetPseudoOpNames(mPseudoOpNames);
mOutputFormatterCpuDef = mProject.CpuDef;
}
if (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()");
}
mReanalysisTimer.StartTask("Generate DisplayList");
CodeLineList.GenerateAll();
mReanalysisTimer.EndTask("Generate DisplayList");
}
#endregion Project management
#region Main window UI event handlers
public void NewProject() {
if (!CloseProject()) {
return;
}
string sysDefsPath = RuntimeDataAccess.GetPathName("SystemDefs.json");
if (sysDefsPath == null) {
MessageBox.Show(Res.Strings.ERR_LOAD_CONFIG_FILE, Res.Strings.OPERATION_FAILED,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
SystemDefSet sds = null;
try {
sds = SystemDefSet.ReadFile(sysDefsPath);
} catch (Exception ex) {
Debug.WriteLine("Failed loading system def set: " + ex);
MessageBox.Show(Res.Strings.ERR_LOAD_CONFIG_FILE, Res.Strings.OPERATION_FAILED,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
NewProject dlg = new NewProject(mMainWin, sds);
if (dlg.ShowDialog() != true) {
return;
}
bool ok = PrepareNewProject(Path.GetFullPath(dlg.DataFileName), dlg.SystemDef);
if (ok) {
FinishPrep();
}
}
public void OpenRecentProject(int projIndex) {
if (!CloseProject()) {
return;
}
DoOpenFile(RecentProjectPaths[projIndex]);
}
/// <summary>
/// Handles opening an existing project by letting the user select the project file.
/// </summary>
public void OpenProject() {
if (!CloseProject()) {
return;
}
OpenFileDialog fileDlg = new OpenFileDialog() {
Filter = ProjectFile.FILENAME_FILTER + "|" + Res.Strings.FILE_FILTER_ALL,
FilterIndex = 1
};
if (fileDlg.ShowDialog() != true) {
return;
}
string projPathName = Path.GetFullPath(fileDlg.FileName);
DoOpenFile(projPathName);
}
/// <summary>
/// Handles opening an existing project, given a full pathname to the project file.
/// </summary>
private void DoOpenFile(string projPathName) {
Debug.WriteLine("DoOpenFile: " + projPathName);
Debug.Assert(mProject == null);
if (!File.Exists(projPathName)) {
// Should only happen for projects in "recents".
string msg = string.Format(Res.Strings.ERR_FILE_NOT_FOUND_FMT, projPathName);
MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
DisasmProject newProject = new DisasmProject();
newProject.UseMainAppDomainForPlugins = mUseMainAppDomainForPlugins;
// Deserialize the project file. I want to do this before loading the data file
// in case we decide to store the data file name in the project (e.g. the data
// file is a disk image or zip archive, and we need to know which part(s) to
// extract).
if (!ProjectFile.DeserializeFromFile(projPathName, newProject,
out FileLoadReport report)) {
// Should probably use a less-busy dialog for something simple like
// "permission denied", but the open file dialog handles most simple
// stuff directly.
ProjectLoadIssues dlg = new ProjectLoadIssues(mMainWin, report.Format(),
ProjectLoadIssues.Buttons.Cancel);
dlg.ShowDialog();
// ignore dlg.DialogResult
return;
}
// Now open the data file, generating the pathname by stripping off the ".dis65"
// extension. If we can't find the file, show a message box and offer the option to
// locate it manually, repeating the process until successful or canceled.
const string UNKNOWN_FILE = "UNKNOWN";
string dataPathName;
if (projPathName.Length <= ProjectFile.FILENAME_EXT.Length) {
dataPathName = UNKNOWN_FILE;
} else {
dataPathName = projPathName.Substring(0,
projPathName.Length - ProjectFile.FILENAME_EXT.Length);
}
byte[] fileData;
while ((fileData = FindValidDataFile(ref dataPathName, newProject,
out bool cancel)) == null) {
if (cancel) {
// give up
Debug.WriteLine("Abandoning attempt to open project");
return;
}
}
newProject.SetFileData(fileData, Path.GetFileName(dataPathName), ref report);
// If there were warnings, notify the user and give the a chance to cancel.
if (report.Count != 0) {
ProjectLoadIssues dlg = new ProjectLoadIssues(mMainWin, report.Format(),
ProjectLoadIssues.Buttons.ContinueOrCancel);
bool? ok = dlg.ShowDialog();
if (ok != true) {
return;
}
}
mProject = newProject;
mProjectPathName = mProject.ProjectPathName = projPathName;
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,
Res.Strings.OPEN_DATA_DOESNT_EXIST);
cancel = (dataPathName == null);
return null;
}
if (fi.Length != proj.FileDataLength) {
Debug.WriteLine("File '" + dataPathName + "' has length=" + fi.Length +
", expected " + proj.FileDataLength);
dataPathName = ChooseDataFile(dataPathName,
string.Format(Res.Strings.OPEN_DATA_WRONG_LENGTH_FMT,
fi.Length, proj.FileDataLength));
cancel = (dataPathName == null);
return null;
}
byte[] fileData = null;
try {
fileData = LoadDataFile(dataPathName);
} catch (Exception ex) {
Debug.WriteLine("File '" + dataPathName + "' failed to load: " + ex.Message);
dataPathName = ChooseDataFile(dataPathName,
string.Format(Res.Strings.OPEN_DATA_LOAD_FAILED_FMT, ex.Message));
cancel = (dataPathName == null);
return null;
}
uint crc = CRC32.OnWholeBuffer(0, fileData);
if (crc != proj.FileDataCrc32) {
Debug.WriteLine("File '" + dataPathName + "' has CRC32=" + crc +
", expected " + proj.FileDataCrc32);
// Format the CRC as signed decimal, so that interested parties can
// easily replace the value in the .dis65 file.
dataPathName = ChooseDataFile(dataPathName,
string.Format(Res.Strings.OPEN_DATA_WRONG_CRC_FMT,
(int)crc, (int)proj.FileDataCrc32));
cancel = (dataPathName == null);
return null;
}
cancel = false;
return fileData;
}
/// <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(mMainWin, origPath, errorMsg);
bool? ok = dlg.ShowDialog();
if (ok != true) {
return null;
}
OpenFileDialog fileDlg = new OpenFileDialog() {
FileName = Path.GetFileName(origPath),
Filter = Res.Strings.FILE_FILTER_ALL
};
if (fileDlg.ShowDialog() != true) {
return null;
}
string newPath = Path.GetFullPath(fileDlg.FileName);
Debug.WriteLine("User selected data file " + newPath);
return newPath;
}
/// <summary>
/// Saves the project, querying for the filename.
/// </summary>
/// <returns>True on success, false if the save attempt failed or was canceled.</returns>
public bool SaveProjectAs() {
SaveFileDialog fileDlg = new SaveFileDialog() {
Filter = ProjectFile.FILENAME_FILTER + "|" + Res.Strings.FILE_FILTER_ALL,
FilterIndex = 1,
ValidateNames = true,
AddExtension = true,
FileName = Path.GetFileName(mDataPathName) + ProjectFile.FILENAME_EXT
};
if (fileDlg.ShowDialog() != true) {
Debug.WriteLine("SaveAs canceled by user");
return false;
}
string pathName = Path.GetFullPath(fileDlg.FileName);
Debug.WriteLine("Project save path: " + pathName);
if (!DoSave(pathName)) {
return false;
}
// Success, record the path name.
mProjectPathName = mProject.ProjectPathName = pathName;
// add it to the title bar
UpdateTitle();
return true;
}
/// <summary>
/// Saves the project. If it hasn't been saved before, use save-as behavior instead.
/// </summary>
/// <returns>True on success, false if the save attempt failed.</returns>
public bool SaveProject() {
if (string.IsNullOrEmpty(mProjectPathName)) {
return SaveProjectAs();
}
return DoSave(mProjectPathName);
}
private bool DoSave(string pathName) {
Debug.WriteLine("SAVING " + pathName);
if (!ProjectFile.SerializeToFile(mProject, pathName, out string errorMessage)) {
MessageBox.Show(Res.Strings.ERR_PROJECT_SAVE_FAIL + ": " + errorMessage,
Res.Strings.OPERATION_FAILED,
MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
mProject.ResetDirtyFlag();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.DisplayText = mProject.DebugGetUndoRedoHistory();
}
UpdateTitle();
// Update this, in case this was a new project.
UpdateRecentProjectList(pathName);
// Seems like a good time to save this off too.
SaveAppSettings();
return true;
}
/// <summary>
/// Handles main window closing.
/// </summary>
/// <returns>True if it's okay for the window to close, false to cancel it.</returns>
public bool WindowClosing() {
SaveAppSettings();
if (!CloseProject()) {
return false;
}
// WPF won't exit until all windows are closed, so any unowned windows need
// to be cleaned up here.
mAsciiChartDialog?.Close();
mHexDumpDialog?.Close();
mShowAnalysisTimersDialog?.Close();
mShowAnalyzerOutputDialog?.Close();
mShowUndoRedoHistoryDialog?.Close();
while (mUnownedWindows.Count > 0) {
int count = mUnownedWindows.Count;
mUnownedWindows[0].Close();
if (count == mUnownedWindows.Count) {
// Window failed to remove itself; this will cause an infinite loop.
// The user will have to close them manually.
Debug.Assert(false, "Failed to close window " + mUnownedWindows[0]);
break;
}
}
return true;
}
/// <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>
public bool CloseProject() {
Debug.WriteLine("CloseProject() - dirty=" +
(mProject == null ? "N/A" : mProject.IsDirty.ToString()));
if (mProject != null && mProject.IsDirty) {
DiscardChanges dlg = new DiscardChanges(mMainWin);
bool? ok = dlg.ShowDialog();
if (ok != true) {
return false;
} else if (dlg.UserChoice == DiscardChanges.Choice.SaveAndContinue) {
if (!SaveProject()) {
return false;
}
}
}
// Close modeless dialogs that depend on project.
mHexDumpDialog?.Close();
mShowAnalysisTimersDialog?.Close();
mShowAnalyzerOutputDialog?.Close();
mShowUndoRedoHistoryDialog?.Close();
// Discard all project state.
if (mProject != null) {
mProject.Cleanup();
mProject = null;
}
mDataPathName = null;
mProjectPathName = null;
mTargetHighlightIndex = -1;
mMainWin.ShowCodeListView = false;
mMainWin.ProjectClosing();
mGenerationLog = null;
// Not necessary, but it lets us check the memory monitor to see if we got
// rid of everything.
GC.Collect();
return true;
}
public bool IsProjectOpen() {
return mProject != null;
}
public void AssembleProject() {
if (string.IsNullOrEmpty(mProjectPathName)) {
// We need a project pathname so we know where to write the assembler
// source files, and what to call the output files. We could just pop up the
// Save As dialog, but that seems confusing unless we do a custom dialog with
// an explanation, or have some annoying click-through.
//
// This only appears for never-saved projects, not projects with unsaved data.
MessageBox.Show(Res.Strings.SAVE_BEFORE_ASM, Res.Strings.SAVE_BEFORE_ASM_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
AsmGen.WpfGui.GenAndAsm dlg =
new AsmGen.WpfGui.GenAndAsm(mMainWin, this, mProject, mProjectPathName);
dlg.ShowDialog();
}
/// <summary>
/// Copies the selection to the clipboard as formatted text.
/// </summary>
public void CopyToClipboard() {
const bool addCsv = true;
ClipLineFormat format = (ClipLineFormat)AppSettings.Global.GetEnum(
AppSettings.CLIP_LINE_FORMAT,
typeof(ClipLineFormat),
(int)ClipLineFormat.AssemblerSource);
DisplayListSelection selection = mMainWin.CodeDisplayList.SelectedIndices;
StringBuilder fullText = new StringBuilder(selection.Count * 50);
StringBuilder csv = new StringBuilder(selection.Count * 40);
StringBuilder sb = new StringBuilder(100);
int addrAdj = mProject.CpuDef.HasAddr16 ? 6 : 9;
int disAdj = 0;
int bytesWidth = 0;
if (format == ClipLineFormat.Disassembly) {
// A limit of 8 gets us 4 bytes from dense display ("20edfd60") and 3 if spaces
// are included ("20 ed fd") with no excess. We want to increase it to 11 so
// we can always show 4 bytes.
bytesWidth = (mFormatterConfig.mSpacesBetweenBytes ? 11 : 8);
disAdj = addrAdj + bytesWidth + 2;
}
// 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.)
foreach (int index in selection) {
LineListGen.Line line = CodeLineList[index];
DisplayList.FormattedParts parts = CodeLineList.GetFormattedParts(index);
switch (line.LineType) {
case LineListGen.Line.Type.Code:
case LineListGen.Line.Type.Data:
case LineListGen.Line.Type.EquDirective:
case LineListGen.Line.Type.RegWidthDirective:
case LineListGen.Line.Type.OrgDirective:
if (format == ClipLineFormat.Disassembly) {
if (!string.IsNullOrEmpty(parts.Addr)) {
sb.Append(parts.Addr);
sb.Append(": ");
}
// Shorten the "...".
string bytesStr = parts.Bytes;
if (bytesStr != null && bytesStr.Length > bytesWidth) {
bytesStr = bytesStr.Substring(0, bytesWidth) + "+";
}
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 LineListGen.Line.Type.LongComment:
if (format == ClipLineFormat.Disassembly) {
TextUtil.AppendPaddedString(sb, string.Empty, disAdj);
}
sb.Append(parts.Comment);
sb.Append("\r\n");
break;
case LineListGen.Line.Type.Note:
// don't include notes
break;
case LineListGen.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();
}
DataObject dataObject = new DataObject();
dataObject.SetText(fullText.ToString());
// 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
if (addCsv) {
byte[] csvData = Encoding.UTF8.GetBytes(csv.ToString());
MemoryStream stream = new MemoryStream(csvData);
dataObject.SetData(DataFormats.CommaSeparatedValue, stream);
}
Clipboard.SetDataObject(dataObject, true);
}
/// <summary>
/// Opens the application settings dialog. All changes to settings are made directly
/// to the AppSettings.Global object.
/// </summary>
public void EditAppSettings() {
ShowAppSettings(mMainWin, WpfGui.EditAppSettings.Tab.Unknown,
AsmGen.AssemblerInfo.Id.Unknown);
}
public void ShowAppSettings(Window owner, EditAppSettings.Tab initialTab,
AsmGen.AssemblerInfo.Id initialAsmId) {
EditAppSettings dlg = new EditAppSettings(owner, mMainWin, this,
initialTab, initialAsmId);
dlg.ShowDialog();
// The settings code calls SetAppSettings() directly whenever "Apply" is hit.
}
public void HandleCodeListDoubleClick(int row, int col) {
//Debug.WriteLine("DCLICK: row=" + row + " col=" + col);
mMainWin.CodeListView_DebugValidateSelectionCount();
// Clicking on some types of lines, such as ORG directives, results in
// specific behavior regardless of which column you click in. We're just
// checking the clicked-on line to decide what action to take. If it doesn't
// make sense to do for a multi-line selection, the action will have been
// disabled.
LineListGen.Line line = CodeLineList[row];
switch (line.LineType) {
case LineListGen.Line.Type.EquDirective:
// Currently only does something for project symbols; platform symbols
// do nothing.
if (CanEditProjectSymbol()) {
EditProjectSymbol();
}
break;
case LineListGen.Line.Type.OrgDirective:
if (CanEditAddress()) {
EditAddress();
}
break;
case LineListGen.Line.Type.RegWidthDirective:
if (CanEditStatusFlags()) {
EditStatusFlags();
}
break;
case LineListGen.Line.Type.LongComment:
if (CanEditLongComment()) {
EditLongComment();
}
break;
case LineListGen.Line.Type.Note:
if (CanEditNote()) {
EditNote();
}
break;
case LineListGen.Line.Type.Code:
case LineListGen.Line.Type.Data:
// For code and data, we have to break it down by column.
switch ((CodeListColumn)col) {
case CodeListColumn.Offset:
// does nothing
break;
case CodeListColumn.Address:
// edit address
if (CanEditAddress()) {
EditAddress();
}
break;
case CodeListColumn.Bytes:
ShowHexDump();
break;
case CodeListColumn.Flags:
if (CanEditStatusFlags()) {
EditStatusFlags();
}
break;
case CodeListColumn.Attributes:
// does nothing
break;
case CodeListColumn.Label:
if (CanEditLabel()) {
EditLabel();
}
break;
case CodeListColumn.Opcode:
// File offset should always be valid, since we excluded the EQU
// statements and header comment earlier.
if (line.FileOffset >= 0) {
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
FormatDescriptor dfd = attr.DataDescriptor;
// Does this have an operand with an in-file target offset?
// (Resolve it as a numeric reference.)
if (attr.OperandOffset >= 0) {
// Yup, find the line for that offset and jump to it.
GoToOffset(attr.OperandOffset, false, true);
} else if (dfd != null && dfd.HasSymbol) {
// Operand has a symbol, do a symbol lookup.
int labelOffset = mProject.FindLabelOffsetByName(
dfd.SymbolRef.Label);
if (labelOffset >= 0) {
GoToOffset(labelOffset, false, true);
}
} else if (attr.IsDataStart || attr.IsInlineDataStart) {
// If it's an Address or Symbol, we can try to resolve
// the value. (Symbols should have been resolved by the
// previous clause, but Address entries would not have been.)
int operandOffset = DataAnalysis.GetDataOperandOffset(
mProject, line.FileOffset);
if (operandOffset >= 0) {
GoToOffset(operandOffset, false, true);
}
}
}
break;
case CodeListColumn.Operand:
if (CanEditOperand()) {
EditOperand();
}
break;
case CodeListColumn.Comment:
if (CanEditComment()) {
EditComment();
}
break;
}
break;
default:
Debug.WriteLine("Double-click: unhandled line type " + line.LineType);
break;
}
}
public bool CanDeleteMlc() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
return (SelectionAnalysis.mLineType == LineListGen.Line.Type.LongComment ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.Note);
}
// Delete multi-line comment (Note or LongComment)
public void DeleteMlc() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line line = CodeLineList[selIndex];
int offset = line.FileOffset;
UndoableChange uc;
if (line.LineType == LineListGen.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 == LineListGen.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);
}
public bool CanEditAddress() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Line must be code, data, or an ORG directive.
return (counts.mDataLines > 0 || counts.mCodeLines > 0) ||
(SelectionAnalysis.mLineType == LineListGen.Line.Type.OrgDirective);
}
public void EditAddress() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
Anattrib attr = mProject.GetAnattrib(offset);
EditAddress dlg = new EditAddress(mMainWin, attr.Address, mProject.CpuDef.MaxAddressValue);
if (dlg.ShowDialog() != true) {
return;
}
if (offset == 0 && dlg.Address < 0) {
// Not allowed. The AddressMap will just put it back, which confuses
// the undo operation.
Debug.WriteLine("EditAddress: not allowed to remove address at offset +000000");
} else if (attr.Address != dlg.Address) {
Debug.WriteLine("EditAddress: changing addr at offset +" + offset.ToString("x6") +
" to " + dlg.Address);
AddressMap addrMap = mProject.AddrMap;
// Get the previous address map entry for this exact offset, if one
// exists. This may be different from the value used as the default
// (attr.Address), which is the address assigned to the offset, in
// the case where no previous mapping existed.
int prevAddress = addrMap.Get(offset);
UndoableChange uc = UndoableChange.CreateAddressChange(offset,
prevAddress, dlg.Address);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
} else {
Debug.WriteLine("EditAddress: no change");
}
}
public bool CanEditComment() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
// Line must be code or data.
return (SelectionAnalysis.mLineType == LineListGen.Line.Type.Code ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.Data);
}
public void EditComment() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
string oldComment = mProject.Comments[offset];
EditComment dlg = new EditComment(mMainWin, oldComment);
if (dlg.ShowDialog() == true) {
if (!oldComment.Equals(dlg.CommentText)) {
Debug.WriteLine("Changing comment at +" + offset.ToString("x6"));
UndoableChange uc = UndoableChange.CreateCommentChange(offset,
oldComment, dlg.CommentText);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
}
public void EditHeaderComment() {
EditLongComment(LineListGen.Line.HEADER_COMMENT_OFFSET);
}
public bool CanEditLabel() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, code or data.
return (counts.mDataLines > 0 || counts.mCodeLines > 0);
}
public void EditLabel() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
Anattrib attr = mProject.GetAnattrib(offset);
EditLabel dlg = new EditLabel(mMainWin, attr.Symbol, attr.Address,
mProject.SymbolTable);
if (dlg.ShowDialog() != true) {
return;
}
// NOTE: if label matching is case-insensitive, we want to allow a situation
// where a label is being renamed from "FOO" to "Foo". (We should be able to
// test for object equality on the Symbol.)
if (attr.Symbol != dlg.LabelSym) {
Debug.WriteLine("Changing label at offset +" + offset.ToString("x6"));
// For undo/redo, we want to update the UserLabels value. This may
// be different from the Anattrib symbol, which can have an auto-generated
// value.
Symbol oldUserValue = null;
if (mProject.UserLabels.ContainsKey(offset)) {
oldUserValue = mProject.UserLabels[offset];
}
UndoableChange uc = UndoableChange.CreateLabelChange(offset,
oldUserValue, dlg.LabelSym);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
public bool CanEditLongComment() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, code or data, or a long comment.
return (counts.mDataLines > 0 || counts.mCodeLines > 0 ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.LongComment);
}
public void EditLongComment() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
EditLongComment(offset);
}
private void EditLongComment(int offset) {
EditLongComment dlg = new EditLongComment(mMainWin, mOutputFormatter);
if (mProject.LongComments.TryGetValue(offset, out MultiLineComment oldComment)) {
dlg.LongComment = oldComment;
}
dlg.ShowDialog();
if (dlg.DialogResult == true) {
MultiLineComment newComment = dlg.LongComment;
if (oldComment != newComment) {
Debug.WriteLine("Changing long comment at +" + offset.ToString("x6"));
UndoableChange uc = UndoableChange.CreateLongCommentChange(offset,
oldComment, newComment);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
}
public bool CanEditNote() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, code or data, or a note.
return (counts.mDataLines > 0 || counts.mCodeLines > 0 ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.Note);
}
public void EditNote() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
MultiLineComment oldNote;
if (!mProject.Notes.TryGetValue(offset, out oldNote)) {
oldNote = null;
}
EditNote dlg = new EditNote(mMainWin, oldNote);
dlg.ShowDialog();
if (dlg.DialogResult != true) {
return;
}
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);
}
}
public bool CanEditOperand() {
if (SelectionAnalysis.mNumItemsSelected == 0) {
return false;
} else if (SelectionAnalysis.mNumItemsSelected == 1) {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int selOffset = CodeLineList[selIndex].FileOffset;
bool editInstr = (CodeLineList[selIndex].LineType == LineListGen.Line.Type.Code &&
mProject.GetAnattrib(selOffset).IsInstructionWithOperand);
bool editData = (CodeLineList[selIndex].LineType == LineListGen.Line.Type.Data);
return editInstr || editData;
} else {
// Data operands are one of the few things we can edit in bulk. It's okay
// if meta-data like ORGs and Notes are selected, but we don't allow it if
// any code is selected.
EntityCounts counts = SelectionAnalysis.mEntityCounts;
return (counts.mDataLines > 0 && counts.mCodeLines == 0);
}
}
public void EditOperand() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int selOffset = CodeLineList[selIndex].FileOffset;
if (CodeLineList[selIndex].LineType == LineListGen.Line.Type.Code) {
EditInstructionOperand(selOffset);
} else {
Debug.Assert(CodeLineList[selIndex].LineType == LineListGen.Line.Type.Data);
EditDataOperand(selOffset);
}
}
private void EditInstructionOperand(int offset) {
EditInstructionOperand dlg = new EditInstructionOperand(mMainWin, offset,
mProject, mOutputFormatter);
// We'd really like to pass in an indication of what the "default" format actually
// resolved to, but we don't always know. If this offset has a FormatDescriptor,
// we might not have auto-generated the label that would have been used otherwise.
// We're editing the FormatDescriptor from OperandFormats, not Anattribs;
// the latter may have auto-generated stuff.
if (mProject.OperandFormats.TryGetValue(offset, out FormatDescriptor dfd)) {
dlg.FormatDescriptor = dfd;
}
dlg.ShowDialog();
if (dlg.DialogResult != true) {
return;
}
ChangeSet cs = new ChangeSet(1);
// Handle shortcut actions.
if (dlg.FormatDescriptor != dfd && dlg.ShortcutAction !=
WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelInstead) {
// Note EditOperand returns a null descriptor when the user selects Default.
// This is different from how EditData works, since that has to deal with
// multiple regions.
Debug.WriteLine("Changing " + dfd + " to " + dlg.FormatDescriptor);
UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset,
dfd, dlg.FormatDescriptor);
cs.Add(uc);
} else if (dfd != null && dlg.ShortcutAction ==
WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelInstead) {
Debug.WriteLine("Removing existing label for CreateLabelInstead");
UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset,
dfd, null);
cs.Add(uc);
} else {
Debug.WriteLine("No change to format descriptor");
}
switch (dlg.ShortcutAction) {
case WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelInstead:
case WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateLabelAlso:
Debug.Assert(!mProject.UserLabels.ContainsKey(dlg.ShortcutArg));
Anattrib targetAttr = mProject.GetAnattrib(dlg.ShortcutArg);
Symbol newLabel = new Symbol(dlg.FormatDescriptor.SymbolRef.Label,
targetAttr.Address, Symbol.Source.User, Symbol.Type.LocalOrGlobalAddr);
UndoableChange uc = UndoableChange.CreateLabelChange(dlg.ShortcutArg,
null, newLabel);
cs.Add(uc);
break;
case WpfGui.EditInstructionOperand.SymbolShortcutAction.CreateProjectSymbolAlso:
Debug.Assert(!mProject.ProjectProps.ProjectSyms.ContainsKey(
dlg.FormatDescriptor.SymbolRef.Label));
DefSymbol defSym = new DefSymbol(dlg.FormatDescriptor.SymbolRef.Label,
dlg.ShortcutArg, Symbol.Source.Project, Symbol.Type.ExternalAddr,
FormatDescriptor.SubType.Hex, string.Empty, string.Empty);
ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps);
newProps.ProjectSyms.Add(defSym.Label, defSym);
uc = UndoableChange.CreateProjectPropertiesChange(
mProject.ProjectProps, newProps);
cs.Add(uc);
break;
case WpfGui.EditInstructionOperand.SymbolShortcutAction.None:
break;
}
if (cs.Count != 0) {
ApplyUndoableChanges(cs);
}
}
private void EditDataOperand(int offset) {
Debug.Assert(mMainWin.CodeListView_GetSelectionCount() > 0);
TypedRangeSet trs = GroupedOffsetSetFromSelected();
if (trs.Count == 0) {
Debug.Assert(false, "EditDataOperand found nothing to edit"); // shouldn't happen
return;
}
// If the first offset has a FormatDescriptor, pass that in as a recommendation
// for the default value in the dialog. This allows single-item editing to work
// as expected. If the format can't be applied to the full selection (which
// would disable that radio button), the dialog will have to pick something
// that does work.
//
// We could pull this out of Anattribs, which would let the dialog reflect the
// auto-format that the user was just looking at. However, I think it's better
// if the dialog shows what's actually there, i.e. no formatting at all.
IEnumerator<TypedRangeSet.Tuple> iter =
(IEnumerator<TypedRangeSet.Tuple>)trs.GetEnumerator();
iter.MoveNext();
TypedRangeSet.Tuple firstOffset = iter.Current;
mProject.OperandFormats.TryGetValue(firstOffset.Value, out FormatDescriptor dfd);
EditDataOperand dlg = new EditDataOperand(mMainWin, mProject.FileData,
mProject.SymbolTable, mOutputFormatter, trs, dfd);
dlg.ShowDialog();
if (dlg.DialogResult == true) {
// Merge the changes into the OperandFormats list. We need to remove all
// FormatDescriptors that overlap the selected region. We don't need to
// pass the selection set in, because the dlg.Results list spans the exact
// set of ranges.
//
// If nothing actually changed, don't generate an undo record.
ChangeSet cs = mProject.GenerateFormatMergeSet(dlg.Results);
if (cs.Count != 0) {
ApplyUndoableChanges(cs);
} else {
Debug.WriteLine("No change to data formats");
}
}
}
public bool CanEditProjectSymbol() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
if (SelectionAnalysis.mLineType != LineListGen.Line.Type.EquDirective) {
return false;
}
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int symIndex = LineListGen.DefSymIndexFromOffset(CodeLineList[selIndex].FileOffset);
DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex];
return (defSym.SymbolSource == Symbol.Source.Project);
}
public void EditProjectSymbol() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int symIndex = LineListGen.DefSymIndexFromOffset(CodeLineList[selIndex].FileOffset);
DefSymbol origDefSym = mProject.ActiveDefSymbolList[symIndex];
Debug.Assert(origDefSym.SymbolSource == Symbol.Source.Project);
EditDefSymbol dlg = new EditDefSymbol(mMainWin, mOutputFormatter,
mProject.ProjectProps.ProjectSyms);
dlg.DefSym = origDefSym;
if (dlg.ShowDialog() == true) {
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);
}
}
public bool CanEditStatusFlags() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, must be code or a RegWidth directive.
return (SelectionAnalysis.mLineType == LineListGen.Line.Type.Code ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.RegWidthDirective);
}
public void EditStatusFlags() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
EditStatusFlags dlg = new EditStatusFlags(mMainWin,
mProject.StatusFlagOverrides[offset], mProject.CpuDef.HasEmuFlag);
if (dlg.ShowDialog() != true) {
return;
}
if (dlg.FlagValue != mProject.StatusFlagOverrides[offset]) {
UndoableChange uc = UndoableChange.CreateStatusFlagChange(offset,
mProject.StatusFlagOverrides[offset], dlg.FlagValue);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
public void EditProjectProperties() {
string projectDir = string.Empty;
if (!string.IsNullOrEmpty(mProjectPathName)) {
projectDir = Path.GetDirectoryName(mProjectPathName);
}
EditProjectProperties dlg = new EditProjectProperties(mMainWin, mProject.ProjectProps,
projectDir, mOutputFormatter);
dlg.ShowDialog();
ProjectProperties newProps = dlg.NewProps;
// The dialog result doesn't matter, because the user might have hit "apply"
// before hitting "cancel".
if (newProps != null) {
UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(
mProject.ProjectProps, newProps);
ApplyUndoableChanges(new ChangeSet(uc));
}
}
public void Find() {
FindBox dlg = new FindBox(mMainWin, mFindString);
if (dlg.ShowDialog() == true) {
mFindString = dlg.TextToFind;
mFindStartIndex = -1;
FindText();
}
}
public void FindNext() {
FindText();
}
private void FindText() {
if (string.IsNullOrEmpty(mFindString)) {
return;
}
// Start from the topmost selected line, or the start of the file if nothing
// is selected.
int index = mMainWin.CodeListView_GetFirstSelectedIndex();
if (index < 0) {
index = 0;
}
// Start one past the selected item.
index++;
if (index == CodeLineList.Count) {
index = 0;
}
//Debug.WriteLine("FindText index=" + index + " start=" + mFindStartIndex +
// " str=" + mFindString);
while (index != mFindStartIndex) {
if (mFindStartIndex < 0) {
// need to latch this inside the loop so the initial test doesn't fail
mFindStartIndex = index;
}
string searchStr = CodeLineList.GetSearchString(index);
int matchPos = searchStr.IndexOf(mFindString,
StringComparison.InvariantCultureIgnoreCase);
if (matchPos >= 0) {
//Debug.WriteLine("Match " + index + ": " + searchStr);
mMainWin.CodeListView_EnsureVisible(index);
mMainWin.CodeListView_DeselectAll();
mMainWin.CodeListView_SelectRange(index, 1);
return;
}
index++;
if (index == CodeLineList.Count) {
index = 0;
}
}
// Announce that we've wrapped around, then clear the start index.
MessageBox.Show(Res.Strings.FIND_REACHED_START,
Res.Strings.FIND_REACHED_START_CAPTION, MessageBoxButton.OK,
MessageBoxImage.Information);
mFindStartIndex = -1;
mMainWin.CodeListView_Focus();
}
public bool CanFormatAsWord() {
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// This is insufficient -- we need to know how many bytes are selected, and
// whether they're already formatted as multi-byte items. Too expensive to
// deal with here, so we'll need to show failure dialogs instead (ugh).
return (counts.mDataLines > 0 && counts.mCodeLines == 0);
}
public void FormatAsWord() {
TypedRangeSet trs = GroupedOffsetSetFromSelected();
if (trs.Count == 0) {
Debug.Assert(false, "nothing to edit"); // shouldn't happen
return;
}
// If the user has only selected a single byte, we want to add the following byte
// to the selection, then proceed as usual. We can't simply modify the ListView
// selection because the following item might be an auto-detected string or fill,
// and we'd be adding multiple bytes. We have to be careful when grabbing the byte
// in case there's a region-split at that point (e.g. user label or .ORG).
//
// We could expand this to allow multiple regions, each of which is a single byte,
// but we'd need to deal with the case where the user selects two adjacent bytes that
// cross a region boundary.
if (trs.RangeCount == 1) {
// Exactly one range entry. Check its size.
IEnumerator<TypedRangeSet.TypedRange> checkIter = trs.RangeListIterator;
checkIter.MoveNext();
TypedRangeSet.TypedRange rng = checkIter.Current;
if (rng.Low == rng.High && rng.Low < mProject.FileDataLength - 1) {
// Single byte selected. Check to see if it's okay to grab the next byte.
Anattrib thisAttr = mProject.GetAnattrib(rng.Low);
Debug.Assert(thisAttr.DataDescriptor.Length == 1);
int nextOffset = rng.Low + 1;
Anattrib nextAttr = mProject.GetAnattrib(nextOffset);
// This must match what GroupedOffsetSetFromSelected() does.
if (!mProject.UserLabels.ContainsKey(nextOffset) &&
!mProject.HasCommentOrNote(nextOffset) &&
thisAttr.Address == nextAttr.Address - 1) {
// Good to go.
Debug.WriteLine("Grabbing second byte from +" + nextOffset.ToString("x6"));
trs.Add(nextOffset, rng.Type);
}
}
}
// Confirm that every selected byte is a single-byte data item (either set by
// the user or as part of the uncategorized data scan).
foreach (TypedRangeSet.Tuple tup in trs) {
FormatDescriptor dfd = mProject.GetAnattrib(tup.Value).DataDescriptor;
if (dfd != null && dfd.Length != 1) {
Debug.WriteLine("Can't format as word: offset +" + tup.Value.ToString("x6") +
" has len=" + dfd.Length + " (must be 1)");
MessageBox.Show(Res.Strings.INVALID_FORMAT_WORD_SEL_NON1,
Res.Strings.INVALID_FORMAT_WORD_SEL_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
}
// Confirm that, in each region, an even number of bytes are selected.
IEnumerator<TypedRangeSet.TypedRange> rngIter = trs.RangeListIterator;
while (rngIter.MoveNext()) {
TypedRangeSet.TypedRange rng = rngIter.Current;
int rangeLen = rng.High - rng.Low + 1;
if ((rangeLen & 0x01) != 0) {
string msg = string.Format(Res.Strings.INVALID_FORMAT_WORD_SEL_UNEVEN_FMT,
trs.RangeCount);
MessageBox.Show(msg,
Res.Strings.INVALID_FORMAT_WORD_SEL_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
}
// Selection is good, generate changes.
SortedList<int, FormatDescriptor> newFmts = new SortedList<int, FormatDescriptor>();
rngIter.Reset();
FormatDescriptor newDfd = FormatDescriptor.Create(2, FormatDescriptor.Type.NumericLE,
FormatDescriptor.SubType.None);
while (rngIter.MoveNext()) {
TypedRangeSet.TypedRange rng = rngIter.Current;
for (int i = rng.Low; i <= rng.High; i += 2) {
newFmts.Add(i, newDfd);
}
}
ChangeSet cs = mProject.GenerateFormatMergeSet(newFmts);
if (cs.Count != 0) {
ApplyUndoableChanges(cs);
}
}
public bool CanFormatSplitAddress() {
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Must be at least one line of data, and no code. Note this is lines, not bytes,
// so we can't screen out single-byte lines without additional work.
return (counts.mDataLines > 0 && counts.mCodeLines == 0);
}
public void FormatSplitAddress() {
TypedRangeSet trs = GroupedOffsetSetFromSelected();
if (trs.Count == 0) {
// shouldn't happen
Debug.Assert(false, "FormatSplitAddressTable found nothing to edit");
return;
}
FormatSplitAddress dlg = new FormatSplitAddress(mMainWin, mProject, trs,
mOutputFormatter);
dlg.ShowDialog();
if (dlg.DialogResult != true) {
return;
}
// 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");
}
}
public void Goto() {
GotoBox dlg = new GotoBox(mMainWin, mProject, mOutputFormatter);
if (dlg.ShowDialog() == true) {
GoToOffset(dlg.TargetOffset, false, true);
mMainWin.CodeListView_Focus();
}
}
/// <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>
public void GoToOffset(int gotoOffset, bool jumpToNote, bool doPush) {
NavStack.Location prevLoc = GetCurrentlySelectedLocation();
if (gotoOffset == prevLoc.Offset && jumpToNote == prevLoc.IsNote) {
// we're jumping to ourselves?
Debug.WriteLine("Ignoring goto to current position");
return;
}
int topLineIndex = CodeLineList.FindLineIndexByOffset(gotoOffset);
if (topLineIndex < 0) {
Debug.Assert(false, "failed goto offset +" + gotoOffset.ToString("x6"));
return;
}
int lastLineIndex;
if (jumpToNote) {
// Select all note lines, disregard the rest.
while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Note) {
topLineIndex++;
Debug.Assert(CodeLineList[topLineIndex].FileOffset == gotoOffset);
}
lastLineIndex = topLineIndex + 1;
while (lastLineIndex < CodeLineList.Count &&
CodeLineList[lastLineIndex].LineType == LineListGen.Line.Type.Note) {
lastLineIndex++;
}
} else if (gotoOffset < 0) {
// This is the offset of the header comment or a .EQ directive. Don't mess with it.
lastLineIndex = topLineIndex + 1;
} else {
// Advance to the code or data line.
while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Code &&
CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Data) {
topLineIndex++;
}
lastLineIndex = topLineIndex + 1;
}
// Make sure the item is visible. For notes, this can span multiple lines.
mMainWin.CodeListView_EnsureVisible(lastLineIndex - 1);
mMainWin.CodeListView_EnsureVisible(topLineIndex);
// Update the selection.
mMainWin.CodeListView_DeselectAll();
mMainWin.CodeListView_SelectRange(topLineIndex, lastLineIndex - topLineIndex);
if (doPush) {
// Update the back stack and associated controls.
mNavStack.Push(prevLoc);
}
}
private NavStack.Location GetCurrentlySelectedLocation() {
int index = mMainWin.CodeListView_GetFirstSelectedIndex();
if (index < 0) {
// nothing selected, use top instead
index = mMainWin.CodeListView_GetTopIndex();
}
int offset = CodeLineList[index].FileOffset;
bool isNote = (CodeLineList[index].LineType == LineListGen.Line.Type.Note);
return new NavStack.Location(offset, isNote);
}
public bool CanNavigateBackward() {
return mNavStack.HasBackward;
}
public void NavigateBackward() {
Debug.Assert(mNavStack.HasBackward);
NavStack.Location backLoc = mNavStack.MoveBackward(GetCurrentlySelectedLocation());
GoToOffset(backLoc.Offset, backLoc.IsNote, false);
}
public bool CanNavigateForward() {
return mNavStack.HasForward;
}
public void NavigateForward() {
Debug.Assert(mNavStack.HasForward);
NavStack.Location fwdLoc = mNavStack.MoveForward(GetCurrentlySelectedLocation());
GoToOffset(fwdLoc.Offset, fwdLoc.IsNote, false);
}
/// <summary>
/// Scrolls the code list so that the specified label is shown.
/// </summary>
/// <param name="sym">Label symbol.</param>
public void GoToLabel(Symbol sym) {
if (sym.IsInternalLabel) {
int offset = mProject.FindLabelOffsetByName(sym.Label);
if (offset >= 0) {
GoToOffset(offset, false, true);
} else {
Debug.WriteLine("DClick symbol: " + sym + ": label not found");
}
} else {
Debug.WriteLine("DClick symbol: " + sym + ": not label");
}
}
public void SelectionChanged() {
SelectionAnalysis = UpdateSelectionState();
UpdateReferencesPanel();
UpdateInfoPanel();
UpdateSelectionHighlight();
}
/// <summary>
/// Gathered facts about the current selection. Recalculated whenever the selection
/// changes.
/// </summary>
public class SelectionState {
// Number of selected items or lines, reduced. This will be:
// 0 if no lines are selected
// 1 if a single *item* is selected (regardless of number of lines)
// >1 if more than one item is selected (exact value not specified)
public int mNumItemsSelected;
// Single selection: the type of line selected. (Multi-sel: Unclassified)
public LineListGen.Line.Type mLineType;
// Single selection: is line an instruction with an operand. (Multi-sel: False)
public bool mIsInstructionWithOperand;
// Single selection: is line an EQU directive for a project symbol. (Multi-sel: False)
public bool mIsProjectSymbolEqu;
// Some totals.
public EntityCounts mEntityCounts;
public SelectionState() {
mLineType = LineListGen.Line.Type.Unclassified;
mEntityCounts = new EntityCounts();
}
}
/// <summary>
/// Updates Actions menu enable states when the selection changes.
/// </summary>
/// is selected.</param>
public SelectionState UpdateSelectionState() {
int selCount = mMainWin.CodeListView_GetSelectionCount();
//Debug.WriteLine("UpdateSelectionState: selCount=" + selCount);
SelectionState state = new SelectionState();
// Use IsSingleItemSelected(), rather than just checking sel.Count, because we
// want the user to be able to e.g. EditData on a multi-line string even if all
// lines in the string are selected.
if (selCount < 0) {
// nothing selected, leave everything set to false / 0
state.mEntityCounts = new EntityCounts();
} else if (IsSingleItemSelected()) {
int firstIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
state.mNumItemsSelected = 1;
state.mEntityCounts = GatherEntityCounts(firstIndex);
LineListGen.Line line = CodeLineList[firstIndex];
state.mLineType = line.LineType;
state.mIsInstructionWithOperand = (line.LineType == LineListGen.Line.Type.Code &&
mProject.GetAnattrib(line.FileOffset).IsInstructionWithOperand);
if (line.LineType == LineListGen.Line.Type.EquDirective) {
// See if this EQU directive is for a project symbol.
int symIndex = LineListGen.DefSymIndexFromOffset(line.FileOffset);
DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex];
state.mIsProjectSymbolEqu = (defSym.SymbolSource == Symbol.Source.Project);
}
} else {
state.mNumItemsSelected = 2;
state.mEntityCounts = GatherEntityCounts(-1);
}
return state;
}
/// <summary>
/// Entity count collection, for GatherEntityCounts.
/// </summary>
public class EntityCounts {
public int mCodeLines;
public int mDataLines;
public int mBlankLines;
public int mControlLines;
public int mCodeHints;
public int mDataHints;
public int mInlineDataHints;
public int mNoHints;
};
/// <summary>
/// Gathers a count of different line types and hints that are currently selected.
/// </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>Object with computed totals.</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 = mMainWin.CodeDisplayList.Count - 1;
} else {
startIndex = endIndex = singleLineIndex;
}
for (int i = startIndex; i <= endIndex; i++) {
if (!mMainWin.CodeDisplayList.SelectedIndices[i]) {
// not selected, ignore
continue;
}
LineListGen.Line line = CodeLineList[i];
switch (line.LineType) {
case LineListGen.Line.Type.Code:
codeLines++;
break;
case LineListGen.Line.Type.Data:
dataLines++;
break;
case LineListGen.Line.Type.Blank:
// Don't generally care how many blank lines there are, but we do want
// to exclude them from the other categories: if we have nothing but
// blank lines, there's nothing to do.
blankLines++;
break;
default:
// These are only editable as single-line items. We do allow mass
// code hint selection to include them (they will be ignored).
// org, equ, rwid, long comment...
controlLines++;
break;
}
// A single line can span multiple offsets, each of which could have a
// different hint. Note the code/data hints are only applied to the first
// byte of each selected line, so we're not quite in sync with that.
//
// For multi-line items, the OffsetSpan of the first item covers the entire
// item (it's the same for all Line instances), so we only want to do this for
// the first entry.
if (line.SubLineIndex == 0) {
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 (start=" + startIndex + " end=" + endIndex +
// " len=" + mMainWin.CodeDisplayList.Count +
// ") took " + (DateTime.Now - startWhen).TotalMilliseconds + " ms");
return new EntityCounts() {
mCodeLines = codeLines,
mDataLines = dataLines,
mBlankLines = blankLines,
mControlLines = controlLines,
mCodeHints = codeHints,
mDataHints = dataHints,
mInlineDataHints = inlineDataHints,
mNoHints = noHints
};
}
/// <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() {
int firstIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
if (firstIndex < 0) {
// empty selection
return false;
}
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
if (lastIndex == firstIndex) {
// only one line is selected
return true;
}
// Just check the first and last entries to see if they're the same.
LineListGen.Line firstItem = CodeLineList[firstIndex];
LineListGen.Line lastItem = CodeLineList[lastIndex];
if (firstItem.FileOffset == lastItem.FileOffset &&
firstItem.LineType == lastItem.LineType) {
return true;
}
return false;
}
/// <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) {
mMainWin.CodeListView_RemoveSelectionHighlight(mTargetHighlightIndex);
mMainWin.CodeListView_AddSelectionHighlight(targetIndex);
mTargetHighlightIndex = targetIndex;
Debug.WriteLine("Selection highlight now " + targetIndex);
}
}
private int FindSelectionHighlight() {
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
return -1;
}
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line line = CodeLineList[selIndex];
if (!line.IsCodeOrData) {
return -1;
}
Debug.Assert(line.FileOffset >= 0);
// Does this have an operand with an in-file target offset?
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
if (attr.OperandOffset >= 0) {
return CodeLineList.FindCodeDataIndexByOffset(attr.OperandOffset);
} else if (attr.IsDataStart || attr.IsInlineDataStart) {
// If it's an Address or Symbol, we can try to resolve
// the value.
int operandOffset = DataAnalysis.GetDataOperandOffset(mProject, line.FileOffset);
if (operandOffset >= 0) {
return CodeLineList.FindCodeDataIndexByOffset(operandOffset);
}
}
return -1;
}
public void ShowFileHexDump() {
OpenFileDialog fileDlg = new OpenFileDialog() {
Filter = Res.Strings.FILE_FILTER_ALL,
FilterIndex = 1
};
if (fileDlg.ShowDialog() != true) {
return;
}
string fileName = fileDlg.FileName;
FileInfo fi = new FileInfo(fileName);
if (fi.Length > Tools.WpfGui.HexDumpViewer.MAX_LENGTH) {
string msg = string.Format(Res.Strings.OPEN_DATA_TOO_LARGE_FMT,
fi.Length / 1024, Tools.WpfGui.HexDumpViewer.MAX_LENGTH / 1024);
MessageBox.Show(msg, Res.Strings.OPEN_DATA_FAIL_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
byte[] data;
try {
data = File.ReadAllBytes(fileName);
} catch (Exception ex) {
// not expecting this to happen
MessageBox.Show(ex.Message);
return;
}
// Create the dialog without an owner, and add it to the "unowned" list.
Tools.WpfGui.HexDumpViewer dlg = new Tools.WpfGui.HexDumpViewer(null,
data, mOutputFormatter);
dlg.SetFileName(Path.GetFileName(fileName));
dlg.Closing += (sender, e) => {
Debug.WriteLine("Window " + dlg + " closed, removing from unowned list");
mUnownedWindows.Remove(dlg);
};
mUnownedWindows.Add(dlg);
dlg.Show();
}
public void ShowHexDump() {
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. Note that "on top"
// means on top of *everything*. We create this without an owner so that,
// when it's not on top, it can sit behind the main app window until you
// double-click something else.
mHexDumpDialog = new Tools.WpfGui.HexDumpViewer(null,
mProject.FileData, mOutputFormatter);
mHexDumpDialog.Closing += (sender, e) => {
Debug.WriteLine("Hex dump dialog closed");
//showHexDumpToolStripMenuItem.Checked = false;
mHexDumpDialog = null;
};
mHexDumpDialog.Topmost = true;
mHexDumpDialog.Show();
}
// Bring it to the front of the window stack. This also transfers focus to the
// window.
mHexDumpDialog.Activate();
// Set the dialog's position.
if (mMainWin.CodeListView_GetSelectionCount() > 0) {
int firstIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
// offsets can be < 0 if they've selected EQU statements
int firstOffset = Math.Max(0, CodeLineList[firstIndex].FileOffset);
int lastOffset = Math.Max(firstOffset, CodeLineList[lastIndex].FileOffset +
CodeLineList[lastIndex].OffsetSpan - 1);
mHexDumpDialog.ShowOffsetRange(firstOffset, lastOffset);
}
}
/// <summary>
/// Handles the four Actions - edit hint commands.
/// </summary>
/// <param name="hint">Type of hint to apply.</param>
/// <param name="firstByteOnly">If set, only the first byte on each line is hinted.</param>
public void MarkAsType(CodeAnalysis.TypeHint hint, bool firstByteOnly) {
RangeSet sel;
if (firstByteOnly) {
sel = new RangeSet();
foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) {
int offset = CodeLineList[index].FileOffset;
if (offset >= 0) {
// Not interested in the header stuff for hinting.
sel.Add(offset);
}
}
} else {
sel = OffsetSetFromSelected();
}
TypedRangeSet newSet = new TypedRangeSet();
TypedRangeSet undoSet = new TypedRangeSet();
foreach (int offset in sel) {
if (offset < 0) {
// header comment
continue;
}
CodeAnalysis.TypeHint oldType = mProject.TypeHints[offset];
if (oldType == hint) {
// no change, don't add to set
continue;
}
undoSet.Add(offset, (int)oldType);
newSet.Add(offset, (int)hint);
}
if (newSet.Count == 0) {
Debug.WriteLine("No changes found (" + hint + ", " + sel.Count + " offsets)");
return;
}
UndoableChange uc = UndoableChange.CreateTypeHintChange(undoSet, newSet);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
/// <summary>
/// Converts the set of selected items into a set of offsets. If a line
/// spans multiple offsets (e.g. a 3-byte instruction), offsets for every
/// byte are included.
///
/// Boundaries such as labels and address changes are ignored.
/// </summary>
/// <returns>RangeSet with all offsets.</returns>
private RangeSet OffsetSetFromSelected() {
RangeSet rs = new RangeSet();
foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) {
int offset = CodeLineList[index].FileOffset;
// Mark every byte of an instruction or multi-byte data item --
// everything that is represented by the line the user selected.
int len;
if (offset >= 0) {
len = mProject.GetAnattrib(offset).Length;
} else {
// header area
len = 1;
}
Debug.Assert(len > 0);
for (int i = offset; i < offset + len; i++) {
rs.Add(i);
}
}
return rs;
}
/// <summary>
/// Handles Help - Help
/// </summary>
public void ShowHelp() {
HelpAccess.ShowHelp(HelpAccess.Topic.Contents);
}
/// <summary>
/// Handles Help - About
/// </summary>
public void ShowAboutBox() {
AboutBox dlg = new AboutBox(mMainWin);
dlg.ShowDialog();
}
public void ToggleAsciiChart() {
if (mAsciiChartDialog == null) {
// Create without owner so it doesn't have to be in front of main window.
mAsciiChartDialog = new Tools.WpfGui.AsciiChart(null);
mAsciiChartDialog.Closing += (sender, e) => {
Debug.WriteLine("ASCII chart closed");
mAsciiChartDialog = null;
};
mAsciiChartDialog.Show();
} else {
mAsciiChartDialog.Close();
}
}
public void ToggleDataScan() {
ProjectProperties oldProps = mProject.ProjectProps;
ProjectProperties newProps = new ProjectProperties(oldProps);
newProps.AnalysisParams.AnalyzeUncategorizedData =
!newProps.AnalysisParams.AnalyzeUncategorizedData;
UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(oldProps, newProps);
ApplyUndoableChanges(new ChangeSet(uc));
}
public bool CanToggleSingleByteFormat() {
EntityCounts counts = SelectionAnalysis.mEntityCounts;
return (counts.mDataLines > 0 && counts.mCodeLines == 0);
}
public void ToggleSingleByteFormat() {
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);
}
}
/// <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;
DateTime startWhen = DateTime.Now;
int prevOffset = -1;
foreach (int index in mMainWin.CodeDisplayList.SelectedIndices) {
// Don't add an offset to the set if the only part of it that is selected
// is a directive or blank line. We only care about file offsets, so skip
// anything that isn't code or data.
if (!CodeLineList[index].IsCodeOrData) {
continue;
}
int offset = CodeLineList[index].FileOffset;
if (offset == prevOffset) {
// This is a continuation of a multi-line item like a string. We've
// already accounted for all bytes associated with this offset.
continue;
}
Anattrib attr = mProject.GetAnattrib(offset);
if (expectedAddr == -1) {
expectedAddr = attr.Address;
}
// Check for user labels.
if (mProject.UserLabels.ContainsKey(offset)) {
//if (mProject.GetAnattrib(offset).Symbol != null) {
// We consider auto labels when splitting regions for the data analysis,
// but I don't think we want to take them into account here. The specific
// example that threw me was loading a 16-bit value from an address table.
// The code does "LDA table,X / STA / LDA table+1,X / STA", which puts auto
// labels at the first two addresses -- splitting the region. That's good
// for the uncategorized data analyzer, but very annoying if you want to
// slap a 16-bit numeric format on all entries in a table.
groupNum++;
} else if (mProject.HasCommentOrNote(offset)) {
// Don't carry across a long comment or note.
groupNum++;
} else if (attr.Address != expectedAddr) {
// For a contiguous selection, this should only happen if there's a .ORG
// address change. For non-contiguous selection this is expected. In the
// latter case, incrementing the group number is unnecessary but harmless.
Debug.WriteLine("Address break: " + attr.Address + " vs. " + expectedAddr);
//Debug.Assert(mProject.AddrMap.Get(offset) >= 0);
expectedAddr = attr.Address;
groupNum++;
}
// Mark every byte of an instruction or multi-byte data item --
// everything that is represented by the line the user selected. Control
// statements and blank lines aren't relevant here, as we only care about
// file offsets.
int len = CodeLineList[index].OffsetSpan; // attr.Length;
Debug.Assert(len > 0);
for (int i = offset; i < offset + len; i++) {
rs.Add(i, groupNum);
}
// Advance the address.
expectedAddr += len;
prevOffset = offset;
}
Debug.WriteLine("Offset selection conv took " +
(DateTime.Now - startWhen).TotalMilliseconds + " ms");
return rs;
}
#endregion Main window UI event handlers
#region References panel
/// <summary>
/// Updates the "references" panel to reflect the current selection.
///
/// The number of references to any given address should be relatively small, and
/// won't change without a data refresh, so recreating the list every time shouldn't
/// be a problem.
/// </summary>
private void UpdateReferencesPanel() {
mMainWin.ReferencesList.Clear();
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
// Nothing selected, or multiple lines selected.
return;
}
int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line.Type type = CodeLineList[lineIndex].LineType;
if (type != LineListGen.Line.Type.Code &&
type != LineListGen.Line.Type.Data &&
type != LineListGen.Line.Type.EquDirective) {
// Code, data, and platform symbol EQUs have xrefs.
return;
}
// Find the appropriate xref set.
int offset = CodeLineList[lineIndex].FileOffset;
XrefSet xrefs;
if (offset < 0) {
int index = LineListGen.DefSymIndexFromOffset(offset);
DefSymbol defSym = mProject.ActiveDefSymbolList[index];
xrefs = defSym.Xrefs;
} else {
xrefs = mProject.GetXrefSet(offset);
}
if (xrefs == null || xrefs.Count == 0) {
return;
}
// TODO(someday): localization
Asm65.Formatter formatter = mOutputFormatter;
bool showBank = !mProject.CpuDef.HasAddr16;
for (int i = 0; i < xrefs.Count; i++) {
XrefSet.Xref xr = xrefs[i];
string typeStr;
switch (xr.Type) {
case XrefSet.XrefType.SubCallOp:
typeStr = "call ";
break;
case XrefSet.XrefType.BranchOp:
typeStr = "branch ";
break;
case XrefSet.XrefType.RefFromData:
typeStr = "data ";
break;
case XrefSet.XrefType.MemAccessOp:
switch (xr.AccType) {
case OpDef.MemoryEffect.Read:
typeStr = "read ";
break;
case OpDef.MemoryEffect.Write:
typeStr = "write ";
break;
case OpDef.MemoryEffect.ReadModifyWrite:
typeStr = "rmw ";
break;
case OpDef.MemoryEffect.None: // e.g. LDA #<symbol, PEA addr
typeStr = "ref ";
break;
case OpDef.MemoryEffect.Unknown:
default:
Debug.Assert(false);
typeStr = "??! ";
break;
}
break;
default:
Debug.Assert(false);
typeStr = "??? ";
break;
}
MainWindow.ReferencesListItem rli = new MainWindow.ReferencesListItem(xr.Offset,
formatter.FormatOffset24(xr.Offset),
formatter.FormatAddress(mProject.GetAnattrib(xr.Offset).Address, showBank),
(xr.IsSymbolic ? "Sym " : "Num ") + typeStr +
formatter.FormatAdjustment(-xr.Adjustment));
mMainWin.ReferencesList.Add(rli);
}
}
public bool CanUndo() {
return (mProject != null && mProject.CanUndo);
}
/// <summary>
/// Handles Edit - Undo.
/// </summary>
public void UndoChanges() {
if (!mProject.CanUndo) {
Debug.Assert(false, "Nothing to undo");
return;
}
ChangeSet cs = mProject.PopUndoSet();
ApplyChanges(cs, true);
UpdateTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.DisplayText = mProject.DebugGetUndoRedoHistory();
}
}
public bool CanRedo() {
return (mProject != null && mProject.CanRedo);
}
/// <summary>
/// Handles Edit - Redo.
/// </summary>
public void RedoChanges() {
if (!mProject.CanRedo) {
Debug.Assert(false, "Nothing to redo");
return;
}
ChangeSet cs = mProject.PopRedoSet();
ApplyChanges(cs, false);
UpdateTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.DisplayText = mProject.DebugGetUndoRedoHistory();
}
}
#endregion References panel
#region Notes panel
private void PopulateNotesList() {
mMainWin.NotesList.Clear();
foreach (KeyValuePair<int, MultiLineComment> kvp in mProject.Notes) {
int offset = kvp.Key;
MultiLineComment mlc = kvp.Value;
// Replace line break with bullet. If there's a single CRLF at the end, strip it.
string nocrlfStr;
if (mlc.Text.EndsWith("\r\n")) {
nocrlfStr =
mlc.Text.Substring(0, mlc.Text.Length - 2).Replace("\r\n", " \u2022 ");
} else {
nocrlfStr = mlc.Text.Replace("\r\n", " \u2022 ");
}
MainWindow.NotesListItem nli = new MainWindow.NotesListItem(offset,
mOutputFormatter.FormatOffset24(offset),
nocrlfStr,
mlc.BackgroundColor);
mMainWin.NotesList.Add(nli);
}
}
#endregion Notes panel
#region Symbols panel
private void PopulateSymbolsList() {
mMainWin.SymbolsList.Clear();
foreach (Symbol sym in mProject.SymbolTable) {
MainWindow.SymbolsListItem sli = new MainWindow.SymbolsListItem(sym,
sym.SourceTypeString,
mOutputFormatter.FormatHexValue(sym.Value, 0),
sym.Label);
mMainWin.SymbolsList.Add(sli);
}
}
#endregion Symbols panel
#region Info panel
private void UpdateInfoPanel() {
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
// Nothing selected, or multiple lines selected.
mMainWin.InfoPanelContents = string.Empty;
return;
}
int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line line = CodeLineList[lineIndex];
StringBuilder sb = new StringBuilder(250);
// TODO(someday): this should be made easier to localize
string lineTypeStr;
string extraStr = string.Empty;
switch (line.LineType) {
case LineListGen.Line.Type.Code:
lineTypeStr = "code";
break;
case LineListGen.Line.Type.Data:
if (mProject.GetAnattrib(line.FileOffset).IsInlineData) {
lineTypeStr = "inline data";
} else {
lineTypeStr = "data";
}
break;
case LineListGen.Line.Type.LongComment:
lineTypeStr = "comment";
break;
case LineListGen.Line.Type.Note:
lineTypeStr = "note";
break;
case LineListGen.Line.Type.Blank:
lineTypeStr = "blank line";
//lineTypeStr = "blank line (+" +
// mOutputFormatter.FormatOffset24(line.FileOffset) + ")";
break;
case LineListGen.Line.Type.OrgDirective:
lineTypeStr = "address directive";
break;
case LineListGen.Line.Type.RegWidthDirective:
lineTypeStr = "register width directive";
break;
case LineListGen.Line.Type.EquDirective: {
lineTypeStr = "equate";
int symIndex = LineListGen.DefSymIndexFromOffset(line.FileOffset);
DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex];
string sourceStr;
if (defSym.SymbolSource == Symbol.Source.Project) {
sourceStr = "project symbol definition";
} else if (defSym.SymbolSource == Symbol.Source.Platform) {
sourceStr = "platform symbol file";
} else {
sourceStr = "???";
}
extraStr = "Source: " + sourceStr;
}
break;
default:
lineTypeStr = "???";
break;
}
// For anything that isn't code or data, show something simple and bail.
if (line.OffsetSpan == 0) {
sb.AppendFormat(Res.Strings.INFO_LINE_SUM_NON_FMT,
lineIndex, lineTypeStr);
#if DEBUG
sb.Append(" [offset=+" + line.FileOffset.ToString("x6") + "]");
#endif
if (!string.IsNullOrEmpty(extraStr)) {
sb.Append("\r\n\r\n");
sb.Append(extraStr);
}
mMainWin.InfoPanelContents = sb.ToString();
return;
}
Debug.Assert(line.IsCodeOrData);
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
// Show number of bytes of code/data.
if (line.OffsetSpan == 1) {
sb.AppendFormat(Res.Strings.INFO_LINE_SUM_SINGULAR_FMT,
lineIndex, line.OffsetSpan, lineTypeStr);
} else {
sb.AppendFormat(Res.Strings.INFO_LINE_SUM_PLURAL_FMT,
lineIndex, line.OffsetSpan, lineTypeStr);
}
sb.Append("\r\n");
if (!mProject.OperandFormats.TryGetValue(line.FileOffset, out FormatDescriptor dfd)) {
// No user-specified format, but there may be a generated format.
sb.AppendFormat(Res.Strings.INFO_FD_SUM_FMT, Res.Strings.DEFAULT_VALUE);
if (attr.DataDescriptor != null) {
sb.Append(" [");
sb.Append(attr.DataDescriptor.ToUiString());
sb.Append("]");
}
} else {
// User-specified operand format.
// If the descriptor has a weak reference to an unknown symbol, should we
// call that out here?
sb.AppendFormat(Res.Strings.INFO_FD_SUM_FMT, dfd.ToUiString());
}
sb.Append("\r\n");
// Debug only
//sb.Append("DEBUG: opAddr=" + attr.OperandAddress.ToString("x4") +
// " opOff=" + attr.OperandOffset.ToString("x4") + "\r\n");
sb.Append("\u2022Attributes:");
if (attr.IsHinted) {
sb.Append(" Hinted(");
for (int i = 0; i < line.OffsetSpan; i++) {
switch (mProject.TypeHints[line.FileOffset + i]) {
case CodeAnalysis.TypeHint.Code:
sb.Append("C");
break;
case CodeAnalysis.TypeHint.Data:
sb.Append("D");
break;
case CodeAnalysis.TypeHint.InlineData:
sb.Append("I");
break;
default:
break;
}
if (i > 8) {
sb.Append("...");
break;
}
}
sb.Append(')');
}
if (attr.IsEntryPoint) {
sb.Append(" EntryPoint");
}
if (attr.IsBranchTarget) {
sb.Append(" BranchTarget");
}
if (attr.DoesNotContinue) {
sb.Append(" NoContinue");
}
if (attr.DoesNotBranch) {
sb.Append(" NoBranch");
}
if (mProject.StatusFlagOverrides[line.FileOffset].AsInt != 0) {
sb.Append(" StatusFlags");
}
sb.Append("\r\n\r\n");
if (attr.IsInstruction) {
Asm65.OpDef op = mProject.CpuDef.GetOpDef(mProject.FileData[line.FileOffset]);
string shortDesc = mOpDesc.GetShortDescription(op.Mnemonic);
if (!string.IsNullOrEmpty(shortDesc)) {
if (op.IsUndocumented) {
sb.Append("\u25b6[*] ");
} else {
sb.Append("\u25b6 ");
}
sb.Append(shortDesc);
string addrStr = mOpDesc.GetAddressModeDescription(op.AddrMode);
if (!string.IsNullOrEmpty(addrStr)) {
sb.Append(", ");
sb.Append(addrStr);
}
sb.Append("\r\n");
}
sb.Append("\u2022Cycles: ");
int cycles = op.Cycles;
Asm65.OpDef.CycleMod cycMods = op.CycleMods;
sb.Append(cycles.ToString());
if (cycMods != 0) {
sb.Append(" (");
int workBits = (int)cycMods;
while (workBits != 0) {
// Isolate rightmost bit.
int firstBit = (~workBits + 1) & workBits;
sb.Append(mOpDesc.GetCycleModDescription((OpDef.CycleMod)firstBit));
// Remove from set.
workBits &= ~firstBit;
if (workBits != 0) {
// more to come
sb.Append(", ");
}
}
sb.Append(")");
}
sb.Append("\r\n");
const string FLAGS = "NVMXDIZC";
sb.Append("\u2022Flags affected: ");
Asm65.StatusFlags affectedFlags = op.FlagsAffected;
for (int i = 0; i < 8; i++) {
if (affectedFlags.GetBit((StatusFlags.FlagBits)(7 - i)) >= 0) {
sb.Append(' ');
sb.Append(FLAGS[i]);
} else {
sb.Append(" -");
}
}
sb.Append("\r\n");
string longDesc = mOpDesc.GetLongDescription(op.Mnemonic);
if (!string.IsNullOrEmpty(longDesc)) {
sb.Append("\r\n");
sb.Append(longDesc);
sb.Append("\r\n");
}
} else {
// do we want descriptions of the pseudo-ops?
}
// Publish
mMainWin.InfoPanelContents = sb.ToString();
}
#endregion Info panel
#region Debug features
public void Debug_ExtensionScriptInfo() {
string info = mProject.DebugGetLoadedScriptInfo();
Tools.WpfGui.ShowText dlg = new Tools.WpfGui.ShowText(mMainWin, info);
dlg.Title = "Loaded Extension Script Info";
dlg.ShowDialog();
}
public void Debug_ShowAnalysisTimers() {
if (mShowAnalysisTimersDialog == null) {
Tools.WpfGui.ShowText dlg = new Tools.WpfGui.ShowText(null, "(no data yet)");
dlg.Title = "Analysis Timers";
dlg.Closing += (sender, e) => {
Debug.WriteLine("Analysis timers dialog closed");
mShowAnalysisTimersDialog = null;
};
dlg.Show();
mShowAnalysisTimersDialog = dlg;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mShowAnalysisTimersDialog.Close();
}
}
public void Debug_ShowAnalyzerOutput() {
if (mShowAnalyzerOutputDialog == null) {
Tools.WpfGui.ShowText dlg = new Tools.WpfGui.ShowText(null, "(no data yet)");
dlg.Title = "Analyzer Output";
dlg.Closing += (sender, e) => {
Debug.WriteLine("Analyzer output dialog closed");
mShowAnalyzerOutputDialog = null;
};
dlg.Show();
mShowAnalyzerOutputDialog = dlg;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mShowAnalyzerOutputDialog.Close();
}
}
public void Debug_ShowUndoRedoHistory() {
if (mShowUndoRedoHistoryDialog == null) {
Tools.WpfGui.ShowText dlg = new Tools.WpfGui.ShowText(null,
mProject.DebugGetUndoRedoHistory());
dlg.Title = "Undo/Redo History";
dlg.Closing += (sender, e) => {
Debug.WriteLine("Undo/redo history dialog closed");
mShowUndoRedoHistoryDialog = null;
};
dlg.Show();
mShowUndoRedoHistoryDialog = dlg;
} else {
// Ask the dialog to close. Do the cleanup in the event.
mShowUndoRedoHistoryDialog.Close();
}
}
public void Debug_RunSourceGenerationTests() {
Tests.WpfGui.GenTestRunner dlg = new Tests.WpfGui.GenTestRunner(mMainWin);
dlg.ShowDialog();
}
public void Debug_Refresh() {
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);
UpdateTitle(); // in case something changed
}
public void Debug_ToggleCommentRulers() {
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);
}
public void Debug_ToggleKeepAliveHack() {
ScriptManager.UseKeepAliveHack = !ScriptManager.UseKeepAliveHack;
}
#endregion
}
}