mirror of
https://github.com/fadden/6502bench.git
synced 2024-11-29 10:50:28 +00:00
1da98d8628
Generation of HTML is extremely fast, but compressing thousands of frames for wireframe animated GIFs can take a little while. Sharing bitmaps between threads required two changes: (1) bitmaps need to be "frozen" after being drawn; (2) you can't use Path because BackgroundWorker isn't a STAThread. You can, however, use a DrawingVisual / DrawingContext to do the rendering. Which is really what I should have been doing all along; I just didn't know the approach existed until I was forced to go looking for it. Also, we now do a "run finalizers" call before generating an animated GIF. Without it things explode after more than 10K GDI objects have been allocated.
4252 lines
182 KiB
C#
4252 lines
182 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;
|
|
using System.Windows.Media;
|
|
|
|
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>
|
|
/// Instruction chart reference window. Not tied to the project.
|
|
/// </summary>
|
|
private Tools.WpfGui.InstructionChart mInstructionChartDialog;
|
|
|
|
/// <summary>
|
|
/// Returns true if the instruction chart window is currently open.
|
|
/// </summary>
|
|
public bool IsInstructionChartOpen { get { return mInstructionChartDialog != 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 mFormatter;
|
|
|
|
/// <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>
|
|
/// True if previous search was backward, so we can tell if we changed direction
|
|
/// (otherwise we think we immediately wrapped around and the search stops).
|
|
/// </summary>
|
|
private bool mFindBackward = false;
|
|
|
|
/// <summary>
|
|
/// Used to highlight the line that is the target of the selected line.
|
|
/// </summary>
|
|
private int mTargetHighlightIndex = -1;
|
|
|
|
/// <summary>
|
|
/// Code list color scheme.
|
|
/// </summary>
|
|
private MainWindow.ColorScheme mColorScheme = MainWindow.ColorScheme.Light;
|
|
|
|
/// <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 mFormatterCpuDef;
|
|
|
|
/// <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,
|
|
AllColumns = 2
|
|
}
|
|
|
|
/// <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() {
|
|
Debug.Assert(CommonUtil.RangeSet.Test());
|
|
Debug.Assert(CommonUtil.TypedRangeSet.Test());
|
|
Debug.Assert(CommonUtil.Version.Test());
|
|
|
|
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_NON_UNIQUE, false);
|
|
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_ADDR, true);
|
|
settings.SetBool(AppSettings.SYMWIN_SHOW_CONST, 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);
|
|
|
|
// Something peculiar happens when we switch from the launch window to the
|
|
// code list: the refs/notes splitter and sym/info splitter shift down a pixel.
|
|
// Closing the project causes everything to shift back. I'm not sure what's
|
|
// causing the layout to change. I'm working around the issue by not saving the
|
|
// splitter positions if they've only moved 1 pixel.
|
|
// TODO: fix this properly
|
|
int refSetting = AppSettings.Global.GetInt(AppSettings.MAIN_REFERENCES_HEIGHT, -1);
|
|
if ((int)mMainWin.ReferencesPanelHeight == refSetting ||
|
|
(int)mMainWin.ReferencesPanelHeight == refSetting - 1) {
|
|
Debug.WriteLine("NOT updating references height");
|
|
} else {
|
|
AppSettings.Global.SetInt(AppSettings.MAIN_REFERENCES_HEIGHT,
|
|
(int)mMainWin.ReferencesPanelHeight);
|
|
}
|
|
int symSetting = AppSettings.Global.GetInt(AppSettings.MAIN_SYMBOLS_HEIGHT, -1);
|
|
if ((int)mMainWin.SymbolsPanelHeight == symSetting ||
|
|
(int)mMainWin.SymbolsPanelHeight == symSetting - 1) {
|
|
Debug.WriteLine("NOT updating symbols height");
|
|
} else {
|
|
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;
|
|
|
|
mFormatterConfig.mNonUniqueLabelPrefix =
|
|
settings.GetString(AppSettings.FMT_NON_UNIQUE_LABEL_PREFIX, string.Empty);
|
|
mFormatterConfig.mLocalVariableLabelPrefix =
|
|
settings.GetString(AppSettings.FMT_LOCAL_VARIABLE_PREFIX, string.Empty);
|
|
mFormatterConfig.mCommaSeparatedDense =
|
|
settings.GetBool(AppSettings.FMT_COMMA_SEP_BULK_DATA, false);
|
|
|
|
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 mFormatterCpuDef to force a refresh
|
|
// of related items.
|
|
mFormatter = new Formatter(mFormatterConfig);
|
|
mFormatterCpuDef = 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.SymFilterNonUniqueLabels =
|
|
settings.GetBool(AppSettings.SYMWIN_SHOW_NON_UNIQUE, 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();
|
|
|
|
// Set the color scheme.
|
|
bool useDark = settings.GetBool(AppSettings.SKIN_DARK_COLOR_SCHEME, false);
|
|
if (useDark) {
|
|
mColorScheme = MainWindow.ColorScheme.Dark;
|
|
} else {
|
|
mColorScheme = MainWindow.ColorScheme.Light;
|
|
}
|
|
mMainWin.SetColorScheme(mColorScheme);
|
|
if (CodeLineList != null) {
|
|
SetCodeLineListColorMultiplier();
|
|
}
|
|
|
|
// 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 SetCodeLineListColorMultiplier() {
|
|
if (mColorScheme == MainWindow.ColorScheme.Dark) {
|
|
CodeLineList.NoteColorMultiplier = 0.6f;
|
|
} else {
|
|
CodeLineList.NoteColorMultiplier = 1.0f;
|
|
}
|
|
}
|
|
|
|
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();
|
|
if (mProject != null) {
|
|
if (string.IsNullOrEmpty(mProjectPathName)) {
|
|
sb.Append(Res.Strings.TITLE_NEW_PROJECT);
|
|
} else {
|
|
sb.Append(Path.GetFileName(mProjectPathName));
|
|
}
|
|
if (mProject.IsReadOnly) {
|
|
sb.Append(" ");
|
|
sb.Append(Res.Strings.TITLE_READ_ONLY);
|
|
}
|
|
sb.Append(" - ");
|
|
}
|
|
|
|
sb.Append(Res.Strings.TITLE_BASE);
|
|
|
|
if (mProject != null && mProject.IsDirty) {
|
|
sb.Append(" - ");
|
|
sb.Append(Res.Strings.TITLE_MODIFIED);
|
|
}
|
|
mMainWin.Title = sb.ToString();
|
|
|
|
UpdateByteCounts();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the code/data/junk percentages in the status bar.
|
|
/// </summary>
|
|
private void UpdateByteCounts() {
|
|
if (mProject == null) {
|
|
mMainWin.ByteCountText = string.Empty;
|
|
return;
|
|
}
|
|
|
|
Debug.Assert(mProject.ByteCounts.CodeByteCount + mProject.ByteCounts.DataByteCount +
|
|
mProject.ByteCounts.JunkByteCount == mProject.FileData.Length);
|
|
|
|
int total = mProject.FileData.Length;
|
|
float codePerc = (mProject.ByteCounts.CodeByteCount * 100.0f) / total;
|
|
float dataPerc = (mProject.ByteCounts.DataByteCount * 100.0f) / total;
|
|
float junkPerc = (mProject.ByteCounts.JunkByteCount * 100.0f) / total;
|
|
mMainWin.ByteCountText = string.Format(Res.Strings.STATUS_BYTE_COUNT_FMT,
|
|
total / 1024.0f, codePerc, dataPerc, junkPerc);
|
|
}
|
|
|
|
#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,
|
|
mFormatter, mPseudoOpNames);
|
|
SetCodeLineListColorMultiplier();
|
|
|
|
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 (mFormatterCpuDef != mProject.CpuDef) { // reference equality is fine
|
|
Debug.WriteLine("CpuDef has changed, resetting formatter (now " +
|
|
mProject.CpuDef + ")");
|
|
mFormatter = new Formatter(mFormatterConfig);
|
|
CodeLineList.SetFormatter(mFormatter);
|
|
CodeLineList.SetPseudoOpNames(mPseudoOpNames);
|
|
mFormatterCpuDef = 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("Update message list");
|
|
mMainWin.UpdateMessageList(mProject.Messages, mFormatter);
|
|
mReanalysisTimer.EndTask("Update message list");
|
|
}
|
|
|
|
mReanalysisTimer.StartTask("Generate DisplayList");
|
|
CodeLineList.GenerateAll();
|
|
mReanalysisTimer.EndTask("Generate DisplayList");
|
|
|
|
mReanalysisTimer.StartTask("Refresh Visualization thumbnails");
|
|
VisualizationSet.RefreshAllThumbnails(mProject);
|
|
mReanalysisTimer.EndTask("Refresh Visualization thumbnails");
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
newProject.IsReadOnly = dlg.WantReadOnly;
|
|
}
|
|
|
|
mProject = newProject;
|
|
mProjectPathName = mProject.ProjectPathName = projPathName;
|
|
mDataPathName = dataPathName;
|
|
FinishPrep();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds and loads the specified data file. The file's length and CRC must match
|
|
/// the project's expectations.
|
|
/// </summary>
|
|
/// <param name="dataPathName">Full path to file.</param>
|
|
/// <param name="proj">Project object.</param>
|
|
/// <param name="cancel">Returns true if we want to cancel the attempt.</param>
|
|
/// <returns></returns>
|
|
private byte[] FindValidDataFile(ref string dataPathName, DisasmProject proj,
|
|
out bool cancel) {
|
|
FileInfo fi = new FileInfo(dataPathName);
|
|
if (!fi.Exists) {
|
|
Debug.WriteLine("File '" + dataPathName + "' doesn't exist");
|
|
dataPathName = ChooseDataFile(dataPathName,
|
|
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() {
|
|
Debug.Assert(!mProject.IsReadOnly);
|
|
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() {
|
|
Debug.Assert(!mProject.IsReadOnly);
|
|
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();
|
|
mInstructionChartDialog?.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;
|
|
|
|
UpdateTitle();
|
|
|
|
// 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 {
|
|
get { return mProject != null; }
|
|
}
|
|
public bool IsProjectReadOnly {
|
|
get { return mProject != null && mProject.IsReadOnly; }
|
|
}
|
|
|
|
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() {
|
|
DisplayListSelection selection = mMainWin.CodeDisplayList.SelectedIndices;
|
|
if (selection.Count == 0) {
|
|
Debug.WriteLine("Selection is empty!");
|
|
return;
|
|
}
|
|
|
|
ClipLineFormat format = (ClipLineFormat)AppSettings.Global.GetEnum(
|
|
AppSettings.CLIP_LINE_FORMAT,
|
|
typeof(ClipLineFormat),
|
|
(int)ClipLineFormat.AssemblerSource);
|
|
|
|
int[] rightWidths = new int[] { 9, 8, 11, 100 };
|
|
|
|
Exporter.ActiveColumnFlags colFlags = Exporter.ActiveColumnFlags.None;
|
|
if (format == ClipLineFormat.Disassembly) {
|
|
colFlags |= Exporter.ActiveColumnFlags.Address |
|
|
Exporter.ActiveColumnFlags.Bytes;
|
|
} else if (format == ClipLineFormat.AllColumns) {
|
|
colFlags = Exporter.ActiveColumnFlags.ALL;
|
|
}
|
|
Exporter eport = new Exporter(mProject, CodeLineList, mFormatter,
|
|
colFlags, rightWidths);
|
|
eport.Selection = selection;
|
|
|
|
// Might want to set Mouse.OverrideCursor if the selection exceeds a few
|
|
// hundred thousand lines.
|
|
eport.SelectionToString(true, out string fullText, out string csvText);
|
|
|
|
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
|
|
const bool addCsv = true;
|
|
if (addCsv) {
|
|
byte[] csvData = Encoding.UTF8.GetBytes(csvText.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.LocalVariableTable:
|
|
if (CanEditLocalVariableTable()) {
|
|
EditLocalVariableTable();
|
|
}
|
|
break;
|
|
case LineListGen.Line.Type.VisualizationSet:
|
|
if (CanEditVisualizationSet()) {
|
|
EditVisualizationSet();
|
|
}
|
|
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:
|
|
HandleDoubleClickOnOpcode(line);
|
|
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;
|
|
}
|
|
}
|
|
|
|
private void HandleDoubleClickOnOpcode(LineListGen.Line line) {
|
|
if (line.FileOffset < 0) {
|
|
// Double-click on project symbol EQUs and the file header comment are handled
|
|
// elsewhere.
|
|
return;
|
|
}
|
|
|
|
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.
|
|
GoToLocation(new NavStack.Location(attr.OperandOffset, 0, false),
|
|
GoToMode.JumpToCodeData, true);
|
|
} else if (dfd != null && dfd.HasSymbol) {
|
|
// Operand has a symbol, do a symbol lookup.
|
|
if (dfd.SymbolRef.IsVariable) {
|
|
GoToVarDefinition(line.FileOffset, dfd.SymbolRef, true);
|
|
} else {
|
|
if (mProject.SymbolTable.TryGetValue(dfd.SymbolRef.Label, out Symbol sym)) {
|
|
if (sym.SymbolSource == Symbol.Source.User ||
|
|
sym.SymbolSource == Symbol.Source.Auto) {
|
|
int labelOffset = mProject.FindLabelOffsetByName(dfd.SymbolRef.Label);
|
|
if (labelOffset >= 0) {
|
|
GoToLocation(new NavStack.Location(labelOffset, 0, false),
|
|
GoToMode.JumpToCodeData, true);
|
|
}
|
|
} else if (sym.SymbolSource == Symbol.Source.Platform ||
|
|
sym.SymbolSource == Symbol.Source.Project) {
|
|
// find entry
|
|
for (int i = 0; i < mProject.ActiveDefSymbolList.Count; i++) {
|
|
if (mProject.ActiveDefSymbolList[i] == sym) {
|
|
int offset = LineListGen.DefSymOffsetFromIndex(i);
|
|
GoToLocation(new NavStack.Location(offset, 0, false),
|
|
GoToMode.JumpToCodeData, true);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
Debug.Assert(false);
|
|
}
|
|
} else {
|
|
// must be a broken weak symbol ref
|
|
Debug.WriteLine("Operand symbol not found: " + dfd.SymbolRef.Label);
|
|
}
|
|
}
|
|
} 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) {
|
|
GoToLocation(new NavStack.Location(operandOffset, 0, false),
|
|
GoToMode.JumpToCodeData, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
// First line must be code, data, or an ORG directive.
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
if (selIndex < 0) {
|
|
return false;
|
|
}
|
|
LineListGen.Line selLine = CodeLineList[selIndex];
|
|
if (selLine.LineType != LineListGen.Line.Type.Code &&
|
|
selLine.LineType != LineListGen.Line.Type.Data &&
|
|
selLine.LineType != LineListGen.Line.Type.OrgDirective) {
|
|
return false;
|
|
}
|
|
|
|
// If multiple lines are selected, there must not be an address change between them.
|
|
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
|
|
int firstOffset = CodeLineList[selIndex].FileOffset;
|
|
int lastOffset = CodeLineList[lastIndex].FileOffset;
|
|
if (firstOffset == lastOffset) {
|
|
// Single-item selection, we're fine.
|
|
return true;
|
|
}
|
|
|
|
int nextOffset = lastOffset + CodeLineList[lastIndex].OffsetSpan;
|
|
|
|
foreach (AddressMap.AddressMapEntry ent in mProject.AddrMap) {
|
|
// It's okay to have an existing entry at firstOffset or nextOffset.
|
|
if (ent.Offset > firstOffset && ent.Offset < nextOffset) {
|
|
Debug.WriteLine("Found mid-selection AddressMap entry at +" +
|
|
ent.Offset.ToString("x6"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void EditAddress() {
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
|
|
int firstOffset = CodeLineList[selIndex].FileOffset;
|
|
int lastOffset = CodeLineList[lastIndex].FileOffset;
|
|
int nextOffset = lastOffset + CodeLineList[lastIndex].OffsetSpan;
|
|
int nextAddr;
|
|
|
|
if (firstOffset == lastOffset || nextOffset == mProject.FileDataLength) {
|
|
// Single item (which may not be a single *line*) is selected, or the
|
|
// last selected item is the end of the file.
|
|
nextOffset = -1;
|
|
nextAddr = AddressMap.NO_ENTRY_ADDR;
|
|
} else {
|
|
// Compute "nextAddr". If there's an existing entry at nextOffset, we use
|
|
// that. If not, we use the "load address", which is determined by the very
|
|
// first address.
|
|
//
|
|
// I tried this by just removing the selected entry and seeing what the address
|
|
// would be without it, useful for relocations inside relocations. This worked
|
|
// poorly when relocations were chained, i.e. two consecutive blocks were
|
|
// relocated to different places. The end address of the second block gets
|
|
// set based on the first address of the first block, which doesn't seem useful.
|
|
#if false
|
|
nextAddr = mProject.AddrMap.Get(nextOffset);
|
|
if (nextAddr == AddressMap.NO_ENTRY_ADDR) {
|
|
AddressMap cloneMap = new AddressMap(mProject.AddrMap.GetEntryList());
|
|
if (firstOffset != 0) {
|
|
cloneMap.Remove(firstOffset);
|
|
}
|
|
nextAddr = cloneMap.OffsetToAddress(nextOffset);
|
|
}
|
|
#else
|
|
int fileStartAddr = mProject.AddrMap.OffsetToAddress(0);
|
|
nextAddr = ((fileStartAddr + nextOffset) & 0xffff) | (fileStartAddr & 0xff0000);
|
|
#endif
|
|
}
|
|
|
|
EditAddress dlg = new EditAddress(mMainWin, firstOffset, nextOffset, nextAddr,
|
|
mProject, mFormatter);
|
|
if (dlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
|
|
if (firstOffset == 0 && dlg.NewAddress < 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");
|
|
return;
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet(1);
|
|
|
|
if (mProject.AddrMap.Get(firstOffset) != dlg.NewAddress) {
|
|
// Added / removed / changed existing entry.
|
|
//
|
|
// We allow creation of an apparently redundant address override, because
|
|
// sometimes it's helpful to add one to "anchor" an area before relocating
|
|
// something that appears earlier in the file.
|
|
int prevAddress = mProject.AddrMap.Get(firstOffset);
|
|
UndoableChange uc = UndoableChange.CreateAddressChange(firstOffset,
|
|
prevAddress, dlg.NewAddress);
|
|
cs.Add(uc);
|
|
Debug.WriteLine("EditAddress: changing addr at offset +" +
|
|
firstOffset.ToString("x6") + " to $" + dlg.NewAddress.ToString("x4"));
|
|
}
|
|
|
|
// We want to create an entry for the chunk that follows the selected area.
|
|
// We don't modify the trailing address if an entry already exists.
|
|
// (Note the "can edit" code prevented us from being called if there's an
|
|
// address map entry in the middle of the selected area.)
|
|
//
|
|
// If they're removing an existing entry, don't add a new entry at the end.
|
|
if (nextAddr >= 0 && dlg.NewAddress != AddressMap.NO_ENTRY_ADDR &&
|
|
mProject.AddrMap.Get(nextOffset) == AddressMap.NO_ENTRY_ADDR) {
|
|
// We don't screen for redundant entries here. That should only happen if
|
|
// they select a range and then don't change the address. Maybe it's useful?
|
|
int prevAddress = mProject.AddrMap.Get(nextOffset);
|
|
UndoableChange uc = UndoableChange.CreateAddressChange(nextOffset,
|
|
prevAddress, nextAddr);
|
|
cs.Add(uc);
|
|
Debug.WriteLine("EditAddress: setting trailing addr at offset +" +
|
|
nextOffset.ToString("x6") + " to $" + nextAddr.ToString("x4"));
|
|
}
|
|
|
|
if (cs.Count > 0) {
|
|
ApplyUndoableChanges(cs);
|
|
} else {
|
|
Debug.WriteLine("EditAddress: no changes");
|
|
}
|
|
}
|
|
|
|
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, offset,
|
|
mProject.SymbolTable, mFormatter);
|
|
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];
|
|
}
|
|
if (oldUserValue == dlg.LabelSym) {
|
|
// Only expected when attr.Symbol is Auto
|
|
Debug.Assert(attr.Symbol.SymbolSource == Symbol.Source.Auto);
|
|
Debug.Assert(oldUserValue == null);
|
|
Debug.WriteLine("Ignoring attempt to delete an auto label");
|
|
} else {
|
|
UndoableChange uc = UndoableChange.CreateLabelChange(offset,
|
|
oldUserValue, dlg.LabelSym);
|
|
ChangeSet cs = new ChangeSet(uc);
|
|
ApplyUndoableChanges(cs);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CanCreateLocalVariableTable() {
|
|
if (SelectionAnalysis.mNumItemsSelected != 1) {
|
|
return false;
|
|
}
|
|
// Only allow on code lines. This is somewhat arbitrary; data would work fine.
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
if (CodeLineList[selIndex].LineType != LineListGen.Line.Type.Code) {
|
|
return false;
|
|
}
|
|
int offset = CodeLineList[selIndex].FileOffset;
|
|
// Don't allow creation if a table already exists.
|
|
return !mProject.LvTables.ContainsKey(offset);
|
|
}
|
|
|
|
public void CreateLocalVariableTable() {
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
int offset = CodeLineList[selIndex].FileOffset;
|
|
Debug.Assert(!mProject.LvTables.ContainsKey(offset));
|
|
CreateOrEditLocalVariableTable(offset);
|
|
}
|
|
|
|
public bool CanEditLocalVariableTable() {
|
|
if (SelectionAnalysis.mNumItemsSelected != 1) {
|
|
return false;
|
|
}
|
|
// Check to see if the offset of the first-defined table is less than or equal to
|
|
// the offset of the selected line. If so, we know there's a table, though we
|
|
// don't know which one.
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
int offset = CodeLineList[selIndex].FileOffset;
|
|
return mProject.LvTables.Count > 0 && mProject.LvTables.Keys[0] <= offset;
|
|
}
|
|
|
|
public void EditLocalVariableTable() {
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
int offset = CodeLineList[selIndex].FileOffset;
|
|
|
|
LocalVariableLookup lvLookup = new LocalVariableLookup(mProject.LvTables, mProject,
|
|
null, false, false);
|
|
int bestOffset = lvLookup.GetNearestTableOffset(offset);
|
|
Debug.Assert(bestOffset >= 0);
|
|
CreateOrEditLocalVariableTable(bestOffset);
|
|
}
|
|
|
|
private void CreateOrEditLocalVariableTable(int offset) {
|
|
// Get existing table, if any.
|
|
mProject.LvTables.TryGetValue(offset, out LocalVariableTable oldLvt);
|
|
|
|
EditLocalVariableTable dlg = new EditLocalVariableTable(mMainWin, mProject,
|
|
mFormatter, oldLvt, offset);
|
|
if (dlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
if (offset != dlg.NewOffset) {
|
|
// Table moved. We create two changes, one to delete the current table, one
|
|
// to create a new table.
|
|
Debug.Assert(!mProject.LvTables.TryGetValue(dlg.NewOffset,
|
|
out LocalVariableTable unused));
|
|
|
|
UndoableChange rem = UndoableChange.CreateLocalVariableTableChange(offset,
|
|
oldLvt, null);
|
|
UndoableChange add = UndoableChange.CreateLocalVariableTableChange(dlg.NewOffset,
|
|
null, dlg.NewTable);
|
|
ChangeSet cs = new ChangeSet(2);
|
|
cs.Add(rem);
|
|
cs.Add(add);
|
|
ApplyUndoableChanges(cs);
|
|
} else if (oldLvt != dlg.NewTable) {
|
|
// New table, edited in place, or deleted.
|
|
UndoableChange uc = UndoableChange.CreateLocalVariableTableChange(offset,
|
|
oldLvt, dlg.NewTable);
|
|
ChangeSet cs = new ChangeSet(uc);
|
|
ApplyUndoableChanges(cs);
|
|
} else {
|
|
Debug.WriteLine("LvTable unchanged");
|
|
}
|
|
}
|
|
|
|
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, mFormatter);
|
|
if (mProject.LongComments.TryGetValue(offset, out MultiLineComment oldComment)) {
|
|
dlg.LongComment = oldComment;
|
|
}
|
|
if (dlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
|
|
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 {
|
|
// We allow the selection to include meta-data like .org and Notes.
|
|
//Debug.Assert(CodeLineList[selIndex].LineType == LineListGen.Line.Type.Data);
|
|
EditDataOperand(selOffset);
|
|
}
|
|
}
|
|
|
|
private void EditInstructionOperand(int offset) {
|
|
EditInstructionOperand dlg = new EditInstructionOperand(mMainWin, mProject,
|
|
offset, mFormatter);
|
|
if (dlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet(1);
|
|
mProject.OperandFormats.TryGetValue(offset, out FormatDescriptor dfd);
|
|
if (dlg.FormatDescriptorResult != dfd) {
|
|
UndoableChange uc = UndoableChange.CreateOperandFormatChange(offset,
|
|
dfd, dlg.FormatDescriptorResult);
|
|
cs.Add(uc);
|
|
} else {
|
|
Debug.WriteLine("No change to operand format");
|
|
}
|
|
|
|
// Check for changes to a local variable table. The edit dialog can't delete an
|
|
// entire table, so a null value here means no changes were made.
|
|
if (dlg.LocalVariableResult != null) {
|
|
int tableOffset = dlg.LocalVariableTableOffsetResult;
|
|
LocalVariableTable lvt = mProject.LvTables[tableOffset];
|
|
Debug.Assert(lvt != null); // cannot create a table either
|
|
UndoableChange uc = UndoableChange.CreateLocalVariableTableChange(tableOffset,
|
|
lvt, dlg.LocalVariableResult);
|
|
cs.Add(uc);
|
|
} else {
|
|
Debug.WriteLine("No change to LvTable");
|
|
}
|
|
|
|
// Check for changes to label at operand target address. Labels can be created,
|
|
// modified, or deleted.
|
|
if (dlg.SymbolEditOffsetResult >= 0) {
|
|
mProject.UserLabels.TryGetValue(dlg.SymbolEditOffsetResult, out Symbol oldLabel);
|
|
UndoableChange uc = UndoableChange.CreateLabelChange(dlg.SymbolEditOffsetResult,
|
|
oldLabel, dlg.SymbolEditResult);
|
|
cs.Add(uc);
|
|
} else {
|
|
Debug.WriteLine("No change to label");
|
|
}
|
|
|
|
// Check for changes to a project property. The dialog can create a new entry or
|
|
// modify an existing entry.
|
|
if (dlg.ProjectPropertyResult != null) {
|
|
DefSymbol oldSym = dlg.PrevProjectPropertyResult;
|
|
DefSymbol newSym = dlg.ProjectPropertyResult;
|
|
ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps);
|
|
// Add new entry, or replace existing entry.
|
|
if (oldSym != null) {
|
|
newProps.ProjectSyms.Remove(oldSym.Label);
|
|
}
|
|
newProps.ProjectSyms.Add(newSym.Label, newSym);
|
|
UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(
|
|
mProject.ProjectProps, newProps);
|
|
cs.Add(uc);
|
|
} else {
|
|
Debug.WriteLine("No change to project property");
|
|
}
|
|
|
|
Debug.WriteLine("EditInstructionOperand: " + cs.Count + " changes");
|
|
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, mFormatter, trs, dfd);
|
|
if (dlg.ShowDialog() == 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 void EditProjectProperties(WpfGui.EditProjectProperties.Tab initialTab) {
|
|
string projectDir = string.Empty;
|
|
if (!string.IsNullOrEmpty(mProjectPathName)) {
|
|
projectDir = Path.GetDirectoryName(mProjectPathName);
|
|
}
|
|
EditProjectProperties dlg = new EditProjectProperties(mMainWin, mProject.ProjectProps,
|
|
projectDir, mFormatter, initialTab);
|
|
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 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, mFormatter,
|
|
mProject.ProjectProps.ProjectSyms, origDefSym, null);
|
|
if (dlg.ShowDialog() == true) {
|
|
ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps);
|
|
newProps.ProjectSyms.Remove(origDefSym.Label);
|
|
newProps.ProjectSyms[dlg.NewSym.Label] = dlg.NewSym;
|
|
|
|
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 bool CanEditVisualizationSet() {
|
|
if (SelectionAnalysis.mNumItemsSelected != 1) {
|
|
return false;
|
|
}
|
|
EntityCounts counts = SelectionAnalysis.mEntityCounts;
|
|
// Single line, must be a visualization set.
|
|
LineListGen.Line.Type lineType = SelectionAnalysis.mLineType;
|
|
return (lineType == LineListGen.Line.Type.VisualizationSet ||
|
|
lineType == LineListGen.Line.Type.Code ||
|
|
lineType == LineListGen.Line.Type.Data);
|
|
}
|
|
|
|
public void EditVisualizationSet() {
|
|
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
int offset = CodeLineList[selIndex].FileOffset;
|
|
mProject.VisualizationSets.TryGetValue(offset, out VisualizationSet curVisSet);
|
|
|
|
EditVisualizationSet dlg = new EditVisualizationSet(mMainWin, mProject,
|
|
mFormatter, curVisSet, offset);
|
|
if (dlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
VisualizationSet newSet = dlg.NewVisSet;
|
|
if (newSet.Count == 0) {
|
|
// empty sets are deleted
|
|
newSet = null;
|
|
}
|
|
if (curVisSet != newSet) {
|
|
ChangeSet cs = new ChangeSet(1);
|
|
|
|
// New table, edited in place, or deleted.
|
|
UndoableChange uc = UndoableChange.CreateVisualizationSetChange(offset,
|
|
curVisSet, newSet);
|
|
//Debug.WriteLine("Change " + curVisSet + " to " + newSet);
|
|
cs.Add(uc);
|
|
|
|
// And now the messy bit. If Visualizations were removed, we need to purge
|
|
// them from any animations that reference them. The edit dialog took care
|
|
// of this for animations in the same set, but we need to check other sets.
|
|
foreach (KeyValuePair<int, VisualizationSet> kvp in mProject.VisualizationSets) {
|
|
if (kvp.Value == curVisSet) {
|
|
continue;
|
|
}
|
|
|
|
VisualizationSet stripSet;
|
|
if (VisualizationSet.StripEntriesFromAnimations(kvp.Value, dlg.RemovedSerials,
|
|
out stripSet)) {
|
|
if (stripSet.Count == 0) {
|
|
stripSet = null;
|
|
}
|
|
uc = UndoableChange.CreateVisualizationSetChange(kvp.Key,
|
|
kvp.Value, stripSet);
|
|
cs.Add(uc);
|
|
Debug.WriteLine("Also updating visSet at +" + kvp.Key.ToString("x6"));
|
|
}
|
|
}
|
|
|
|
ApplyUndoableChanges(cs);
|
|
} else {
|
|
Debug.WriteLine("No change to VisualizationSet");
|
|
}
|
|
}
|
|
|
|
public void Export() {
|
|
string outName;
|
|
if (string.IsNullOrEmpty(mProjectPathName)) {
|
|
outName = Path.GetFileName(mDataPathName);
|
|
} else {
|
|
outName = Path.GetFileName(mProjectPathName);
|
|
}
|
|
|
|
Export dlg = new Export(mMainWin, outName);
|
|
if (dlg.ShowDialog() == false) {
|
|
return;
|
|
}
|
|
|
|
int[] rightWidths = new int[] {
|
|
dlg.AsmLabelColWidth, dlg.AsmOpcodeColWidth,
|
|
dlg.AsmOperandColWidth, dlg.AsmCommentColWidth
|
|
};
|
|
Exporter eport = new Exporter(mProject, CodeLineList, mFormatter,
|
|
dlg.ColFlags, rightWidths);
|
|
eport.IncludeNotes = dlg.IncludeNotes;
|
|
eport.GenerateImageFiles = dlg.GenerateImageFiles;
|
|
eport.LongLabelNewLine = dlg.LongLabelNewLine;
|
|
if (dlg.SelectionOnly) {
|
|
DisplayListSelection selection = mMainWin.CodeDisplayList.SelectedIndices;
|
|
if (selection.Count == 0) {
|
|
// no selection == select all
|
|
selection = null;
|
|
}
|
|
eport.Selection = selection;
|
|
}
|
|
|
|
if (dlg.GenType == WpfGui.Export.GenerateFileType.Html) {
|
|
// Generating wireframe animations can be slow, so we need to use a
|
|
// progress dialog.
|
|
eport.OutputToHtml(mMainWin, dlg.PathName, dlg.OverwriteCss);
|
|
} else {
|
|
// Text output is generally very fast. Put up a wait cursor just in case.
|
|
try {
|
|
Mouse.OverrideCursor = Cursors.Wait;
|
|
eport.OutputToText(dlg.PathName, dlg.TextModeCsv);
|
|
} finally {
|
|
Mouse.OverrideCursor = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Find() {
|
|
FindBox dlg = new FindBox(mMainWin, mFindString);
|
|
if (dlg.ShowDialog() == true) {
|
|
mFindString = dlg.TextToFind;
|
|
mFindStartIndex = -1;
|
|
FindText(dlg.IsBackward);
|
|
}
|
|
}
|
|
|
|
public void FindNext() {
|
|
FindText(false);
|
|
}
|
|
|
|
public void FindPrevious() {
|
|
FindText(true);
|
|
}
|
|
|
|
private void FindText(bool goBackward) {
|
|
if (string.IsNullOrEmpty(mFindString)) {
|
|
return;
|
|
}
|
|
int incr = goBackward ? -1 : 1;
|
|
|
|
// If we reversed direction, reset the "start index" so we don't tell the user
|
|
// we've wrapped around.
|
|
if (mFindBackward != goBackward) {
|
|
mFindStartIndex = -1;
|
|
mFindBackward = goBackward;
|
|
}
|
|
|
|
// Start from the topmost selected line, or the start of the file if nothing
|
|
// is selected.
|
|
// TODO(maybe): if multiple lines are selected, search only within the selected set.
|
|
int index = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
if (index < 0) {
|
|
index = 0;
|
|
}
|
|
|
|
// Start one past the selected item.
|
|
index += incr;
|
|
if (index == CodeLineList.Count) {
|
|
index = 0;
|
|
} else if (index == -1) {
|
|
index = CodeLineList.Count - 1;
|
|
}
|
|
//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_SelectRange(index, 1);
|
|
return;
|
|
}
|
|
|
|
index += incr;
|
|
if (index == CodeLineList.Count) {
|
|
index = 0;
|
|
} else if (index == -1) {
|
|
index = CodeLineList.Count - 1;
|
|
}
|
|
}
|
|
|
|
// 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.HasCommentNoteOrVis(nextOffset) &&
|
|
mProject.AddrMap.IsSingleAddrRange(nextOffset - 1, 2)) {
|
|
// 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 CanFormatAddressTable() {
|
|
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 FormatAddressTable() {
|
|
TypedRangeSet trs = GroupedOffsetSetFromSelected();
|
|
if (trs.Count == 0) {
|
|
// shouldn't happen
|
|
Debug.Assert(false, "FormatSplitAddressTable found nothing to edit");
|
|
return;
|
|
}
|
|
|
|
FormatAddressTable dlg = new FormatAddressTable(mMainWin, mProject, trs,
|
|
mFormatter);
|
|
|
|
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() {
|
|
int index = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
if (index < 0) {
|
|
index = mMainWin.CodeListView_GetTopIndex(); // nothing selected
|
|
}
|
|
int offset = CodeLineList[index].FileOffset;
|
|
|
|
GotoBox dlg = new GotoBox(mMainWin, mProject, offset, mFormatter);
|
|
if (dlg.ShowDialog() == true) {
|
|
GoToLocation(new NavStack.Location(dlg.TargetOffset, 0, false),
|
|
GoToMode.JumpToCodeData, true);
|
|
mMainWin.CodeListView_Focus();
|
|
}
|
|
}
|
|
|
|
public enum GoToMode { Unknown = 0, JumpToCodeData, JumpToNote, JumpToAdjIndex };
|
|
/// <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 GoToLocation(NavStack.Location loc, GoToMode mode, bool doPush) {
|
|
NavStack.Location prevLoc = GetCurrentlySelectedLocation();
|
|
//Debug.WriteLine("GoToLocation: " + loc + " mode=" + mode + " doPush=" + doPush +
|
|
// " (curLoc=" + prevLoc + ")");
|
|
|
|
// Avoid pushing multiple copies of the same address on. This doesn't quite work
|
|
// because we can't compare the LineDelta without figuring out JumpToCodeData first.
|
|
// If we're sitting in a long comment or LvTable and the user double-clicks on the
|
|
// entry in the symbol table for the current offset, we want to move the selection,
|
|
// so we don't want to bail out if the offset matches. Easiest thing to do is to
|
|
// do the move but not push it.
|
|
bool jumpToNote = (mode == GoToMode.JumpToNote);
|
|
if (loc.Offset == prevLoc.Offset && jumpToNote == prevLoc.IsNote) {
|
|
// we're jumping to ourselves?
|
|
if (doPush) {
|
|
Debug.WriteLine("Ignoring push for goto to current offset");
|
|
doPush = false;
|
|
}
|
|
}
|
|
|
|
int topLineIndex = CodeLineList.FindLineIndexByOffset(loc.Offset);
|
|
if (topLineIndex < 0) {
|
|
Debug.Assert(false, "failed goto offset +" + loc.Offset.ToString("x6"));
|
|
return;
|
|
}
|
|
int lastLineIndex;
|
|
if (mode == GoToMode.JumpToNote) {
|
|
// Select all note lines, disregard the rest.
|
|
while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Note) {
|
|
topLineIndex++;
|
|
Debug.Assert(CodeLineList[topLineIndex].FileOffset == loc.Offset);
|
|
}
|
|
lastLineIndex = topLineIndex + 1;
|
|
while (lastLineIndex < CodeLineList.Count &&
|
|
CodeLineList[lastLineIndex].LineType == LineListGen.Line.Type.Note) {
|
|
lastLineIndex++;
|
|
}
|
|
} else if (loc.Offset < 0) {
|
|
// This is the offset of the header comment or a .EQ directive. Don't mess with it.
|
|
lastLineIndex = topLineIndex + 1;
|
|
} else if (mode == GoToMode.JumpToCodeData) {
|
|
// 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;
|
|
} else if (mode == GoToMode.JumpToAdjIndex) {
|
|
// Adjust the line position by the line delta. If the adjustment moves us to
|
|
// a different element, ignore the adjustment.
|
|
if (CodeLineList[topLineIndex].FileOffset ==
|
|
CodeLineList[topLineIndex + loc.LineDelta].FileOffset) {
|
|
topLineIndex += loc.LineDelta;
|
|
}
|
|
lastLineIndex = topLineIndex + 1;
|
|
} else {
|
|
Debug.Assert(false);
|
|
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_SelectRange(topLineIndex, lastLineIndex - topLineIndex);
|
|
|
|
if (doPush) {
|
|
// Update the back stack and associated controls.
|
|
mNavStack.Push(prevLoc);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the view and selection to the definition of a local variable.
|
|
/// </summary>
|
|
/// <param name="offset">Offset at which the variable was referenced.</param>
|
|
/// <param name="symRef">Reference to variable.</param>
|
|
public void GoToVarDefinition(int offset, WeakSymbolRef symRef, bool doPush) {
|
|
Debug.Assert(offset >= 0);
|
|
Debug.Assert(symRef.IsVariable);
|
|
|
|
LocalVariableLookup lvLookup = new LocalVariableLookup(mProject.LvTables, mProject,
|
|
null, false, false);
|
|
int varOffset = lvLookup.GetDefiningTableOffset(offset, symRef);
|
|
if (varOffset <= 0) {
|
|
Debug.WriteLine("Local variable not found; offset=" + offset + " ref=" + symRef);
|
|
return;
|
|
}
|
|
|
|
// Find the actual symbol definition.
|
|
LocalVariableTable lvTable = mProject.LvTables[varOffset];
|
|
DefSymbol foundSym = lvTable.GetByLabel(symRef.Label);
|
|
if (foundSym == null) {
|
|
// shouldn't be possible
|
|
Debug.WriteLine("Did not find " + symRef.Label + " in expected table");
|
|
Debug.Assert(false);
|
|
return;
|
|
}
|
|
|
|
// We have the offset to which the local variable table is bound. We need to
|
|
// walk down until we find the variable definitions, and find the line with the
|
|
// matching symbol.
|
|
//
|
|
// We're comparing to the formatted strings -- safer than trying to find the symbol
|
|
// in the table and then guess at how the table arranges itself for display -- so we
|
|
// need to compare the formatted form of the label.
|
|
//
|
|
// We need to use GenerateDisplayLabel() because the symbol might have an annotation.
|
|
string cmpStr = mFormatter.FormatVariableLabel(
|
|
foundSym.GenerateDisplayLabel(mFormatter));
|
|
int lineIndex = CodeLineList.FindLineIndexByOffset(varOffset);
|
|
while (lineIndex < mProject.FileDataLength) {
|
|
LineListGen.Line line = CodeLineList[lineIndex];
|
|
if (line.FileOffset != varOffset) {
|
|
// we've gone too far
|
|
Debug.WriteLine("ran out of LV table");
|
|
return;
|
|
}
|
|
|
|
if (line.LineType == LineListGen.Line.Type.LocalVariableTable) {
|
|
DisplayList.FormattedParts parts = CodeLineList.GetFormattedParts(lineIndex);
|
|
if (cmpStr.Equals(parts.Label)) {
|
|
// Eureka
|
|
NavStack.Location prevLoc = GetCurrentlySelectedLocation();
|
|
|
|
mMainWin.CodeListView_EnsureVisible(lineIndex);
|
|
|
|
// Update the selection.
|
|
mMainWin.CodeListView_SelectRange(lineIndex, 1);
|
|
|
|
if (doPush) {
|
|
// Update the back stack and associated controls.
|
|
mNavStack.Push(prevLoc);
|
|
}
|
|
|
|
return;
|
|
} else {
|
|
//Debug.WriteLine("Var: '" + cmpStr + "' != '" + parts.Label + "'");
|
|
}
|
|
}
|
|
|
|
lineIndex++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the currently-selected location.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is done whenever we jump somewhere else. For the most part we'll be in a
|
|
/// line of code, jumping when an operand or reference is double-clicked, but we might
|
|
/// be in the middle of a long comment when a symbol is double-clicked or the
|
|
/// nav-forward arrow is clicked. The most interesting case is when a reference for
|
|
/// a local variable table entry is double-clicked, since we want to be sure that we
|
|
/// return to the correct entry in the LvTable (assuming it still exists).
|
|
/// </remarks>
|
|
/// <returns>Returns the location.</returns>
|
|
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;
|
|
int lineDelta = index - CodeLineList.FindLineIndexByOffset(offset);
|
|
bool isNote = (CodeLineList[index].LineType == LineListGen.Line.Type.Note);
|
|
return new NavStack.Location(offset, lineDelta, isNote);
|
|
}
|
|
|
|
public void GotoLastChange() {
|
|
ChangeSet cs = mProject.GetTopChange();
|
|
Debug.Assert(cs.Count > 0);
|
|
|
|
// Get the offset from the first change in the set. Ignore the rest.
|
|
UndoableChange uc = cs[0];
|
|
int offset;
|
|
bool isNote = false;
|
|
if (uc.HasOffset) {
|
|
offset = uc.Offset;
|
|
|
|
// If we altered a Note, and didn't remove it, jump to the note instead of
|
|
// the nearby code/data.
|
|
//
|
|
// TODO(someday): we can do similar things for comment edits, e.g. if it's
|
|
// SetLongComment we can find the line on which the comment starts and
|
|
// pass that as a line delta.
|
|
if (uc.Type == UndoableChange.ChangeType.SetNote &&
|
|
uc.NewValue != null) {
|
|
isNote = true;
|
|
}
|
|
} else if (uc.Type == UndoableChange.ChangeType.SetProjectProperties) {
|
|
// some chance it modified the EQU statements... jump there
|
|
offset = 0;
|
|
} else if (uc.Type == UndoableChange.ChangeType.SetTypeHint) {
|
|
TypedRangeSet newSet = (TypedRangeSet)uc.NewValue;
|
|
if (newSet.Count == 0) {
|
|
// unexpected
|
|
Debug.Assert(false);
|
|
return;
|
|
}
|
|
|
|
// Get the offset of the first entry.
|
|
IEnumerator<TypedRangeSet.Tuple> iter =
|
|
(IEnumerator<TypedRangeSet.Tuple>)newSet.GetEnumerator();
|
|
iter.MoveNext();
|
|
TypedRangeSet.Tuple firstOffset = iter.Current;
|
|
offset = firstOffset.Value;
|
|
} else {
|
|
Debug.Assert(false);
|
|
return;
|
|
}
|
|
|
|
if (isNote) {
|
|
GoToLocation(new NavStack.Location(offset, 0, true),
|
|
GoToMode.JumpToNote, true);
|
|
} else {
|
|
GoToLocation(new NavStack.Location(offset, 0, false),
|
|
GoToMode.JumpToCodeData, true);
|
|
}
|
|
}
|
|
|
|
public bool CanNavigateBackward() {
|
|
return mNavStack.HasBackward;
|
|
}
|
|
public void NavigateBackward() {
|
|
Debug.Assert(mNavStack.HasBackward);
|
|
NavStack.Location backLoc = mNavStack.MoveBackward(GetCurrentlySelectedLocation());
|
|
GoToLocation(backLoc,
|
|
backLoc.IsNote ? GoToMode.JumpToNote : GoToMode.JumpToAdjIndex, false);
|
|
}
|
|
|
|
public bool CanNavigateForward() {
|
|
return mNavStack.HasForward;
|
|
}
|
|
public void NavigateForward() {
|
|
Debug.Assert(mNavStack.HasForward);
|
|
NavStack.Location fwdLoc = mNavStack.MoveForward(GetCurrentlySelectedLocation());
|
|
GoToLocation(fwdLoc,
|
|
fwdLoc.IsNote ? GoToMode.JumpToNote : GoToMode.JumpToAdjIndex, 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) {
|
|
GoToLocation(new NavStack.Location(offset, 0, false),
|
|
GoToMode.JumpToCodeData, 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;
|
|
}
|
|
|
|
private bool mUpdatingSelectionHighlight; // recursion guard
|
|
/// <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 && !mUpdatingSelectionHighlight) {
|
|
Debug.WriteLine("Target highlight moving from " + mTargetHighlightIndex +
|
|
" to " + targetIndex);
|
|
|
|
// The highlight is currently implemented by modifying the item in the
|
|
// display list. Because those items are immutable, we have to remove the
|
|
// old and add a new. The WPF ListView maintains its selection by object
|
|
// reference, so replacing an item requires removing the old item from the
|
|
// selection set and adding it to the new.
|
|
//
|
|
// So if a line references itself (like the ZipGS cache conditioner loop does),
|
|
// it will be the selected line while we're doing this little dance. When the
|
|
// calls below update the selection, this method will be called again. This
|
|
// turns into infinite recursion.
|
|
mUpdatingSelectionHighlight = true;
|
|
mMainWin.CodeListView_RemoveSelectionHighlight(mTargetHighlightIndex);
|
|
mMainWin.CodeListView_AddSelectionHighlight(targetIndex);
|
|
mUpdatingSelectionHighlight = false;
|
|
|
|
mTargetHighlightIndex = 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 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, mFormatter);
|
|
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 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 ||
|
|
mProject.GetAnattrib(firstOffset).IsInlineDataStart);
|
|
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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
///
|
|
/// The type values used in the TypedRangeSet may not be contiguous. They're only
|
|
/// there to create group separation from otherwise contiguous address ranges.
|
|
/// </remarks>
|
|
/// <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 things that start a new group.
|
|
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
|
|
// (the TypedRangeSet splits at the gap).
|
|
//Debug.WriteLine("Address break: $" + attr.Address.ToString("x4") + " vs. $"
|
|
// + expectedAddr.ToString("x4"));
|
|
expectedAddr = attr.Address;
|
|
groupNum++;
|
|
} else if (offset > 0 && !mProject.AddrMap.IsSingleAddrRange(offset - 1, 2)) {
|
|
// Was the previous byte in a different address range? This is only
|
|
// strictly necessary if the previous byte was in the selection set (which
|
|
// it won't be if the selection starts at the beginning of an address
|
|
// range), but bumping the group number is harmless if it wasn't.
|
|
groupNum++;
|
|
} else 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.HasCommentNoteOrVis(offset)) {
|
|
// Don't carry across a long comment, note, or visualization.
|
|
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 &&
|
|
type != LineListGen.Line.Type.LocalVariableTable) {
|
|
// Code, data, and platform symbol EQUs have xrefs.
|
|
return;
|
|
}
|
|
|
|
XrefSet xrefs;
|
|
|
|
// Find the appropriate xref set.
|
|
if (type == LineListGen.Line.Type.LocalVariableTable) {
|
|
DefSymbol defSym = CodeLineList.GetLocalVariableFromLine(lineIndex);
|
|
xrefs = (defSym == null) ? null : defSym.Xrefs;
|
|
} else {
|
|
int offset = CodeLineList[lineIndex].FileOffset;
|
|
if (offset < 0) {
|
|
// EQU in header
|
|
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 = mFormatter;
|
|
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.IsByName ? "Sym " : "Oth ") + 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,
|
|
mFormatter.FormatOffset24(offset),
|
|
nocrlfStr,
|
|
mlc.BackgroundColor);
|
|
mMainWin.NotesList.Add(nli);
|
|
}
|
|
}
|
|
|
|
#endregion Notes panel
|
|
|
|
#region Symbols panel
|
|
|
|
/// <summary>
|
|
/// Populates the ItemsSource for the Symbols window. Each entry in the project
|
|
/// symbol table is added.
|
|
/// </summary>
|
|
private void PopulateSymbolsList() {
|
|
mMainWin.SymbolsList.Clear();
|
|
foreach (Symbol sym in mProject.SymbolTable) {
|
|
string valueStr = mFormatter.FormatHexValue(sym.Value, 0);
|
|
string sourceTypeStr = sym.SourceTypeString;
|
|
if (sym is DefSymbol) {
|
|
DefSymbol defSym = (DefSymbol)sym;
|
|
if (defSym.MultiMask != null) {
|
|
valueStr += " & " +
|
|
mFormatter.FormatHexValue(defSym.MultiMask.AddressMask, 4);
|
|
}
|
|
if (defSym.Direction == DefSymbol.DirectionFlags.Read) {
|
|
sourceTypeStr += '<';
|
|
} else if (defSym.Direction == DefSymbol.DirectionFlags.Write) {
|
|
sourceTypeStr += '>';
|
|
}
|
|
}
|
|
|
|
MainWindow.SymbolsListItem sli = new MainWindow.SymbolsListItem(sym,
|
|
sourceTypeStr, valueStr, sym.GenerateDisplayLabel(mFormatter));
|
|
mMainWin.SymbolsList.Add(sli);
|
|
}
|
|
}
|
|
|
|
#endregion Symbols panel
|
|
|
|
#region Info panel
|
|
|
|
private void UpdateInfoPanel() {
|
|
mMainWin.ClearInfoPanel();
|
|
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
|
|
// Nothing selected, or multiple lines selected.
|
|
return;
|
|
}
|
|
int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
|
|
LineListGen.Line line = CodeLineList[lineIndex];
|
|
|
|
// TODO(someday): this should be made easier to localize
|
|
string lineTypeStr = null;
|
|
bool isSimple = true;
|
|
DefSymbol defSym = null;
|
|
switch (line.LineType) {
|
|
case LineListGen.Line.Type.LongComment:
|
|
lineTypeStr = "comment";
|
|
break;
|
|
case LineListGen.Line.Type.Note:
|
|
lineTypeStr = "note";
|
|
break;
|
|
case LineListGen.Line.Type.Blank:
|
|
lineTypeStr = "blank line";
|
|
break;
|
|
case LineListGen.Line.Type.OrgDirective:
|
|
lineTypeStr = "address directive";
|
|
break;
|
|
case LineListGen.Line.Type.RegWidthDirective:
|
|
lineTypeStr = "register width directive";
|
|
break;
|
|
|
|
case LineListGen.Line.Type.LocalVariableTable:
|
|
isSimple = false;
|
|
lineTypeStr = "variable table";
|
|
break;
|
|
case LineListGen.Line.Type.Code:
|
|
isSimple = false;
|
|
lineTypeStr = "code";
|
|
break;
|
|
case LineListGen.Line.Type.Data:
|
|
isSimple = false;
|
|
if (mProject.GetAnattrib(line.FileOffset).IsInlineData) {
|
|
lineTypeStr = "inline data";
|
|
} else {
|
|
lineTypeStr = "data";
|
|
}
|
|
break;
|
|
case LineListGen.Line.Type.EquDirective:
|
|
isSimple = false;
|
|
int defSymIndex = LineListGen.DefSymIndexFromOffset(line.FileOffset);
|
|
defSym = mProject.ActiveDefSymbolList[defSymIndex];
|
|
if (defSym.SymbolSource == Symbol.Source.Project) {
|
|
lineTypeStr = "project symbol equate";
|
|
} else if (defSym.SymbolSource == Symbol.Source.Platform) {
|
|
lineTypeStr = "platform symbol equate";
|
|
} else {
|
|
lineTypeStr = "???";
|
|
}
|
|
break;
|
|
case LineListGen.Line.Type.VisualizationSet:
|
|
lineTypeStr = "visualization set";
|
|
break;
|
|
default:
|
|
lineTypeStr = "???";
|
|
break;
|
|
}
|
|
|
|
if (line.IsCodeOrData) {
|
|
// Show number of bytes of code/data.
|
|
if (line.OffsetSpan == 1) {
|
|
mMainWin.InfoLineDescrText =
|
|
string.Format(Res.Strings.INFO_LINE_SUM_SINGULAR_FMT,
|
|
lineIndex, line.OffsetSpan, lineTypeStr);
|
|
} else {
|
|
mMainWin.InfoLineDescrText =
|
|
string.Format(Res.Strings.INFO_LINE_SUM_PLURAL_FMT,
|
|
lineIndex, line.OffsetSpan, lineTypeStr);
|
|
}
|
|
} else {
|
|
mMainWin.InfoLineDescrText = string.Format(Res.Strings.INFO_LINE_SUM_NON_FMT,
|
|
lineIndex, lineTypeStr);
|
|
}
|
|
|
|
#if DEBUG
|
|
mMainWin.InfoOffsetText = ("[offset=+" + line.FileOffset.ToString("x6") + "]");
|
|
#endif
|
|
if (isSimple) {
|
|
return;
|
|
}
|
|
|
|
if (line.LineType == LineListGen.Line.Type.LocalVariableTable) {
|
|
string str = string.Empty;
|
|
if (mProject.LvTables.TryGetValue(line.FileOffset,
|
|
out LocalVariableTable lvt)) {
|
|
if (lvt.Count == 1) {
|
|
str = "1 entry";
|
|
} else {
|
|
str = lvt.Count + " entries";
|
|
}
|
|
if (lvt.ClearPrevious) {
|
|
str += "; clear previous";
|
|
}
|
|
}
|
|
mMainWin.InfoPanelDetail1 = str;
|
|
return;
|
|
}
|
|
|
|
if (line.LineType == LineListGen.Line.Type.EquDirective) {
|
|
StringBuilder esb = new StringBuilder();
|
|
//esb.Append("\u25b6 ");
|
|
esb.Append("\u2022 ");
|
|
if (defSym.IsConstant) {
|
|
esb.Append("Constant");
|
|
} else {
|
|
esb.Append("External address");
|
|
if (defSym.HasWidth) {
|
|
esb.Append(", width=");
|
|
esb.Append(defSym.DataDescriptor.Length);
|
|
}
|
|
}
|
|
if (defSym.Direction != DefSymbol.DirectionFlags.ReadWrite) {
|
|
esb.Append("\r\nI/O direction: ");
|
|
esb.Append(defSym.Direction);
|
|
}
|
|
if (defSym.MultiMask != null) {
|
|
esb.Append("\r\nMulti-mask:");
|
|
int i = 23;
|
|
if ((defSym.MultiMask.AddressMask | defSym.MultiMask.CompareMask |
|
|
defSym.MultiMask.CompareValue) < 0x10000) {
|
|
i = 15;
|
|
}
|
|
for ( ; i >= 0; i--) {
|
|
if ((i & 3) == 3) {
|
|
esb.Append(' ');
|
|
}
|
|
int bit = 1 << i;
|
|
if ((defSym.MultiMask.AddressMask & bit) != 0) {
|
|
esb.Append('x');
|
|
} else if ((defSym.MultiMask.CompareMask & bit) != 0) {
|
|
if ((defSym.MultiMask.CompareValue & bit) != 0) {
|
|
esb.Append('1');
|
|
} else {
|
|
esb.Append('0');
|
|
}
|
|
} else {
|
|
esb.Append('?');
|
|
}
|
|
}
|
|
}
|
|
if (defSym.SymbolSource == Symbol.Source.Platform) {
|
|
esb.Append("\r\n\r\nSource file # ");
|
|
esb.Append(defSym.LoadOrdinal);
|
|
esb.Append(": ");
|
|
esb.Append(defSym.FileIdentifier);
|
|
|
|
if (!string.IsNullOrEmpty(defSym.Tag)) {
|
|
esb.Append(", tag=");
|
|
esb.Append(defSym.Tag);
|
|
}
|
|
}
|
|
mMainWin.InfoPanelDetail1 = esb.ToString();
|
|
return;
|
|
}
|
|
|
|
|
|
//
|
|
// Handle code/data items. In particular, the format descriptor.
|
|
//
|
|
Debug.Assert(line.IsCodeOrData);
|
|
bool isCode = (line.LineType == LineListGen.Line.Type.Code);
|
|
|
|
StringBuilder sb = new StringBuilder(250);
|
|
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
|
|
|
|
if (attr.Symbol != null) {
|
|
string descr;
|
|
switch (attr.Symbol.SymbolType) {
|
|
case Symbol.Type.NonUniqueLocalAddr:
|
|
descr = "non-unique local";
|
|
break;
|
|
case Symbol.Type.LocalOrGlobalAddr:
|
|
descr = "unique local";
|
|
break;
|
|
case Symbol.Type.GlobalAddr:
|
|
descr = "unique global";
|
|
break;
|
|
case Symbol.Type.GlobalAddrExport:
|
|
descr = "global + marked for export";
|
|
break;
|
|
default:
|
|
descr = "???";
|
|
break;
|
|
}
|
|
if (attr.Symbol.SymbolSource == Symbol.Source.Auto) {
|
|
descr += ", auto-generated";
|
|
} else if (attr.Symbol.LabelAnno == Symbol.LabelAnnotation.Generated) {
|
|
descr += " [gen]";
|
|
}
|
|
mMainWin.InfoLabelDescrText =
|
|
string.Format(Res.Strings.INFO_LABEL_DESCR_FMT, descr);
|
|
}
|
|
|
|
if (!mProject.OperandFormats.TryGetValue(line.FileOffset, out FormatDescriptor dfd)) {
|
|
// No user-specified format, but there may be a generated format.
|
|
mMainWin.InfoFormatBoxBrush = Brushes.Blue;
|
|
if (attr.DataDescriptor != null) {
|
|
mMainWin.InfoFormatShowSolid = true;
|
|
sb.Append(Res.Strings.INFO_AUTO_FORMAT);
|
|
sb.Append(' ');
|
|
sb.Append(attr.DataDescriptor.ToUiString(!isCode));
|
|
} else {
|
|
mMainWin.InfoFormatShowDashes = true;
|
|
sb.AppendFormat(Res.Strings.INFO_DEFAULT_FORMAT);
|
|
}
|
|
} else {
|
|
// User-specified operand format.
|
|
mMainWin.InfoFormatBoxBrush = Brushes.Green;
|
|
mMainWin.InfoFormatShowSolid = true;
|
|
sb.Append(Res.Strings.INFO_CUSTOM_FORMAT);
|
|
sb.Append(' ');
|
|
sb.Append(dfd.ToUiString(!isCode));
|
|
}
|
|
mMainWin.InfoFormatText = sb.ToString();
|
|
|
|
sb.Clear();
|
|
|
|
// Debug only
|
|
//sb.Append("DEBUG: opAddr=" + attr.OperandAddress.ToString("x4") +
|
|
// " opOff=" + attr.OperandOffset.ToString("x4") + "\r\n");
|
|
|
|
if (attr.IsHinted) {
|
|
sb.Append("\u2022 Hints: ");
|
|
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("\r\n");
|
|
}
|
|
|
|
if (attr.IsInstruction) {
|
|
sb.Append("\r\n");
|
|
|
|
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;
|
|
sb.Append(cycles.ToString());
|
|
|
|
OpDef.CycleMod allMods = op.CycleMods;
|
|
OpDef.CycleMod nowMods =
|
|
mProject.CpuDef.GetOpCycleMod(mProject.FileData[line.FileOffset]);
|
|
if (allMods != 0) {
|
|
StringBuilder nowSb = new StringBuilder();
|
|
StringBuilder otherSb = new StringBuilder();
|
|
int workBits = (int)allMods;
|
|
while (workBits != 0) {
|
|
// Isolate rightmost bit.
|
|
int firstBit = (~workBits + 1) & workBits;
|
|
|
|
string desc = mOpDesc.GetCycleModDescription((OpDef.CycleMod)firstBit);
|
|
if (((int)nowMods & firstBit) != 0) {
|
|
if (nowSb.Length != 0) {
|
|
nowSb.Append(", ");
|
|
}
|
|
nowSb.Append(desc);
|
|
} else {
|
|
if (otherSb.Length != 0) {
|
|
otherSb.Append(", ");
|
|
}
|
|
otherSb.Append(desc);
|
|
}
|
|
// Remove from set.
|
|
workBits &= ~firstBit;
|
|
}
|
|
if (nowSb.Length != 0) {
|
|
sb.Append(" (");
|
|
sb.Append(nowSb);
|
|
sb.Append(")");
|
|
}
|
|
if (otherSb.Length != 0) {
|
|
sb.Append(" [");
|
|
sb.Append(otherSb);
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Publish
|
|
mMainWin.InfoPanelDetail1 = sb.ToString();
|
|
}
|
|
|
|
#endregion Info panel
|
|
|
|
#region Tools
|
|
|
|
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 ToggleInstructionChart() {
|
|
if (mInstructionChartDialog == null) {
|
|
// Create without owner so it doesn't have to be in front of main window.
|
|
mInstructionChartDialog = new Tools.WpfGui.InstructionChart(null, mFormatter);
|
|
mInstructionChartDialog.Closing += (sender, e) => {
|
|
Debug.WriteLine("Instruction chart closed");
|
|
mInstructionChartDialog = null;
|
|
};
|
|
mInstructionChartDialog.Show();
|
|
} else {
|
|
mInstructionChartDialog.Close();
|
|
}
|
|
}
|
|
|
|
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, mFormatter);
|
|
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 ConcatenateFiles() {
|
|
Tools.WpfGui.FileConcatenator concat =
|
|
new Tools.WpfGui.FileConcatenator(this.mMainWin);
|
|
concat.ShowDialog();
|
|
}
|
|
|
|
public void SliceFiles() {
|
|
OpenFileDialog fileDlg = new OpenFileDialog() {
|
|
Filter = Res.Strings.FILE_FILTER_ALL,
|
|
FilterIndex = 1
|
|
};
|
|
if (fileDlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
string pathName = Path.GetFullPath(fileDlg.FileName);
|
|
|
|
Tools.WpfGui.FileSlicer slicer = new Tools.WpfGui.FileSlicer(this.mMainWin, pathName,
|
|
mFormatter);
|
|
slicer.ShowDialog();
|
|
}
|
|
|
|
#endregion Tools
|
|
|
|
#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;
|
|
}
|
|
|
|
public void Debug_ApplesoftToHtml() {
|
|
OpenFileDialog fileDlg = new OpenFileDialog() {
|
|
Filter = Res.Strings.FILE_FILTER_ALL,
|
|
FilterIndex = 1
|
|
};
|
|
if (fileDlg.ShowDialog() != true) {
|
|
return;
|
|
}
|
|
|
|
byte[] data;
|
|
string basPathName = Path.GetFullPath(fileDlg.FileName);
|
|
try {
|
|
data = File.ReadAllBytes(basPathName);
|
|
} catch (Exception ex) {
|
|
// not expecting this to happen
|
|
MessageBox.Show(ex.Message);
|
|
return;
|
|
}
|
|
|
|
Tools.ApplesoftToHtml conv = new Tools.ApplesoftToHtml();
|
|
string html = conv.Convert(data);
|
|
|
|
Tools.WpfGui.ShowText showTextDlg = new Tools.WpfGui.ShowText(mMainWin, html);
|
|
showTextDlg.Title = "Applesoft to HTML";
|
|
showTextDlg.ShowDialog();
|
|
}
|
|
|
|
public void Debug_ApplyPlatformSymbols() {
|
|
ChangeSet cs = new ChangeSet(1);
|
|
|
|
foreach (Symbol sym in mProject.SymbolTable) {
|
|
if (sym.SymbolSource != Symbol.Source.Platform) {
|
|
continue;
|
|
}
|
|
DefSymbol defSym = (DefSymbol)sym;
|
|
if (defSym.MultiMask != null) {
|
|
// These would require additional work... probably.
|
|
continue;
|
|
}
|
|
|
|
int offset = mProject.AddrMap.AddressToOffset(0, sym.Value);
|
|
if (offset < 0) {
|
|
continue;
|
|
}
|
|
|
|
// Make sure this is the start of an instruction or data item. (If you
|
|
// haven't finished hinting code, it's best to disable the string/fill finder.)
|
|
Anattrib attr = mProject.GetAnattrib(offset);
|
|
if (!attr.IsStart) {
|
|
Debug.WriteLine("Found match at non-start +" + offset.ToString("x6") +
|
|
": " + defSym);
|
|
continue;
|
|
}
|
|
|
|
// Check for user label. Okay to overwrite auto label.
|
|
if (mProject.UserLabels.ContainsKey(offset)) {
|
|
Debug.WriteLine("User label already exists at +" + offset.ToString("x6"));
|
|
continue;
|
|
}
|
|
|
|
// Create a new user label symbol. We should not be creating a duplicate name,
|
|
// because user labels have priority over platform symbols when populating
|
|
// the symbol table.
|
|
Symbol newSym = new Symbol(sym.Label, sym.Value, Symbol.Source.User,
|
|
Symbol.Type.GlobalAddr, Symbol.LabelAnnotation.None);
|
|
UndoableChange uc = UndoableChange.CreateLabelChange(offset, null, newSym);
|
|
cs.Add(uc);
|
|
}
|
|
|
|
ApplyUndoableChanges(cs);
|
|
}
|
|
|
|
#endregion Debug features
|
|
}
|
|
}
|