1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-08-09 17:29:17 +00:00
6502bench/SourceGenWPF/MainController.cs
Andy McFadden 99b484df4c Implement long-comment editing
We can now Edit Long Comment and Edit Header Comment.

Changing the number of lines in a long comment exposed a bug in the
display list update.  Fixed.

Running the WinForms version trashed the column widths, which exposed
the fact that the default column widths were set too narrow.  Fixed.

On an unrelated note, hitting undo/redo quickly will sometimes leave
you scrolled to the bottom of the code list.  Some sort of command-
handling race in the WPF framework?  (Not fixed.)
2019-07-07 16:18:46 -07:00

2425 lines
98 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 Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Web.Script.Serialization;
using System.Windows;
using Asm65;
using CommonUtil;
using CommonWPF;
using SourceGenWPF.Sandbox;
using SourceGenWPF.WpfGui;
namespace SourceGenWPF {
/// <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>
/// List of recently-opened projects.
/// </summary>
public List<string> RecentProjectPaths = new List<string>(MAX_RECENT_PROJECTS);
public const int MAX_RECENT_PROJECTS = 6;
/// <summary>
/// Analyzed selection state, updated whenever the selection changes.
/// </summary>
public SelectionState SelectionAnalysis { get; set; }
/// <summary>
/// Activity log generated by the code and data analyzers. Displayed in window.
/// </summary>
private DebugLog mGenerationLog;
/// <summary>
/// Timing data generated during analysis.
/// </summary>
TaskTimer mReanalysisTimer = new TaskTimer();
/// <summary>
/// Stack for navigate forward/backward.
/// </summary>
private NavStack mNavStack = new NavStack();
/// <summary>
/// Output format configuration.
/// </summary>
private Formatter.FormatConfig mFormatterConfig;
/// <summary>
/// Output format controller.
///
/// This is shared with the DisplayList.
/// </summary>
private Formatter mOutputFormatter;
/// <summary>
/// Pseudo-op names.
///
/// This is shared with the DisplayList.
/// </summary>
private PseudoOp.PseudoOpNames mPseudoOpNames;
/// <summary>
/// String we most recently searched for.
/// </summary>
private string mFindString = string.Empty;
/// <summary>
/// Initial start point of most recent search.
/// </summary>
private int mFindStartIndex = -1;
/// <summary>
/// Used to highlight the line that is the target of the selected line.
/// </summary>
private int mTargetHighlightIndex = -1;
/// <summary>
/// CPU definition used when the Formatter was created. If the CPU choice or
/// inclusion of undocumented opcodes changes, we need to wipe the formatter.
/// </summary>
private CpuDef mOutputFormatterCpuDef;
/// <summary>
/// Instruction description object. Used for Info window.
/// </summary>
private OpDescription mOpDesc = OpDescription.GetOpDescription(null);
/// <summary>
/// If true, plugins will execute in the main application's AppDomain instead of
/// the sandbox.
/// </summary>
private bool mUseMainAppDomainForPlugins = false;
/// <summary>
/// Code list column numbers.
/// </summary>
public enum CodeListColumn {
Offset = 0, Address, Bytes, Flags, Attributes, Label, Opcode, Operand, Comment,
COUNT // must be last; must equal number of columns
}
/// <summary>
/// Clipboard format enumeration.
/// </summary>
public enum ClipLineFormat {
Unknown = -1,
AssemblerSource = 0,
Disassembly = 1
}
#region Init and settings
public MainController(MainWindow win) {
mMainWin = win;
}
/// <summary>
/// Early initialization, before the window is visible. Notably, we want to get the
/// window placement data, so we can position and size the window before it's first
/// drawn (avoids a blink).
/// </summary>
public void WindowSourceInitialized() {
// Load the settings from the file. If this fails we have no way to tell the user,
// so just keep going.
LoadAppSettings();
SetAppWindowLocation();
}
/// <summary>
/// Perform one-time initialization after the Window has finished loading. We defer
/// to this point so we can report fatal errors directly to the user.
/// </summary>
public void WindowLoaded() {
if (RuntimeDataAccess.GetDirectory() == null) {
MessageBox.Show(Res.Strings.RUNTIME_DIR_NOT_FOUND,
Res.Strings.RUNTIME_DIR_NOT_FOUND_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
try {
PluginDllCache.PreparePluginDir();
} catch (Exception ex) {
string pluginPath = PluginDllCache.GetPluginDirPath();
if (pluginPath == null) {
pluginPath = "<???>";
}
string msg = string.Format(Res.Strings.PLUGIN_DIR_FAIL_FMT,
pluginPath + ": " + ex.Message);
MessageBox.Show(msg, Res.Strings.PLUGIN_DIR_FAIL_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
// Place the main window and apply the various settings.
ApplyAppSettings();
#if false
UpdateMenuItemsAndTitle();
#endif
mMainWin.UpdateRecentLinks();
ProcessCommandLine();
// Create an initial value.
SelectionAnalysis = UpdateSelectionState();
}
private void ProcessCommandLine() {
string[] args = Environment.GetCommandLineArgs();
if (args.Length == 2) {
DoOpenFile(Path.GetFullPath(args[1]));
}
}
/// <summary>
/// Loads settings from the settings file into AppSettings.Global. Does not apply
/// them to the ProjectView.
/// </summary>
private void LoadAppSettings() {
AppSettings settings = AppSettings.Global;
// Set some default settings for first-time use. The general rule is to set
// a default value of false, 0, or the empty string, so we only need to set
// values here when that isn't the case. The point at which the setting is
// actually used is expected to do something reasonable by default.
settings.SetBool(AppSettings.SYMWIN_SHOW_USER, true);
settings.SetBool(AppSettings.SYMWIN_SHOW_PROJECT, true);
settings.SetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false);
settings.SetBool(AppSettings.SYMWIN_SHOW_AUTO, false);
settings.SetBool(AppSettings.SYMWIN_SHOW_CONST, true);
settings.SetBool(AppSettings.SYMWIN_SHOW_ADDR, true);
settings.SetBool(AppSettings.SYMWIN_SORT_ASCENDING, true);
settings.SetInt(AppSettings.SYMWIN_SORT_COL, (int)Symbol.SymbolSortField.Name);
settings.SetBool(AppSettings.FMT_UPPER_OPERAND_A, true);
settings.SetBool(AppSettings.FMT_UPPER_OPERAND_S, true);
settings.SetBool(AppSettings.FMT_ADD_SPACE_FULL_COMMENT, true);
settings.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);
// Load the settings file, and merge it into the globals.
string runtimeDataDir = RuntimeDataAccess.GetDirectory();
if (runtimeDataDir == null) {
Debug.WriteLine("Unable to load settings file");
return;
}
string settingsDir = Path.GetDirectoryName(runtimeDataDir);
string settingsPath = Path.Combine(settingsDir, SETTINGS_FILE_NAME);
try {
string text = File.ReadAllText(settingsPath);
AppSettings fileSettings = AppSettings.Deserialize(text);
AppSettings.Global.MergeSettings(fileSettings);
Debug.WriteLine("Settings file loaded and merged");
} catch (Exception ex) {
Debug.WriteLine("Unable to read settings file: " + ex.Message);
}
}
/// <summary>
/// Saves AppSettings to a file.
/// </summary>
private void SaveAppSettings() {
if (!AppSettings.Global.Dirty) {
Debug.WriteLine("Settings not dirty, not saving");
return;
}
// Main window position and size.
AppSettings.Global.SetString(AppSettings.MAIN_WINDOW_PLACEMENT,
mMainWin.GetPlacement());
// Horizontal splitters.
AppSettings.Global.SetInt(AppSettings.MAIN_LEFT_PANEL_WIDTH,
(int)mMainWin.LeftPanelWidth);
AppSettings.Global.SetInt(AppSettings.MAIN_RIGHT_PANEL_WIDTH,
(int)mMainWin.RightPanelWidth);
// Vertical splitters.
AppSettings.Global.SetInt(AppSettings.MAIN_REFERENCES_HEIGHT,
(int)mMainWin.ReferencesPanelHeight);
AppSettings.Global.SetInt(AppSettings.MAIN_SYMBOLS_HEIGHT,
(int)mMainWin.SymbolsPanelHeight);
mMainWin.CaptureColumnWidths();
string runtimeDataDir = RuntimeDataAccess.GetDirectory();
if (runtimeDataDir == null) {
Debug.WriteLine("Unable to save settings file");
return;
}
string settingsDir = Path.GetDirectoryName(runtimeDataDir);
string settingsPath = Path.Combine(settingsDir, SETTINGS_FILE_NAME);
try {
string cereal = AppSettings.Global.Serialize();
File.WriteAllText(settingsPath, cereal);
AppSettings.Global.Dirty = false;
Debug.WriteLine("Saved settings (" + settingsPath + ")");
} catch (Exception ex) {
Debug.WriteLine("Failed to save settings: " + ex.Message);
}
}
/// <summary>
/// Replaces the contents of the global settings object with the new settings,
/// then applies them to the project.
/// </summary>
/// <param name="settings"></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.mAllowHighAsciiCharConst = true;
mOutputFormatter = new Formatter(mFormatterConfig);
mOutputFormatterCpuDef = null;
// Set pseudo-op names. Entries aren't allowed to be blank, so we start with the
// default values and merge in whatever the user has configured.
mPseudoOpNames = PseudoOp.sDefaultPseudoOpNames.GetCopy();
string pseudoCereal = settings.GetString(AppSettings.FMT_PSEUDO_OP_NAMES, null);
if (!string.IsNullOrEmpty(pseudoCereal)) {
PseudoOp.PseudoOpNames deser = PseudoOp.PseudoOpNames.Deserialize(pseudoCereal);
if (deser != null) {
mPseudoOpNames.Merge(deser);
}
}
#if false
// Configure the Symbols window.
symbolUserCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_USER, false);
symbolAutoCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_AUTO, false);
symbolProjectCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_PROJECT, false);
symbolPlatformCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false);
symbolConstantCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_CONST, false);
symbolAddressCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR, false);
#endif
// 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();
#if false
// Enable the DEBUG menu if configured.
bool showDebugMenu = AppSettings.Global.GetBool(AppSettings.DEBUG_MENU_ENABLED, false);
if (dEBUGToolStripMenuItem.Visible != showDebugMenu) {
dEBUGToolStripMenuItem.Visible = showDebugMenu;
mainMenuStrip.Refresh();
}
#endif
// Finally, update the display list generator with all the fancy settings.
if (CodeLineList != null) {
// Regenerate the display list with the latest formatter config and
// pseudo-op definition. (These are set as part of the refresh.)
UndoableChange uc =
UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DisplayOnly);
ApplyChanges(new ChangeSet(uc), false);
}
}
private void UnpackRecentProjectList() {
RecentProjectPaths.Clear();
string cereal = AppSettings.Global.GetString(
AppSettings.PRVW_RECENT_PROJECT_LIST, null);
if (string.IsNullOrEmpty(cereal)) {
return;
}
try {
JavaScriptSerializer ser = new JavaScriptSerializer();
RecentProjectPaths = ser.Deserialize<List<string>>(cereal);
} catch (Exception ex) {
Debug.WriteLine("Failed deserializing recent projects: " + ex.Message);
return;
}
}
/// <summary>
/// Ensures that the named project is at the top of the list. If it's elsewhere
/// in the list, move it to the top. Excess items are removed.
/// </summary>
/// <param name="projectPath"></param>
private void UpdateRecentProjectList(string projectPath) {
if (string.IsNullOrEmpty(projectPath)) {
// This can happen if you create a new project, then close the window
// without having saved it.
return;
}
int index = RecentProjectPaths.IndexOf(projectPath);
if (index == 0) {
// Already in the list, nothing changes. No need to update anything else.
return;
}
if (index > 0) {
RecentProjectPaths.RemoveAt(index);
}
RecentProjectPaths.Insert(0, projectPath);
// Trim the list to the max allowed.
while (RecentProjectPaths.Count > MAX_RECENT_PROJECTS) {
Debug.WriteLine("Recent projects: dropping " +
RecentProjectPaths[MAX_RECENT_PROJECTS]);
RecentProjectPaths.RemoveAt(MAX_RECENT_PROJECTS);
}
// Store updated list in app settings. JSON-in-JSON is ugly and inefficient,
// but it'll do for now.
JavaScriptSerializer ser = new JavaScriptSerializer();
string cereal = ser.Serialize(RecentProjectPaths);
AppSettings.Global.SetString(AppSettings.PRVW_RECENT_PROJECT_LIST, cereal);
mMainWin.UpdateRecentLinks();
}
#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));
proj.LongComments.Add(LineListGen.Line.HEADER_COMMENT_OFFSET,
new MultiLineComment("6502bench SourceGen v" + App.ProgramVersion));
// The system definition provides a set of defaults that can be overridden.
// We pull everything of interest out and then discard the object.
proj.ApplySystemDef(sysDef);
mProject = proj;
return true;
}
private void FinishPrep() {
string messages = mProject.LoadExternalFiles();
if (messages.Length != 0) {
// ProjectLoadIssues isn't quite the right dialog, but it'll do.
ProjectLoadIssues dlg = new ProjectLoadIssues(mMainWin, messages,
ProjectLoadIssues.Buttons.Continue);
dlg.ShowDialog();
}
CodeLineList = new LineListGen(mProject, mMainWin.CodeDisplayList,
mOutputFormatter, mPseudoOpNames);
RefreshProject(UndoableChange.ReanalysisScope.CodeAndData);
// Populate the Symbols list.
PopulateSymbolsList();
// Load initial contents of Notes panel.
PopulateNotesList();
mMainWin.ShowCodeListView = true;
mNavStack.Clear();
UpdateRecentProjectList(mProjectPathName);
}
/// <summary>
/// Loads the data file, reading it entirely into memory.
///
/// All errors are reported as exceptions.
/// </summary>
/// <param name="dataFileName">Full pathname.</param>
/// <returns>Data file contents.</returns>
private byte[] LoadDataFile(string dataFileName) {
byte[] fileData;
using (FileStream fs = File.Open(dataFileName, FileMode.Open, FileAccess.Read)) {
// Check length; should have been caught earlier.
if (fs.Length > DisasmProject.MAX_DATA_FILE_SIZE) {
throw new InvalidDataException(
string.Format(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);
#if false
UpdateMenuItemsAndTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
#endif
}
/// <summary>
/// Applies the changes to the project, and updates the display.
///
/// This is called by the undo/redo commands. Don't call this directly from the
/// various UI-driven functions, as this does not add the change to the undo stack.
/// </summary>
/// <param name="cs">Set of changes to apply.</param>
/// <param name="backward">If set, undo the changes instead.</param>
private void ApplyChanges(ChangeSet cs, bool backward) {
mReanalysisTimer.Clear();
mReanalysisTimer.StartTask("ProjectView.ApplyChanges()");
mReanalysisTimer.StartTask("Save selection");
int topItemIndex = mMainWin.CodeListView_GetTopIndex();
LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate(
CodeLineList, mMainWin.CodeDisplayList.SelectedIndices,
CodeLineList[topItemIndex].FileOffset);
//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 list as well.
PopulateNotesList();
mReanalysisTimer.EndTask("ProjectView.ApplyChanges()");
//mReanalysisTimer.DumpTimes("ProjectView timers:", mGenerationLog);
#if false
if (mShowAnalysisTimersDialog != null) {
string timerStr = mReanalysisTimer.DumpToString("ProjectView timers:");
mShowAnalysisTimersDialog.BodyText = timerStr;
}
#endif
// 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.
#if false
UpdateSelectionHighlight();
#endif
}
/// <summary>
/// Refreshes the project after something of substance has changed. Some
/// re-analysis will be done, followed by a complete rebuild of the DisplayList.
/// </summary>
/// <param name="reanalysisRequired">Indicates whether reanalysis is required, and
/// what level.</param>
private void RefreshProject(UndoableChange.ReanalysisScope reanalysisRequired) {
Debug.Assert(reanalysisRequired != UndoableChange.ReanalysisScope.None);
// NOTE: my goal is to arrange things so that reanalysis (data-only, and ideally
// code+data) takes less than 100ms. With that response time there's no need for
// background processing and progress bars. Since we need to do data-only
// reanalysis after many common operations, the program becomes unpleasant to
// use if we miss this goal, and progress bars won't make it less so.
// Changing the CPU type or whether undocumented instructions are supported
// invalidates the Formatter's mnemonic cache. We can change these values
// through undo/redo, so we need to check it here.
if (mOutputFormatterCpuDef != mProject.CpuDef) { // reference equality is fine
Debug.WriteLine("CpuDef has changed, resetting formatter (now " +
mProject.CpuDef + ")");
mOutputFormatter = new Formatter(mFormatterConfig);
CodeLineList.SetFormatter(mOutputFormatter);
CodeLineList.SetPseudoOpNames(mPseudoOpNames);
mOutputFormatterCpuDef = mProject.CpuDef;
}
#if false
if (mDisplayList.Count > 200000) {
string prevStatus = toolStripStatusLabel.Text;
// The Windows stuff can take 50-100ms, potentially longer than the actual
// work, so don't bother unless the file is very large.
try {
mReanalysisTimer.StartTask("Do Windows stuff");
Application.UseWaitCursor = true;
Cursor.Current = Cursors.WaitCursor;
toolStripStatusLabel.Text = Res.Strings.STATUS_RECALCULATING;
Refresh(); // redraw status label
mReanalysisTimer.EndTask("Do Windows stuff");
DoRefreshProject(reanalysisRequired);
} finally {
Application.UseWaitCursor = false;
toolStripStatusLabel.Text = prevStatus;
}
} else {
#endif
DoRefreshProject(reanalysisRequired);
#if false
}
#endif
if (FormatDescriptor.DebugCreateCount != 0) {
Debug.WriteLine("FormatDescriptor total=" + FormatDescriptor.DebugCreateCount +
" prefab=" + FormatDescriptor.DebugPrefabCount + " (" +
(FormatDescriptor.DebugPrefabCount * 100) / FormatDescriptor.DebugCreateCount +
"%)");
}
}
/// <summary>
/// Updates all of the specified ListView entries. This is called after minor changes,
/// such as editing a comment or renaming a label, that can be handled by regenerating
/// selected parts of the DisplayList.
/// </summary>
/// <param name="offsetSet"></param>
private void RefreshCodeListViewEntries(RangeSet offsetSet) {
IEnumerator<RangeSet.Range> iter = offsetSet.RangeListIterator;
while (iter.MoveNext()) {
RangeSet.Range range = iter.Current;
CodeLineList.GenerateRange(range.Low, range.High);
}
}
private void DoRefreshProject(UndoableChange.ReanalysisScope reanalysisRequired) {
if (reanalysisRequired != UndoableChange.ReanalysisScope.DisplayOnly) {
mGenerationLog = new CommonUtil.DebugLog();
mGenerationLog.SetMinPriority(CommonUtil.DebugLog.Priority.Debug);
mGenerationLog.SetShowRelTime(true);
mReanalysisTimer.StartTask("Call DisasmProject.Analyze()");
mProject.Analyze(reanalysisRequired, mGenerationLog, mReanalysisTimer);
mReanalysisTimer.EndTask("Call DisasmProject.Analyze()");
}
if (mGenerationLog != null) {
//mReanalysisTimer.StartTask("Save _log");
//mGenerationLog.WriteToFile(@"C:\Src\WorkBench\SourceGen\TestData\_log.txt");
//mReanalysisTimer.EndTask("Save _log");
#if false
if (mShowAnalyzerOutputDialog != null) {
mShowAnalyzerOutputDialog.BodyText = mGenerationLog.WriteToString();
}
#endif
}
mReanalysisTimer.StartTask("Generate DisplayList");
CodeLineList.GenerateAll();
mReanalysisTimer.EndTask("Generate DisplayList");
}
#endregion Project management
#region Main window UI event handlers
public void NewProject() {
if (!CloseProject()) {
return;
}
string sysDefsPath = RuntimeDataAccess.GetPathName("SystemDefs.json");
if (sysDefsPath == null) {
MessageBox.Show(Res.Strings.ERR_LOAD_CONFIG_FILE, Res.Strings.OPERATION_FAILED,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
SystemDefSet sds = null;
try {
sds = SystemDefSet.ReadFile(sysDefsPath);
} catch (Exception ex) {
Debug.WriteLine("Failed loading system def set: " + ex);
MessageBox.Show(Res.Strings.ERR_LOAD_CONFIG_FILE, Res.Strings.OPERATION_FAILED,
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
NewProject dlg = new NewProject(mMainWin, sds);
if (dlg.ShowDialog() != true) {
return;
}
bool ok = PrepareNewProject(Path.GetFullPath(dlg.DataFileName), dlg.SystemDef);
if (ok) {
FinishPrep();
}
}
public void OpenRecentProject(int projIndex) {
if (!CloseProject()) {
return;
}
DoOpenFile(RecentProjectPaths[projIndex]);
}
/// <summary>
/// Handles opening an existing project by letting the user select the project file.
/// </summary>
public void OpenProject() {
if (!CloseProject()) {
return;
}
OpenFileDialog fileDlg = new OpenFileDialog() {
Filter = ProjectFile.FILENAME_FILTER + "|" + Res.Strings.FILE_FILTER_ALL,
FilterIndex = 1
};
if (fileDlg.ShowDialog() != true) {
return;
}
string projPathName = Path.GetFullPath(fileDlg.FileName);
DoOpenFile(projPathName);
}
/// <summary>
/// Handles opening an existing project, given a pathname to the project file.
/// </summary>
private void DoOpenFile(string projPathName) {
Debug.WriteLine("DoOpenFile: " + projPathName);
Debug.Assert(mProject == null);
if (!File.Exists(projPathName)) {
string msg = string.Format(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;
}
}
// If there were warnings, notify the user and give the a chance to cancel.
if (report.Count != 0) {
ProjectLoadIssues dlg = new ProjectLoadIssues(mMainWin, report.Format(),
ProjectLoadIssues.Buttons.ContinueOrCancel);
bool? ok = dlg.ShowDialog();
if (ok != true) {
return;
}
}
mProject = newProject;
mProjectPathName = mProject.ProjectPathName = projPathName;
mProject.SetFileData(fileData, Path.GetFileName(dataPathName));
FinishPrep();
}
/// <summary>
/// Finds and loads the specified data file. The file's length and CRC must match
/// the project's expectations.
/// </summary>
/// <param name="dataPathName">Full path to file.</param>
/// <param name="proj">Project object.</param>
/// <param name="cancel">Returns true if we want to cancel the attempt.</param>
/// <returns></returns>
private byte[] FindValidDataFile(ref string dataPathName, DisasmProject proj,
out bool cancel) {
FileInfo fi = new FileInfo(dataPathName);
if (!fi.Exists) {
Debug.WriteLine("File '" + dataPathName + "' doesn't exist");
dataPathName = ChooseDataFile(dataPathName,
Res.Strings.OPEN_DATA_DOESNT_EXIST);
cancel = (dataPathName == null);
return null;
}
if (fi.Length != proj.FileDataLength) {
Debug.WriteLine("File '" + dataPathName + "' has length=" + fi.Length +
", expected " + proj.FileDataLength);
dataPathName = ChooseDataFile(dataPathName,
string.Format(Res.Strings.OPEN_DATA_WRONG_LENGTH_FMT,
fi.Length, proj.FileDataLength));
cancel = (dataPathName == null);
return null;
}
byte[] fileData = null;
try {
fileData = LoadDataFile(dataPathName);
} catch (Exception ex) {
Debug.WriteLine("File '" + dataPathName + "' failed to load: " + ex.Message);
dataPathName = ChooseDataFile(dataPathName,
string.Format(Res.Strings.OPEN_DATA_LOAD_FAILED_FMT, ex.Message));
cancel = (dataPathName == null);
return null;
}
uint crc = CRC32.OnWholeBuffer(0, fileData);
if (crc != proj.FileDataCrc32) {
Debug.WriteLine("File '" + dataPathName + "' has CRC32=" + crc +
", expected " + proj.FileDataCrc32);
// Format the CRC as signed decimal, so that interested parties can
// easily replace the value in the .dis65 file.
dataPathName = ChooseDataFile(dataPathName,
string.Format(Res.Strings.OPEN_DATA_WRONG_CRC_FMT,
(int)crc, (int)proj.FileDataCrc32));
cancel = (dataPathName == null);
return null;
}
cancel = false;
return fileData;
}
/// <summary>
/// Displays a "do you want to pick a different file" message, then (on OK) allows the
/// user to select a file.
/// </summary>
/// <param name="origPath">Pathname of original file.</param>
/// <param name="errorMsg">Message to display in the message box.</param>
/// <returns>Full path of file to open.</returns>
private string ChooseDataFile(string origPath, string errorMsg) {
DataFileLoadIssue dlg = new DataFileLoadIssue(mMainWin, origPath, errorMsg);
bool? ok = dlg.ShowDialog();
if (ok != true) {
return null;
}
OpenFileDialog fileDlg = new OpenFileDialog() {
FileName = Path.GetFileName(origPath),
Filter = Res.Strings.FILE_FILTER_ALL
};
if (fileDlg.ShowDialog() != true) {
return null;
}
string newPath = Path.GetFullPath(fileDlg.FileName);
Debug.WriteLine("User selected data file " + newPath);
return newPath;
}
/// <summary>
/// Saves the project, querying for the filename.
/// </summary>
/// <returns>True on success, false if the save attempt failed or was canceled.</returns>
public bool SaveProjectAs() {
SaveFileDialog fileDlg = new SaveFileDialog() {
Filter = ProjectFile.FILENAME_FILTER + "|" + Res.Strings.FILE_FILTER_ALL,
FilterIndex = 1,
ValidateNames = true,
AddExtension = true,
FileName = Path.GetFileName(mDataPathName) + ProjectFile.FILENAME_EXT
};
if (fileDlg.ShowDialog() != true) {
Debug.WriteLine("SaveAs canceled by user");
return false;
}
string pathName = Path.GetFullPath(fileDlg.FileName);
Debug.WriteLine("Project save path: " + pathName);
if (!DoSave(pathName)) {
return false;
}
// Success, record the path name.
mProjectPathName = mProject.ProjectPathName = pathName;
// add it to the title bar
#if false
UpdateMenuItemsAndTitle();
#endif
return true;
}
/// <summary>
/// Saves the project. If it hasn't been saved before, use save-as behavior instead.
/// </summary>
/// <returns>True on success, false if the save attempt failed.</returns>
public bool SaveProject() {
if (string.IsNullOrEmpty(mProjectPathName)) {
return SaveProjectAs();
}
return DoSave(mProjectPathName);
}
private bool DoSave(string pathName) {
Debug.WriteLine("SAVING " + pathName);
if (!ProjectFile.SerializeToFile(mProject, pathName, out string errorMessage)) {
MessageBox.Show(Res.Strings.ERR_PROJECT_SAVE_FAIL + ": " + errorMessage,
Res.Strings.OPERATION_FAILED,
MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
mProject.ResetDirtyFlag();
#if false
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
UpdateMenuItemsAndTitle();
#endif
// 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();
return CloseProject();
}
/// <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;
}
}
}
#if false
// Close modeless dialogs that depend on project.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.Close();
}
if (mShowAnalysisTimersDialog != null) {
mShowAnalysisTimersDialog.Close();
}
if (mShowAnalyzerOutputDialog != null) {
mShowAnalyzerOutputDialog.Close();
}
if (mHexDumpDialog != null) {
mHexDumpDialog.Close();
}
#endif
// Discard all project state.
if (mProject != null) {
mProject.Cleanup();
mProject = null;
}
mDataPathName = null;
mProjectPathName = null;
mTargetHighlightIndex = -1;
// Clear this to release the memory.
mMainWin.CodeDisplayList.Clear();
mMainWin.InfoPanelContents = string.Empty;
mMainWin.ShowCodeListView = false;
mGenerationLog = null;
// Not necessary, but it lets us check the memory monitor to see if we got
// rid of everything.
GC.Collect();
return true;
}
public bool IsProjectOpen() {
return mProject != null;
}
public void AssembleProject() {
if (string.IsNullOrEmpty(mProjectPathName)) {
// We need a project pathname so we know where to write the assembler
// source files, and what to call the output files. We could just pop up the
// Save As dialog, but that seems confusing unless we do a custom dialog with
// an explanation, or have some annoying click-through.
//
// This only appears for never-saved projects, not projects with unsaved data.
MessageBox.Show(Res.Strings.SAVE_BEFORE_ASM, Res.Strings.SAVE_BEFORE_ASM_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
AsmGen.WpfGui.GenAndAsm dlg =
new AsmGen.WpfGui.GenAndAsm(mMainWin, mProject, mProjectPathName);
dlg.ShowDialog();
}
/// <summary>
/// Opens the application settings dialog. All changes to settings are made directly
/// to the AppSettings.Global object.
/// </summary>
public void EditAppSettings() {
EditAppSettings dlg = new EditAppSettings(mMainWin, mMainWin, this,
WpfGui.EditAppSettings.Tab.Unknown, AsmGen.AssemblerInfo.Id.Unknown);
dlg.ShowDialog();
}
public void HandleCodeListDoubleClick(int row, int col) {
Debug.WriteLine("DCLICK: row=" + row + " col=" + col);
// 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 false
if (editProjectSymbolToolStripMenuItem.Enabled) {
EditProjectSymbol_Click(sender, e);
}
#endif
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 false
if (editNoteToolStripMenuItem.Enabled) {
EditNote_Click(sender, e);
}
#endif
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:
#if false
if (showHexDumpToolStripMenuItem.Enabled) {
ShowHexDump_Click(sender, e);
}
#endif
break;
case CodeListColumn.Flags:
if (CanEditStatusFlags()) {
EditStatusFlags();
}
break;
case CodeListColumn.Attributes:
// does nothing
break;
case CodeListColumn.Label:
if (CanEditLabel()) {
EditLabel();
}
break;
case CodeListColumn.Opcode:
// File offset should always be valid, since we excluded the EQU
// statements and header comment earlier.
if (line.FileOffset >= 0) {
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
FormatDescriptor dfd = attr.DataDescriptor;
// Does this have an operand with an in-file target offset?
// (Resolve it as a numeric reference.)
if (attr.OperandOffset >= 0) {
// Yup, find the line for that offset and jump to it.
GoToOffset(attr.OperandOffset, false, true);
} else if (dfd != null && dfd.HasSymbol) {
// Operand has a symbol, do a symbol lookup.
int labelOffset = mProject.FindLabelOffsetByName(
dfd.SymbolRef.Label);
if (labelOffset >= 0) {
GoToOffset(labelOffset, false, true);
}
} else if (attr.IsDataStart || attr.IsInlineDataStart) {
// If it's an Address or Symbol, we can try to resolve
// the value. (Symbols should have been resolved by the
// previous clause, but Address entries would not have been.)
int operandOffset = DataAnalysis.GetDataOperandOffset(
mProject, line.FileOffset);
if (operandOffset >= 0) {
GoToOffset(operandOffset, false, true);
}
}
}
break;
case CodeListColumn.Operand:
#if false
if (editOperandToolStripMenuItem.Enabled) {
EditInstrDataOperand_Click(sender, e);
}
#endif
break;
case CodeListColumn.Comment:
#if false
if (editCommentToolStripMenuItem.Enabled) {
EditComment_Click(sender, e);
}
#endif
break;
}
break;
default:
Debug.WriteLine("Double-click: unhandled line type " + line.LineType);
break;
}
}
public bool CanEditAddress() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Line must be code, data, or an ORG directive.
return (counts.mDataLines > 0 || counts.mCodeLines > 0) ||
(SelectionAnalysis.mLineType == LineListGen.Line.Type.OrgDirective);
}
public void EditAddress() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
Anattrib attr = mProject.GetAnattrib(offset);
EditAddress dlg = new EditAddress(mMainWin, attr.Address, mProject.CpuDef.MaxAddressValue);
if (dlg.ShowDialog() != true) {
return;
}
if (offset == 0 && dlg.Address < 0) {
// Not allowed. The AddressMap will just put it back, which confuses
// the undo operation.
Debug.WriteLine("EditAddress: not allowed to remove address at offset +000000");
} else if (attr.Address != dlg.Address) {
Debug.WriteLine("EditAddress: changing addr at offset +" + offset.ToString("x6") +
" to " + dlg.Address);
AddressMap addrMap = mProject.AddrMap;
// Get the previous address map entry for this exact offset, if one
// exists. This may be different from the value used as the default
// (attr.Address), which is the address assigned to the offset, in
// the case where no previous mapping existed.
int prevAddress = addrMap.Get(offset);
UndoableChange uc = UndoableChange.CreateAddressChange(offset,
prevAddress, dlg.Address);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
} else {
Debug.WriteLine("EditAddress: no change");
}
}
public void EditHeaderComment() {
EditLongComment(LineListGen.Line.HEADER_COMMENT_OFFSET);
}
public bool CanEditLabel() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, code or data.
return (counts.mDataLines > 0 || counts.mCodeLines > 0);
}
public void EditLabel() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
Anattrib attr = mProject.GetAnattrib(offset);
EditLabel dlg = new EditLabel(mMainWin, attr.Symbol, attr.Address,
mProject.SymbolTable);
if (dlg.ShowDialog() != true) {
return;
}
// NOTE: if label matching is case-insensitive, we want to allow a situation
// where a label is being renamed from "FOO" to "Foo". (We should be able to
// test for object equality on the Symbol.)
if (attr.Symbol != dlg.LabelSym) {
Debug.WriteLine("Changing label at offset +" + offset.ToString("x6"));
// For undo/redo, we want to update the UserLabels value. This may
// be different from the Anattrib symbol, which can have an auto-generated
// value.
Symbol oldUserValue = null;
if (mProject.UserLabels.ContainsKey(offset)) {
oldUserValue = mProject.UserLabels[offset];
}
UndoableChange uc = UndoableChange.CreateLabelChange(offset,
oldUserValue, dlg.LabelSym);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
public bool CanEditLongComment() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, code or data, or a long comment.
return (counts.mDataLines > 0 || counts.mCodeLines > 0 ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.LongComment);
}
public void EditLongComment() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
EditLongComment(offset);
}
private void EditLongComment(int offset) {
EditLongComment dlg = new EditLongComment(mMainWin, mOutputFormatter);
if (mProject.LongComments.TryGetValue(offset, out MultiLineComment oldComment)) {
dlg.LongComment = oldComment;
}
dlg.ShowDialog();
if (dlg.DialogResult == true) {
MultiLineComment newComment = dlg.LongComment;
if (oldComment != newComment) {
Debug.WriteLine("Changing long comment at +" + offset.ToString("x6"));
UndoableChange uc = UndoableChange.CreateLongCommentChange(offset,
oldComment, newComment);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
}
public bool CanEditStatusFlags() {
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
EntityCounts counts = SelectionAnalysis.mEntityCounts;
// Single line, must be code or a RegWidth directive.
return (SelectionAnalysis.mLineType == LineListGen.Line.Type.Code ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.RegWidthDirective);
}
public void EditStatusFlags() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
EditStatusFlags dlg = new EditStatusFlags(mMainWin,
mProject.StatusFlagOverrides[offset], mProject.CpuDef.HasEmuFlag);
if (dlg.ShowDialog() != true) {
return;
}
if (dlg.FlagValue != mProject.StatusFlagOverrides[offset]) {
UndoableChange uc = UndoableChange.CreateStatusFlagChange(offset,
mProject.StatusFlagOverrides[offset], dlg.FlagValue);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
public void EditProjectProperties() {
string projectDir = string.Empty;
if (!string.IsNullOrEmpty(mProjectPathName)) {
projectDir = Path.GetDirectoryName(mProjectPathName);
}
EditProjectProperties dlg = new EditProjectProperties(mMainWin, mProject.ProjectProps,
projectDir, mOutputFormatter);
dlg.ShowDialog();
ProjectProperties newProps = dlg.NewProps;
// The dialog result doesn't matter, because the user might have hit "apply"
// before hitting "cancel".
if (newProps != null) {
UndoableChange uc = UndoableChange.CreateProjectPropertiesChange(
mProject.ProjectProps, newProps);
ApplyUndoableChanges(new ChangeSet(uc));
}
}
public void Find() {
FindBox dlg = new FindBox(mMainWin, mFindString);
if (dlg.ShowDialog() == true) {
mFindString = dlg.TextToFind;
mFindStartIndex = -1;
FindText();
}
}
public void FindNext() {
FindText();
}
private void FindText() {
if (string.IsNullOrEmpty(mFindString)) {
return;
}
// Start from the topmost selected line, or the start of the file if nothing
// is selected.
int index = mMainWin.CodeListView_GetFirstSelectedIndex();
if (index < 0) {
index = 0;
}
// Start one past the selected item.
index++;
if (index == CodeLineList.Count) {
index = 0;
}
//Debug.WriteLine("FindText index=" + index + " start=" + mFindStartIndex +
// " str=" + mFindString);
while (index != mFindStartIndex) {
if (mFindStartIndex < 0) {
// need to latch this inside the loop so the initial test doesn't fail
mFindStartIndex = index;
}
string searchStr = CodeLineList.GetSearchString(index);
int matchPos = searchStr.IndexOf(mFindString,
StringComparison.InvariantCultureIgnoreCase);
if (matchPos >= 0) {
//Debug.WriteLine("Match " + index + ": " + searchStr);
mMainWin.CodeListView_EnsureVisible(index);
mMainWin.CodeListView_DeselectAll();
mMainWin.CodeListView_SelectRange(index, 1);
return;
}
index++;
if (index == CodeLineList.Count) {
index = 0;
}
}
// Announce that we've wrapped around, then clear the start index.
MessageBox.Show(Res.Strings.FIND_REACHED_START,
Res.Strings.FIND_REACHED_START_CAPTION, MessageBoxButton.OK,
MessageBoxImage.Information);
mFindStartIndex = -1;
mMainWin.CodeListView_Focus();
}
public void Goto() {
GotoBox dlg = new GotoBox(mMainWin, mProject, mOutputFormatter);
if (dlg.ShowDialog() == true) {
GoToOffset(dlg.TargetOffset, false, true);
mMainWin.CodeListView_Focus();
}
}
/// <summary>
/// Moves the view and selection to the specified offset. We want to select stuff
/// differently if we're jumping to a note vs. jumping to an instruction.
/// </summary>
/// <param name="gotoOffset">Offset to jump to.</param>
/// <param name="doPush">If set, push new offset onto navigation stack.</param>
public void GoToOffset(int gotoOffset, bool jumpToNote, bool doPush) {
NavStack.Location prevLoc = GetCurrentlySelectedLocation();
if (gotoOffset == prevLoc.Offset && jumpToNote == prevLoc.IsNote) {
// we're jumping to ourselves?
Debug.WriteLine("Ignoring goto to current position");
return;
}
int topLineIndex = CodeLineList.FindLineIndexByOffset(gotoOffset);
if (topLineIndex < 0) {
Debug.Assert(false, "failed goto offset +" + gotoOffset.ToString("x6"));
return;
}
int lastLineIndex;
if (jumpToNote) {
// Select all note lines, disregard the rest.
while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Note) {
topLineIndex++;
Debug.Assert(CodeLineList[topLineIndex].FileOffset == gotoOffset);
}
lastLineIndex = topLineIndex + 1;
while (lastLineIndex < CodeLineList.Count &&
CodeLineList[lastLineIndex].LineType == LineListGen.Line.Type.Note) {
lastLineIndex++;
}
} else if (gotoOffset < 0) {
// This is the offset of the header comment or a .EQ directive. Don't mess with it.
lastLineIndex = topLineIndex + 1;
} else {
// Advance to the code or data line.
while (CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Code &&
CodeLineList[topLineIndex].LineType != LineListGen.Line.Type.Data) {
topLineIndex++;
}
lastLineIndex = topLineIndex + 1;
}
// Make sure the item is visible. For notes, this can span multiple lines.
mMainWin.CodeListView_EnsureVisible(lastLineIndex - 1);
mMainWin.CodeListView_EnsureVisible(topLineIndex);
// Update the selection.
mMainWin.CodeListView_DeselectAll();
mMainWin.CodeListView_SelectRange(topLineIndex, lastLineIndex - topLineIndex);
if (doPush) {
// Update the back stack and associated controls.
mNavStack.Push(prevLoc);
}
}
private NavStack.Location GetCurrentlySelectedLocation() {
int index = mMainWin.CodeListView_GetFirstSelectedIndex();
if (index < 0) {
// nothing selected, use top instead
index = mMainWin.CodeListView_GetTopIndex();
}
int offset = CodeLineList[index].FileOffset;
bool isNote = (CodeLineList[index].LineType == LineListGen.Line.Type.Note);
return new NavStack.Location(offset, isNote);
}
public bool CanNavigateBackward() {
return mNavStack.HasBackward;
}
public void NavigateBackward() {
Debug.Assert(mNavStack.HasBackward);
NavStack.Location backLoc = mNavStack.MoveBackward(GetCurrentlySelectedLocation());
GoToOffset(backLoc.Offset, backLoc.IsNote, false);
}
public bool CanNavigateForward() {
return mNavStack.HasForward;
}
public void NavigateForward() {
Debug.Assert(mNavStack.HasForward);
NavStack.Location fwdLoc = mNavStack.MoveForward(GetCurrentlySelectedLocation());
GoToOffset(fwdLoc.Offset, fwdLoc.IsNote, false);
}
/// <summary>
/// Scrolls the code list so that the specified label is shown.
/// </summary>
/// <param name="sym">Label symbol.</param>
public void GoToLabel(Symbol sym) {
if (sym.IsInternalLabel) {
int offset = mProject.FindLabelOffsetByName(sym.Label);
if (offset >= 0) {
GoToOffset(offset, false, true);
} else {
Debug.WriteLine("DClick symbol: " + sym + ": label not found");
}
} else {
Debug.WriteLine("DClick symbol: " + sym + ": not label");
}
}
public void SelectionChanged() {
SelectionAnalysis = UpdateSelectionState();
UpdateReferencesPanel();
UpdateInfoPanel();
UpdateSelectionHighlight();
}
/// <summary>
/// Gathered facts about the current selection. Recalculated whenever the selection
/// changes.
/// </summary>
public class SelectionState {
// Number of selected items or lines, reduced. This will be:
// 0 if no lines are selected
// 1 if a single *item* is selected (regardless of number of lines)
// >1 if more than one item is selected (exact value not specified)
public int mNumItemsSelected;
// Single selection: the type of line selected. (Multi-sel: Unclassified)
public LineListGen.Line.Type mLineType;
// Single selection: is line an instruction with an operand. (Multi-sel: False)
public bool mIsInstructionWithOperand;
// Single selection: is line an EQU directive for a project symbol. (Multi-sel: False)
public bool mIsProjectSymbolEqu;
// Some totals.
public EntityCounts mEntityCounts;
public SelectionState() {
mLineType = LineListGen.Line.Type.Unclassified;
mEntityCounts = new EntityCounts();
}
}
/// <summary>
/// Updates Actions menu enable states when the selection changes.
/// </summary>
/// is selected.</param>
public SelectionState UpdateSelectionState() {
int selCount = mMainWin.CodeListView_GetSelectionCount();
Debug.WriteLine("SelectionChanged, 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 (int offset = line.FileOffset; offset < line.FileOffset + line.OffsetSpan;
offset++) {
switch (mProject.TypeHints[offset]) {
case CodeAnalysis.TypeHint.Code:
codeHints++;
break;
case CodeAnalysis.TypeHint.Data:
dataHints++;
break;
case CodeAnalysis.TypeHint.InlineData:
inlineDataHints++;
break;
case CodeAnalysis.TypeHint.NoHint:
noHints++;
break;
default:
Debug.Assert(false);
break;
}
}
}
//Debug.WriteLine("GatherEntityCounts (len=" + CodeListGen.Count + ") took " +
// (DateTime.Now - startWhen).TotalMilliseconds + " ms");
return new EntityCounts() {
mCodeLines = codeLines,
mDataLines = dataLines,
mBlankLines = blankLines,
mControlLines = controlLines,
mCodeHints = codeHints,
mDataHints = dataHints,
mInlineDataHints = inlineDataHints,
mNoHints = noHints
};
}
/// <summary>
/// Determines whether the current selection spans a single item. This could be a
/// single-line item or a multi-line item.
/// </summary>
private bool IsSingleItemSelected() {
int firstIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
if (firstIndex < 0) {
// empty selection
return false;
}
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
if (lastIndex == firstIndex) {
// only one line is selected
return true;
}
// Just check the first and last entries to see if they're the same.
LineListGen.Line firstItem = CodeLineList[firstIndex];
LineListGen.Line lastItem = CodeLineList[lastIndex];
if (firstItem.FileOffset == lastItem.FileOffset &&
firstItem.LineType == lastItem.LineType) {
return true;
}
return false;
}
/// <summary>
/// Updates the selection highlight. When a code item with an operand offset is
/// selected, such as a branch, we want to highlight the address and label of the
/// target.
/// </summary>
private void UpdateSelectionHighlight() {
int targetIndex = FindSelectionHighlight();
if (mTargetHighlightIndex != targetIndex) {
mMainWin.CodeListView_RemoveSelectionHighlight(mTargetHighlightIndex);
mMainWin.CodeListView_AddSelectionHighlight(targetIndex);
mTargetHighlightIndex = targetIndex;
Debug.WriteLine("Selection highlight now " + targetIndex);
}
}
private int FindSelectionHighlight() {
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
return -1;
}
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line line = CodeLineList[selIndex];
if (!line.IsCodeOrData) {
return -1;
}
Debug.Assert(line.FileOffset >= 0);
// Does this have an operand with an in-file target offset?
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
if (attr.OperandOffset >= 0) {
return CodeLineList.FindCodeDataIndexByOffset(attr.OperandOffset);
} else if (attr.IsDataStart || attr.IsInlineDataStart) {
// If it's an Address or Symbol, we can try to resolve
// the value.
int operandOffset = DataAnalysis.GetDataOperandOffset(mProject, line.FileOffset);
if (operandOffset >= 0) {
return CodeLineList.FindCodeDataIndexByOffset(operandOffset);
}
}
return -1;
}
public 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);
#if false
UpdateMenuItemsAndTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
#endif
}
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);
#if false
UpdateMenuItemsAndTitle();
// If the debug dialog is visible, update it.
if (mShowUndoRedoHistoryDialog != null) {
mShowUndoRedoHistoryDialog.BodyText = mProject.DebugGetUndoRedoHistory();
}
#endif
}
/// <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();
}
#endregion Main window UI event handlers
#region References panel
/// <summary>
/// Updates the "references" panel to reflect the current selection.
///
/// The number of references to any given address should be relatively small, and
/// won't change without a data refresh, so recreating the list every time shouldn't
/// be a problem.
/// </summary>
private void UpdateReferencesPanel() {
mMainWin.ReferencesList.Clear();
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
// Nothing selected, or multiple lines selected.
return;
}
int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line.Type type = CodeLineList[lineIndex].LineType;
if (type != LineListGen.Line.Type.Code &&
type != LineListGen.Line.Type.Data &&
type != LineListGen.Line.Type.EquDirective) {
// Code, data, and platform symbol EQUs have xrefs.
return;
}
// Find the appropriate xref set.
int offset = CodeLineList[lineIndex].FileOffset;
XrefSet xrefs;
if (offset < 0) {
int index = LineListGen.DefSymIndexFromOffset(offset);
DefSymbol defSym = mProject.ActiveDefSymbolList[index];
xrefs = defSym.Xrefs;
} else {
xrefs = mProject.GetXrefSet(offset);
}
if (xrefs == null || xrefs.Count == 0) {
return;
}
// TODO(someday): localization
Asm65.Formatter formatter = mOutputFormatter;
bool showBank = !mProject.CpuDef.HasAddr16;
for (int i = 0; i < xrefs.Count; i++) {
XrefSet.Xref xr = xrefs[i];
string typeStr;
switch (xr.Type) {
case XrefSet.XrefType.SubCallOp:
typeStr = "call ";
break;
case XrefSet.XrefType.BranchOp:
typeStr = "branch ";
break;
case XrefSet.XrefType.RefFromData:
typeStr = "data ";
break;
case XrefSet.XrefType.MemAccessOp:
switch (xr.AccType) {
case OpDef.MemoryEffect.Read:
typeStr = "read ";
break;
case OpDef.MemoryEffect.Write:
typeStr = "write ";
break;
case OpDef.MemoryEffect.ReadModifyWrite:
typeStr = "rmw ";
break;
case OpDef.MemoryEffect.None: // e.g. LDA #<symbol, PEA addr
typeStr = "ref ";
break;
case OpDef.MemoryEffect.Unknown:
default:
Debug.Assert(false);
typeStr = "??! ";
break;
}
break;
default:
Debug.Assert(false);
typeStr = "??? ";
break;
}
MainWindow.ReferencesListItem rli = new MainWindow.ReferencesListItem(xr.Offset,
formatter.FormatOffset24(xr.Offset),
formatter.FormatAddress(mProject.GetAnattrib(xr.Offset).Address, showBank),
(xr.IsSymbolic ? "Sym " : "Num ") + typeStr +
formatter.FormatAdjustment(-xr.Adjustment));
mMainWin.ReferencesList.Add(rli);
}
}
#endregion References panel
#region Notes panel
private void PopulateNotesList() {
mMainWin.NotesList.Clear();
foreach (KeyValuePair<int, MultiLineComment> kvp in mProject.Notes) {
int offset = kvp.Key;
MultiLineComment mlc = kvp.Value;
// Replace line break with bullet. If there's a single CRLF at the end, strip it.
string nocrlfStr;
if (mlc.Text.EndsWith("\r\n")) {
nocrlfStr =
mlc.Text.Substring(0, mlc.Text.Length - 2).Replace("\r\n", " \u2022 ");
} else {
nocrlfStr = mlc.Text.Replace("\r\n", " \u2022 ");
}
MainWindow.NotesListItem nli = new MainWindow.NotesListItem(offset,
mOutputFormatter.FormatOffset24(offset),
nocrlfStr,
mlc.BackgroundColor);
mMainWin.NotesList.Add(nli);
}
}
#endregion Notes panel
#region Symbols panel
private void PopulateSymbolsList() {
mMainWin.SymbolsList.Clear();
foreach (Symbol sym in mProject.SymbolTable) {
MainWindow.SymbolsListItem sli = new MainWindow.SymbolsListItem(sym,
sym.SourceTypeString,
mOutputFormatter.FormatHexValue(sym.Value, 0),
sym.Label);
mMainWin.SymbolsList.Add(sli);
}
}
#endregion Symbols panel
#region Info panel
private void UpdateInfoPanel() {
if (mMainWin.CodeListView_GetSelectionCount() != 1) {
// Nothing selected, or multiple lines selected.
mMainWin.InfoPanelContents = string.Empty;
return;
}
int lineIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
LineListGen.Line line = CodeLineList[lineIndex];
StringBuilder sb = new StringBuilder(250);
// TODO(someday): this should be made easier to localize
string lineTypeStr;
string extraStr = string.Empty;
switch (line.LineType) {
case LineListGen.Line.Type.Code:
lineTypeStr = "code";
break;
case LineListGen.Line.Type.Data:
if (mProject.GetAnattrib(line.FileOffset).IsInlineData) {
lineTypeStr = "inline data";
} else {
lineTypeStr = "data";
}
break;
case LineListGen.Line.Type.LongComment:
lineTypeStr = "comment";
break;
case LineListGen.Line.Type.Note:
lineTypeStr = "note";
break;
case LineListGen.Line.Type.Blank:
lineTypeStr = "blank line";
//lineTypeStr = "blank line (+" +
// mOutputFormatter.FormatOffset24(line.FileOffset) + ")";
break;
case LineListGen.Line.Type.OrgDirective:
lineTypeStr = "address directive";
break;
case LineListGen.Line.Type.RegWidthDirective:
lineTypeStr = "register width directive";
break;
case LineListGen.Line.Type.EquDirective: {
lineTypeStr = "equate";
int symIndex = LineListGen.DefSymIndexFromOffset(line.FileOffset);
DefSymbol defSym = mProject.ActiveDefSymbolList[symIndex];
string sourceStr;
if (defSym.SymbolSource == Symbol.Source.Project) {
sourceStr = "project symbol definition";
} else if (defSym.SymbolSource == Symbol.Source.Platform) {
sourceStr = "platform symbol file";
} else {
sourceStr = "???";
}
extraStr = "Source: " + sourceStr;
}
break;
default:
lineTypeStr = "???";
break;
}
// For anything that isn't code or data, show something simple and bail.
if (line.OffsetSpan == 0) {
sb.AppendFormat(Res.Strings.INFO_LINE_SUM_NON_FMT,
lineIndex, lineTypeStr);
#if DEBUG
sb.Append(" [offset=+" + line.FileOffset.ToString("x6") + "]");
#endif
if (!string.IsNullOrEmpty(extraStr)) {
sb.Append("\r\n\r\n");
sb.Append(extraStr);
}
mMainWin.InfoPanelContents = sb.ToString();
return;
}
Debug.Assert(line.IsCodeOrData);
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
// Show number of bytes of code/data.
if (line.OffsetSpan == 1) {
sb.AppendFormat(Res.Strings.INFO_LINE_SUM_SINGULAR_FMT,
lineIndex, line.OffsetSpan, lineTypeStr);
} else {
sb.AppendFormat(Res.Strings.INFO_LINE_SUM_PLURAL_FMT,
lineIndex, line.OffsetSpan, lineTypeStr);
}
sb.Append("\r\n");
if (!mProject.OperandFormats.TryGetValue(line.FileOffset, out FormatDescriptor dfd)) {
// No user-specified format, but there may be a generated format.
sb.AppendFormat(Res.Strings.INFO_FD_SUM_FMT, Res.Strings.DEFAULT_VALUE);
if (attr.DataDescriptor != null) {
sb.Append(" [");
sb.Append(attr.DataDescriptor.ToUiString());
sb.Append("]");
}
} else {
// User-specified operand format.
// If the descriptor has a weak reference to an unknown symbol, should we
// call that out here?
sb.AppendFormat(Res.Strings.INFO_FD_SUM_FMT, dfd.ToUiString());
}
sb.Append("\r\n");
// Debug only
//sb.Append("DEBUG: opAddr=" + attr.OperandAddress.ToString("x4") +
// " opOff=" + attr.OperandOffset.ToString("x4") + "\r\n");
sb.Append("\u2022Attributes:");
if (attr.IsHinted) {
sb.Append(" Hinted(");
for (int i = 0; i < line.OffsetSpan; i++) {
switch (mProject.TypeHints[line.FileOffset + i]) {
case CodeAnalysis.TypeHint.Code:
sb.Append("C");
break;
case CodeAnalysis.TypeHint.Data:
sb.Append("D");
break;
case CodeAnalysis.TypeHint.InlineData:
sb.Append("I");
break;
default:
break;
}
if (i > 8) {
sb.Append("...");
break;
}
}
sb.Append(')');
}
if (attr.IsEntryPoint) {
sb.Append(" EntryPoint");
}
if (attr.IsBranchTarget) {
sb.Append(" BranchTarget");
}
if (attr.DoesNotContinue) {
sb.Append(" NoContinue");
}
if (attr.DoesNotBranch) {
sb.Append(" NoBranch");
}
if (mProject.StatusFlagOverrides[line.FileOffset].AsInt != 0) {
sb.Append(" StatusFlags");
}
sb.Append("\r\n\r\n");
if (attr.IsInstruction) {
Asm65.OpDef op = mProject.CpuDef.GetOpDef(mProject.FileData[line.FileOffset]);
string shortDesc = mOpDesc.GetShortDescription(op.Mnemonic);
if (!string.IsNullOrEmpty(shortDesc)) {
if (op.IsUndocumented) {
sb.Append("\u25b6[*] ");
} else {
sb.Append("\u25b6 ");
}
sb.Append(shortDesc);
string addrStr = mOpDesc.GetAddressModeDescription(op.AddrMode);
if (!string.IsNullOrEmpty(addrStr)) {
sb.Append(", ");
sb.Append(addrStr);
}
sb.Append("\r\n");
}
sb.Append("\u2022Cycles: ");
int cycles = op.Cycles;
Asm65.OpDef.CycleMod cycMods = op.CycleMods;
sb.Append(cycles.ToString());
if (cycMods != 0) {
sb.Append(" (");
int workBits = (int)cycMods;
while (workBits != 0) {
// Isolate rightmost bit.
int firstBit = (~workBits + 1) & workBits;
sb.Append(mOpDesc.GetCycleModDescription((OpDef.CycleMod)firstBit));
// Remove from set.
workBits &= ~firstBit;
if (workBits != 0) {
// more to come
sb.Append(", ");
}
}
sb.Append(")");
}
sb.Append("\r\n");
const string FLAGS = "NVMXDIZC";
sb.Append("\u2022Flags affected: ");
Asm65.StatusFlags affectedFlags = op.FlagsAffected;
for (int i = 0; i < 8; i++) {
if (affectedFlags.GetBit((StatusFlags.FlagBits)(7 - i)) >= 0) {
sb.Append(' ');
sb.Append(FLAGS[i]);
} else {
sb.Append(" -");
}
}
sb.Append("\r\n");
string longDesc = mOpDesc.GetLongDescription(op.Mnemonic);
if (!string.IsNullOrEmpty(longDesc)) {
sb.Append("\r\n");
sb.Append(longDesc);
sb.Append("\r\n");
}
} else {
// do we want descriptions of the pseudo-ops?
}
// Publish
mMainWin.InfoPanelContents = sb.ToString();
}
#endregion Info panel
#region Debug features
public void RunSourceGenerationTests() {
Tests.WpfGui.GenTestRunner dlg = new Tests.WpfGui.GenTestRunner(mMainWin);
dlg.ShowDialog();
}
#endregion
}
}