1
0
mirror of https://github.com/fadden/6502bench.git synced 2025-02-12 00:30:48 +00:00
6502bench/SourceGen/MainController.cs
Andy McFadden ec9017cbc3 Add "omit implicit acc operand" feature
By default, implicit acc operands are shown, e.g. "LSR A" rather
than just "LSR".  I like showing operands for instructions that
have multiple address modes.

Not everyone agrees, so now it's a setting.  They're shown by default,
but enabling the option will strip them on-screen, in generated
assembly, and in the instruction chart.

They are always omitted for ACME output, which doesn't allow them.

(issue #162)
2024-09-15 13:29:04 -07:00

5329 lines
230 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 System.Windows.Media;
using System.Windows.Threading;
using Microsoft.Win32;
using Asm65;
using CommonUtil;
using CommonWPF;
using SourceGen.Sandbox;
using SourceGen.WpfGui;
namespace SourceGen {
/// <summary>
/// This class manages user interaction. The goal is for this to be relatively
/// GUI-toolkit-agnostic, with all the WPF stuff tucked into the code-behind files. An
/// instance of this class is created by MainWindow when the app starts.
///
/// There is some Windows-specific stuff, like MessageBox and OpenFileDialog.
/// </summary>
public class MainController {
private const string SETTINGS_FILE_NAME = "SourceGen-settings";
#region Project state
// Currently open project, or null if none.
private DisasmProject mProject;
// Pathname to 65xx data file.
private string mDataPathName;
// Pathname of .dis65 file. This will be empty for a new project.
private string mProjectPathName;
/// <summary>
/// Data backing the code list. Will be null if the project is not open.
/// </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>
/// Apple II screen chart window. Not tied to the project.
/// </summary>
private Tools.WpfGui.Apple2ScreenChart mApple2ScreenChartDialog;
/// <summary>
/// Returns true if the ASCII chart window is currently open.
/// </summary>
public bool IsApple2ScreenChartOpen { get { return mApple2ScreenChartDialog != 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>
/// Tracks the operands we have highlighted.
/// </summary>
private List<int> mOperandHighlights = new List<int>();
/// <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 (effectively disabling the security features).
/// </summary>
public bool UseMainAppDomainForPlugins { get; private set; }
/// <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
/// <summary>
/// Constructor, called from the main window code.
/// </summary>
public MainController(MainWindow win) {
mMainWin = win;
CreateAutoSaveTimer();
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(); // <-- this causes WindowLoaded to fire
}
/// <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() {
// Run library unit tests.
Debug.Assert(CommonUtil.AddressMap.Test());
Debug.Assert(CommonUtil.RangeSet.Test());
Debug.Assert(CommonUtil.TypedRangeSet.Test());
Debug.Assert(CommonUtil.Version.Test());
Debug.Assert(Asm65.CpuDef.DebugValidate());
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.SetInt(AppSettings.PROJ_AUTO_SAVE_INTERVAL, 60); // enabled 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_ADDR_PRE_LABELS, true);
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.SetEnum(AppSettings.SRCGEN_LABEL_NEW_LINE,
AsmGen.GenCommon.LabelPlacement.SplitIfTooLong);
#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>
/// 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>
public void ApplyAppSettings() {
Debug.WriteLine("ApplyAppSettings...");
AppSettings settings = AppSettings.Global;
// Set up the formatter with default values.
mFormatterConfig = new Formatter.FormatConfig();
AsmGen.GenCommon.ConfigureFormatterFromSettings(AppSettings.Global,
ref mFormatterConfig);
mFormatterConfig.EndOfLineCommentDelimiter = ";";
mFormatterConfig.NonUniqueLabelPrefix =
settings.GetString(AppSettings.FMT_NON_UNIQUE_LABEL_PREFIX, string.Empty);
mFormatterConfig.LocalVariableLabelPrefix =
settings.GetString(AppSettings.FMT_LOCAL_VARIABLE_PREFIX, string.Empty);
mFormatterConfig.CommaSeparatedDense =
settings.GetBool(AppSettings.FMT_COMMA_SEP_BULK_DATA, true);
mFormatterConfig.SuppressImpliedAcc =
settings.GetBool(AppSettings.SRCGEN_OMIT_IMPLIED_ACC_OPERAND, false);
mFormatterConfig.DebugLongComments = DebugLongComments;
string chrDelCereal = settings.GetString(AppSettings.FMT_CHAR_DELIM, null);
if (chrDelCereal != null) {
mFormatterConfig.CharDelimiters =
Formatter.DelimiterSet.Deserialize(chrDelCereal);
}
string strDelCereal = settings.GetString(AppSettings.FMT_STRING_DELIM, null);
if (strDelCereal != null) {
mFormatterConfig.StringDelimiters =
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.SymFilterAddrPreLabels =
settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR_PRE_LABELS, 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);
// Refresh the toolbar checkbox.
mMainWin.DoShowCycleCounts =
AppSettings.Global.GetBool(AppSettings.FMT_SHOW_CYCLE_COUNTS, false);
// 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);
}
// If auto-save was enabled or disabled, create or remove the recovery file.
RefreshRecoveryFile();
}
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 Auto-save
private const string RECOVERY_EXT_ADD = "_rec";
private const string RECOVERY_EXT = ProjectFile.FILENAME_EXT + RECOVERY_EXT_ADD;
private string mRecoveryPathName = string.Empty; // path to recovery file, or empty str
private Stream mRecoveryStream = null; // stream for recovery file, or null
private DispatcherTimer mAutoSaveTimer = null; // auto-save timer, may be disabled
private DateTime mLastEditWhen = DateTime.Now; // timestamp of last user edit
private DateTime mLastAutoSaveWhen = DateTime.Now; // timestamp of last auto-save
private bool mAutoSaveDeferred = false;
/// <summary>
/// Creates an interval timer that fires an event on the GUI thread.
/// </summary>
private void CreateAutoSaveTimer() {
mAutoSaveTimer = new DispatcherTimer();
mAutoSaveTimer.Tick += new EventHandler(AutoSaveTick);
mAutoSaveTimer.Interval = TimeSpan.FromSeconds(30); // place-holder, overwritten later
}
/// <summary>
/// Resets the auto-save timer to the configured interval. Has no effect if the timer
/// isn't currently running.
/// </summary>
private void ResetAutoSaveTimer() {
if (mAutoSaveTimer.IsEnabled) {
// Setting the Interval resets the timer.
mAutoSaveTimer.Interval = mAutoSaveTimer.Interval;
}
}
/// <summary>
/// Handles the auto-save timer event.
/// </summary>
/// <remarks>
/// We're using a DispatcherTimer, which appears to execute as part of the dispatcher,
/// not a System.Timers.Timer thread, which runs asynchronously. So not only do we not
/// have to worry about SynchronizationObjects, it seems likely that this won't fire
/// after the timer is disabled.
/// </remarks>
private void AutoSaveTick(object sender, EventArgs e) {
try {
if (mRecoveryStream == null) {
Debug.WriteLine("AutoSave tick: no recovery file");
return;
}
if (mLastEditWhen <= mLastAutoSaveWhen) {
Debug.WriteLine("AutoSave tick: recovery file is current (edit at " +
mLastEditWhen + ", auto-save at " + mLastAutoSaveWhen + ")");
return;
}
if (!mProject.IsDirty) {
// This may seem off, because of the following scenario: open a file, make a
// single edit, wait for auto-save, then hit Undo. Changes have been made,
// but the project is now back to its original form, so IsDirty is false. If
// we don't auto-save now, the recovery file will have a newer modification
// date than the project file, but will be stale.
//
// Technically, we don't need to update the recovery file, because the base
// project file has the correct and complete project. There's no real need
// for us to save another copy. If we crash, we'll have a stale recovery file
// with a newer timestamp, but we could handle that by back-dating the file
// timestamp or simply by truncating the recovery stream.
//
// The real reason for this test is that we don't want to auto-save if the
// user is being good about manual saves.
if (mRecoveryStream.Length != 0) {
Debug.WriteLine("AutoSave tick: project not dirty, truncating recovery");
mRecoveryStream.SetLength(0);
} else {
Debug.WriteLine("AutoSave tick: project not dirty");
}
mLastAutoSaveWhen = mLastEditWhen; // bump this so earlier test fires
return;
}
// The project is dirty, and we haven't auto-saved since the last change was
// made. Serialize the project to the recovery file.
Mouse.OverrideCursor = Cursors.Wait;
DateTime startWhen = DateTime.Now;
mRecoveryStream.Position = 0;
mRecoveryStream.SetLength(0);
if (!ProjectFile.SerializeToStream(mProject, mRecoveryStream,
out string errorMessage)) {
Debug.WriteLine("AutoSave FAILED: " + errorMessage);
}
mRecoveryStream.Flush(); // flush is very important, timing is not; try Async?
mLastAutoSaveWhen = DateTime.Now;
Debug.WriteLine("AutoSave tick: recovery file updated: " + mRecoveryStream.Length +
" bytes (" + (mLastAutoSaveWhen - startWhen).TotalMilliseconds + " ms)");
} catch (Exception ex) {
// Not expected, but let's not crash just because auto-save is broken.
Debug.WriteLine("AutoSave FAILED ENTIRELY: " + ex);
} finally {
Mouse.OverrideCursor = null;
}
}
/// <summary>
/// Creates or deletes the recovery file, based on the current app settings.
/// </summary>
/// <remarks>
/// <para>This is called when:</para>
/// <list type="bullet">
/// <item>a new project is created</item>
/// <item>an existing project is opened</item>
/// <item>app settings are updated</item>
/// <item>Save As is used to change the project path</item>
/// <item>the project is saved for the first time after a recovery file decision (i.e.
/// while mAutoSaveDeferred is true)</item>
/// </list>
/// </remarks>
private void RefreshRecoveryFile() {
if (mProject == null) {
// Project not open, nothing to do.
return;
}
if (mProject.IsReadOnly) {
// Changes cannot be made, so there's no need for a recovery file. Also, we
// might be in read-only mode because the project is already open and has a
// recovery file opened by another process.
Debug.WriteLine("Recovery: project is read-only, not creating recovery file");
Debug.Assert(mRecoveryStream == null);
return;
}
if (mAutoSaveDeferred) {
Debug.WriteLine("Recovery: auto-save deferred, not touching recovery file");
return;
}
int interval = AppSettings.Global.GetInt(AppSettings.PROJ_AUTO_SAVE_INTERVAL, 0);
if (interval <= 0) {
// We don't want a recovery file. If one is open, close it and remove it.
if (mRecoveryStream != null) {
Debug.WriteLine("Recovery: auto-save is disabled");
DiscardRecoveryFile();
Debug.Assert(string.IsNullOrEmpty(mRecoveryPathName));
} else {
Debug.WriteLine("Recovery: auto-save is disabled, file was not open");
}
mAutoSaveTimer.Stop();
} else {
// Configure auto-save. We need to update the interval in case it was changed
// by an app settings update.
mAutoSaveTimer.Interval = TimeSpan.FromSeconds(interval);
// Force an initial auto-save (on next timer tick) if the project is dirty, in
// case auto-save was previously disabled.
mLastAutoSaveWhen = mLastEditWhen.AddSeconds(-1);
string pathName = GenerateRecoveryPathName(mProjectPathName);
if (!string.IsNullOrEmpty(mRecoveryPathName) && pathName == mRecoveryPathName) {
// File is open and the filename hasn't changed. Nothing to do.
Debug.Assert(mRecoveryStream != null);
Debug.WriteLine("Recovery: open, no changes");
} else {
if (mRecoveryStream != null) {
Debug.WriteLine("Recovery: closing '" + mRecoveryPathName +
"' in favor of '" + pathName + "'");
DiscardRecoveryFile();
}
if (!string.IsNullOrEmpty(pathName)) {
Debug.WriteLine("Recovery: creating '" + pathName + "'");
PrepareRecoveryFile();
} else {
// Must be a new project that has never been saved.
Debug.WriteLine("Recovery: project name not set, can't create recovery file");
}
}
mAutoSaveTimer.Start();
}
}
private static string GenerateRecoveryPathName(string pathName) {
if (string.IsNullOrEmpty(pathName)) {
return string.Empty;
} else {
return pathName + RECOVERY_EXT_ADD;
}
}
/// <summary>
/// Creates the recovery file, overwriting any existing file. If auto-save is disabled
/// (indicated by an empty recovery file name), this does nothing.
/// </summary>
private void PrepareRecoveryFile() {
Debug.Assert(mRecoveryStream == null);
Debug.Assert(!string.IsNullOrEmpty(mProjectPathName));
Debug.Assert(string.IsNullOrEmpty(mRecoveryPathName));
string pathName = GenerateRecoveryPathName(mProjectPathName);
try {
mRecoveryStream = new FileStream(pathName, FileMode.OpenOrCreate, FileAccess.Write);
mRecoveryPathName = pathName;
} catch (Exception ex) {
MessageBox.Show(mMainWin, "Failed to create recovery file '" +
pathName + "': " + ex.Message, "File Error",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
/// <summary>
/// If we have a recovery file, close and delete it. This does nothing if the recovery
/// file is not currently open.
/// </summary>
private void DiscardRecoveryFile() {
if (mRecoveryStream == null) {
Debug.Assert(string.IsNullOrEmpty(mRecoveryPathName));
return;
}
Debug.WriteLine("Recovery: discarding recovery file '" + mRecoveryPathName + "'");
mRecoveryStream.Close();
try {
File.Delete(mRecoveryPathName);
} catch (Exception ex) {
MessageBox.Show(mMainWin, "Failed to delete recovery file '" +
mRecoveryPathName + "': " + ex.Message, "File Error",
MessageBoxButton.OK, MessageBoxImage.Warning);
// Discard our internal state.
}
mRecoveryStream = null;
mRecoveryPathName = string.Empty;
mAutoSaveTimer.Stop();
}
/// <summary>
/// Asks the user if they want to use the recovery file, if one is present and non-empty.
/// Both files must exist.
/// </summary>
/// <param name="projPathName">Path to project file we're trying to open</param>
/// <param name="recoveryPath">Path to recovery file.</param>
/// <param name="pathToUse">Result: path the user wishes to use. If we didn't ask the
/// user to choose, because the recovery file was empty or in use by another process,
/// this will be an empty string.</param>
/// <param name="asReadOnly">Result: true if project should be opened read-only.</param>
/// <returns>False if the user cancelled the operation, true to continue.</returns>
private bool HandleRecoveryChoice(string projPathName, string recoveryPath,
out string pathToUse, out bool asReadOnly) {
pathToUse = string.Empty;
asReadOnly = false;
try {
using (FileStream stream = new FileStream(recoveryPath, FileMode.Open,
FileAccess.ReadWrite, FileShare.None)) {
if (stream.Length == 0) {
// Recovery file exists, but is empty and not open by another process.
// Ignore it. (We could delete it here, but there's no need.)
Debug.WriteLine("Recovery: found existing zero-length file (ignoring)");
return true;
}
}
} catch (Exception ex) {
// Unable to open recovery file. This is probably happening because another
// process has the file open.
Debug.WriteLine("Unable to open recovery file: " + ex.Message);
MessageBoxResult mbr = MessageBox.Show(mMainWin,
"The project has a recovery file that can't be opened, possibly because the " +
"project is currently open by another copy of the application. Do you wish " +
"to open the file read-only?",
"Unable to Open", MessageBoxButton.OKCancel, MessageBoxImage.Hand);
if (mbr == MessageBoxResult.OK) {
asReadOnly = true;
return true;
} else {
asReadOnly = false;
return false;
}
}
RecoveryChoice dlg = new RecoveryChoice(mMainWin, projPathName, recoveryPath);
if (dlg.ShowDialog() != true) {
return false;
}
if (dlg.UseRecoveryFile) {
Debug.WriteLine("Recovery: user chose recovery file");
pathToUse = recoveryPath;
} else {
Debug.WriteLine("Recovery: user chose project file");
pathToUse = projPathName;
}
return true;
}
#endregion Auto-save
#region Project management
private bool PrepareNewProject(string dataPathName, SystemDef sysDef) {
DisasmProject proj = new DisasmProject();
mDataPathName = dataPathName;
mProjectPathName = string.Empty;
byte[] fileData;
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 = UseMainAppDomainForPlugins;
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 static 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");
// Apply anyway to create an undoable non-event?
}
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 the addr/label highlight index.
// (Certain changes will blow away the CodeDisplayList and affect the selection,
// which will cause the selection-changed handler to try to un-highlight something
// that doesn't exist. We want to clear the index here, but we probably also want
// to clear the highlighting before we do it. As it happens, changes will either
// be big enough to wipe out our highlight, or small enough that we immediately
// re-highlight the thing that's already highlighted, so it doesn't really matter.
// If we start to see vestigial highlighting after a change, we'll need to be
// more rigorous here.)
mTargetHighlightIndex = -1;
// Clear operand highlighting indices as well.
mOperandHighlights.Clear();
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);
mReanalysisTimer.StartTask("Restore selection and top position");
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.
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.
mReanalysisTimer.StartTask("Populate Notes and Symbols");
PopulateNotesList();
PopulateSymbolsList();
mReanalysisTimer.EndTask("Populate Notes and Symbols");
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();
// Bump the edit timestamp so the auto-save will run.
mLastEditWhen = DateTime.Now;
}
/// <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
/// <summary>
/// Handles creation of a new project.
/// </summary>
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;
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();
SaveProjectAs();
RefreshRecoveryFile();
}
}
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 = UseMainAppDomainForPlugins;
// Is there a recovery file?
mAutoSaveDeferred = false;
string recoveryPath = GenerateRecoveryPathName(projPathName);
string openPath = projPathName;
if (File.Exists(recoveryPath)) {
// Found a recovery file.
bool ok = HandleRecoveryChoice(projPathName, recoveryPath, out string pathToUse,
out bool asReadOnly);
if (!ok) {
// Open has been cancelled.
return;
}
if (!string.IsNullOrEmpty(pathToUse)) {
// One was chosen. This should be the case unless the recovery file was
// empty, or was open by a different process.
Debug.WriteLine("Open: user chose '" + pathToUse + "', deferring auto-save");
openPath = pathToUse;
mAutoSaveDeferred = true;
}
newProject.IsReadOnly |= asReadOnly;
}
// 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(openPath, 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.EndsWith(ProjectFile.FILENAME_EXT,
StringComparison.InvariantCultureIgnoreCase)) {
dataPathName = projPathName.Substring(0,
projPathName.Length - ProjectFile.FILENAME_EXT.Length);
} else if (projPathName.EndsWith(RECOVERY_EXT,
StringComparison.InvariantCultureIgnoreCase)) {
dataPathName = projPathName.Substring(0,
projPathName.Length - RECOVERY_EXT.Length);
} else {
dataPathName = UNKNOWN_FILE;
}
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();
RefreshRecoveryFile();
}
/// <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>File data.</returns>
private byte[] FindValidDataFile(ref string dataPathName, DisasmProject proj,
out bool cancel) {
// TODO(someday):
// It would be nice to "fix" the length and CRC if they don't match while we're
// making manual edits to test files. We can pass "can fix" to the ChooseDataFile
// dialog, and have it return a "want fix" if they click on the "fix" button, and
// only enable this if the DEBUG menu is enabled. It's a little ugly but mostly
// works. One issue that must be handled is that "proj" has sized a bunch of data
// structures based on the expected file length, and will blow up if the actual
// length is different. So we really need to check both len/crc here, and if
// all broken things are fixable, return the "do fix" back to the caller so
// it can re-generate the DisasmProject object with the corrected length.
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;
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;
RefreshRecoveryFile();
// 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.Assert(!mProject.IsReadOnly); // save commands should be disabled
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();
if (mAutoSaveDeferred) {
mAutoSaveDeferred = false;
RefreshRecoveryFile();
}
// The project file is saved, no need to auto-save for a while.
ResetAutoSaveTimer();
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.
mApple2ScreenChartDialog?.Close();
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;
CodeLineList = null;
// We may get a "selection changed" message as things are being torn down. Clear
// these so we don't try to remove the highlight from something that doesn't exist.
mTargetHighlightIndex = -1;
mOperandHighlights.Clear();
mMainWin.ShowCodeListView = false;
mMainWin.ProjectClosing();
mGenerationLog = null;
UpdateTitle();
DiscardRecoveryFile();
// 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 = AppSettings.Global.GetEnum(AppSettings.CLIP_LINE_FORMAT,
ClipLineFormat.AssemblerSource);
int[] rightWidths = new int[] { 16, 6, 16, 80 };
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>
/// Handles Edit &gt; App Settings.
/// </summary>
public void EditAppSettings() {
ShowAppSettings(mMainWin, WpfGui.EditAppSettings.Tab.Unknown,
AsmGen.AssemblerInfo.Id.Unknown);
}
/// <summary>
/// Opens the application settings dialog. All changes to settings are made directly
/// to the AppSettings.Global object.
/// </summary>
public void ShowAppSettings(Window owner, EditAppSettings.Tab initialTab,
AsmGen.AssemblerInfo.Id initialAsmId) {
EditAppSettings dlg = new EditAppSettings(owner, mMainWin, initialTab, initialAsmId);
dlg.SettingsApplied += SetAppSettings; // called when "Apply" is clicked
dlg.ShowDialog();
}
/// <summary>
/// Applies settings to the project, and saves them to the settings files.
/// </summary>
private void SetAppSettings() {
ApplyAppSettings();
SaveAppSettings();
}
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((CodeListColumn)col);
}
break;
case LineListGen.Line.Type.ArStartDirective:
case LineListGen.Line.Type.ArEndDirective:
if ((CodeListColumn)col == CodeListColumn.Opcode) {
JumpToOperandTarget(line, false);
} else if (CanEditAddress()) {
EditAddress();
}
break;
case LineListGen.Line.Type.RegWidthDirective:
if (CanEditStatusFlags()) {
EditStatusFlags();
}
break;
case LineListGen.Line.Type.DataBankDirective:
if (CanEditDataBank()) {
EditDataBank();
}
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:
if (IsPlbInstruction(line) && CanEditDataBank()) {
// Special handling for PLB instruction, so you can update the bank
// value just by double-clicking on it. Only used for PLBs without
// user- or auto-assigned bank changes.
EditDataBank();
} else {
JumpToOperandTarget(line, false);
}
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 bool IsPlbInstruction(LineListGen.Line line) {
if (line.LineType != LineListGen.Line.Type.Code) {
return false;
}
int offset = line.FileOffset;
Anattrib attr = mProject.GetAnattrib(offset);
// should always be an instruction start since this is a code line
if (!attr.IsInstructionStart) {
Debug.Assert(false);
return false;
}
OpDef op = mProject.CpuDef.GetOpDef(mProject.FileData[offset]);
if (op != OpDef.OpPLB_StackPull) {
return false;
}
return true;
}
/// <summary>
/// Jumps to the line referenced by the operand of the selected line.
/// </summary>
/// <param name="line">Selected line.</param>
/// <param name="testOnly">If set, don't actually do the goto.</param>
/// <returns>True if a jump is available for this line.</returns>
private bool JumpToOperandTarget(LineListGen.Line line, bool testOnly) {
if (line.FileOffset < 0) {
// Double-click on project symbol EQUs and the file header comment are handled
// elsewhere.
return false;
}
if (line.IsAddressRangeDirective) {
// TODO(someday): make this jump to the specific directive rather than the first
// (should be able to do it with LineDelta)
AddressMap.AddressRegion region = CodeLineList.GetAddrRegionFromLine(line,
out bool unused);
if (region == null) {
Debug.Assert(false);
return false;
}
if (!testOnly) {
if (line.LineType == LineListGen.Line.Type.ArStartDirective) {
// jump to end
GoToLocation(new NavStack.Location(region.Offset + region.ActualLength - 1,
0, NavStack.GoToMode.JumpToArEnd), true);
} else {
// jump to start
GoToLocation(new NavStack.Location(region.Offset,
0, NavStack.GoToMode.JumpToArStart), true);
}
}
return true;
}
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
FormatDescriptor dfd = attr.DataDescriptor;
if (dfd != null && dfd.HasSymbol) {
// Operand has a symbol, do a symbol lookup. This is slower than a simple
// jump based on OperandOffset, but if we've incorporated reloc data then
// the jump will be wrong.
if (dfd.SymbolRef.IsVariable) {
if (!testOnly) {
GoToVarDefinition(line.FileOffset, dfd.SymbolRef, true);
}
return true;
} else {
if (mProject.SymbolTable.TryGetValue(dfd.SymbolRef.Label, out Symbol sym)) {
if (sym.SymbolSource == Symbol.Source.User ||
sym.SymbolSource == Symbol.Source.Auto ||
sym.SymbolSource == Symbol.Source.AddrPreLabel) {
int labelOffset = mProject.FindLabelOffsetByName(dfd.SymbolRef.Label);
if (labelOffset >= 0) {
if (!testOnly) {
NavStack.GoToMode mode = NavStack.GoToMode.JumpToCodeData;
if (sym.SymbolSource == Symbol.Source.AddrPreLabel) {
mode = NavStack.GoToMode.JumpToArStart;
}
GoToLocation(new NavStack.Location(labelOffset, 0, mode), true);
}
return 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);
if (!testOnly) {
GoToLocation(new NavStack.Location(offset, 0,
NavStack.GoToMode.JumpToCodeData), true);
}
return true;
}
}
} else {
Debug.Assert(false);
}
} else {
// must be a broken weak symbol ref
Debug.WriteLine("Operand symbol not found: " + dfd.SymbolRef.Label);
}
}
} else if (attr.OperandOffset >= 0) {
// Operand has an in-file target offset. We can resolve it as a numeric reference.
// Find the line for that offset and jump to it.
if (!testOnly) {
GoToLocation(new NavStack.Location(attr.OperandOffset, 0,
NavStack.GoToMode.JumpToCodeData), true);
}
return 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) {
if (!testOnly) {
GoToLocation(new NavStack.Location(operandOffset, 0,
NavStack.GoToMode.JumpToCodeData), true);
}
return true;
}
}
return false;
}
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 AR 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.ArStartDirective &&
selLine.LineType != LineListGen.Line.Type.ArEndDirective) {
return false;
}
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
// Can only start with arend if it's single-selection.
if (selIndex != lastIndex && selLine.LineType == LineListGen.Line.Type.ArEndDirective) {
return false;
}
// If multiple lines with code/data are selected, there must not be an arstart
// between them unless we're resizing a region. Determining whether or not a resize
// is valid is left to the edit dialog. It's okay for an arend to be in the middle
// so long as the corresponding arstart is at the current offset.
if (selLine.LineType == LineListGen.Line.Type.ArStartDirective) {
// Skip overlapping region check.
return true;
}
int firstOffset = CodeLineList[selIndex].FileOffset;
int lastOffset = CodeLineList[lastIndex].FileOffset;
if (firstOffset == lastOffset) {
// Single-item selection, we're fine.
return true;
}
// Anything else is too complicated to be worth messing with here. We could do
// the work, but we have no good way of telling the user what went wrong.
// Let the dialog explain it.
//// Compute exclusive end point of selected range.
//int nextOffset = lastOffset + CodeLineList[lastIndex].OffsetSpan;
//if (!mProject.AddrMap.IsRangeUnbroken(firstOffset, nextOffset - firstOffset)) {
// Debug.WriteLine("Found mid-selection AddressMap entry (len=" +
// (nextOffset - firstOffset) + ")");
// return false;
//}
//Debug.WriteLine("First +" + firstOffset.ToString("x6") +
// ", last +" + lastOffset.ToString("x6") + ",next +" + nextOffset.ToString("x6"));
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;
// The offset of an arend directive is the last byte in the address region. It
// has a span length of zero because it's a directive, so if it's selected as
// the last offset then our nextOffset calculation will be off by one. (This would
// be avoided by using an exclusive end offset, but that causes other problems.)
// Work around it here.
if (CodeLineList[lastIndex].LineType == LineListGen.Line.Type.ArEndDirective) {
nextOffset++;
}
// Compute length of selection. May be zero if it's entirely arstart/arend.
int selectedLen = nextOffset - firstOffset;
AddressMap.AddressRegion curRegion;
if (CodeLineList[selIndex].LineType == LineListGen.Line.Type.ArStartDirective ||
CodeLineList[selIndex].LineType == LineListGen.Line.Type.ArEndDirective) {
// First selected line was arstart/arend, find the address map entry.
curRegion = CodeLineList.GetAddrRegionFromLine(CodeLineList[selIndex],
out bool isSynth);
Debug.Assert(curRegion != null);
if (isSynth) {
// Synthetic regions are created for non-addressable "holes" in the map.
// They're not part of the map, so this is a create operation rather than
// a resize operation.
curRegion = null;
Debug.WriteLine("Ignoring synthetic region");
} else {
Debug.WriteLine("Using region from " + CodeLineList[selIndex].LineType +
": " + curRegion);
}
} else {
if (selectedLen == 0) {
// A length of zero is only possible if nothing but directives were selected,
// but since the first entry wasn't arstart/arend this can't happen.
Debug.Assert(false);
return;
}
curRegion = null;
}
AddressMap.AddressMapEntry newEntry = null;
if (curRegion == null) {
// No entry, create a new one. Use the current address as the default value,
// unless the region is non-addressable.
Anattrib attr = mProject.GetAnattrib(firstOffset);
int addr;
if (attr.IsNonAddressable) {
addr = Address.NON_ADDR;
} else {
addr = attr.Address;
}
// Create a prototype entry with the various values.
newEntry = new AddressMap.AddressMapEntry(firstOffset, selectedLen, addr);
Debug.WriteLine("New entry prototype: " + newEntry);
}
EditAddress dlg = new EditAddress(mMainWin, curRegion, newEntry,
selectedLen, firstOffset == lastOffset, mProject, mFormatter);
if (dlg.ShowDialog() != true) {
return;
}
ChangeSet cs = new ChangeSet(1);
if (curRegion != dlg.ResultEntry) {
UndoableChange uc = UndoableChange.CreateAddressChange(curRegion, dlg.ResultEntry);
cs.Add(uc);
}
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 CanEditDataBank() {
if (mProject.CpuDef.HasAddr16) {
return false; // only available for 65816
}
if (SelectionAnalysis.mNumItemsSelected != 1) {
return false;
}
return (SelectionAnalysis.mLineType == LineListGen.Line.Type.Code ||
SelectionAnalysis.mLineType == LineListGen.Line.Type.DataBankDirective);
}
public void EditDataBank() {
int selIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int offset = CodeLineList[selIndex].FileOffset;
// Get current user-specified value, or null.
mProject.DbrOverrides.TryGetValue(offset, out CodeAnalysis.DbrValue curValue);
EditDataBank dlg = new EditDataBank(mMainWin, mProject, mFormatter, curValue);
if (dlg.ShowDialog() != true) {
return;
}
if (dlg.Result != curValue) {
Debug.WriteLine("Changing DBR at +" + offset.ToString("x6") + " to $" + dlg.Result);
UndoableChange uc =
UndoableChange.CreateDataBankChange(offset, curValue, dlg.Result);
ChangeSet cs = new ChangeSet(uc);
ApplyUndoableChanges(cs);
}
}
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);
int addr = attr.Address;
if (attr.IsNonAddressable) {
addr = Address.NON_ADDR;
}
EditLabel dlg = new EditLabel(mMainWin, attr.Symbol, addr, 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();
}
}
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 symbol. The dialog can create a new entry or
// modify an existing entry, but can't delete an entry.
if (dlg.ProjectSymbolResult != null) {
DefSymbol oldSym = dlg.OrigProjectSymbolResult;
DefSymbol newSym = dlg.ProjectSymbolResult;
if (oldSym == newSym) {
Debug.WriteLine("No actual change to project symbol");
} else {
// Generate a completely new set of project properties.
ProjectProperties newProps = new ProjectProperties(mProject.ProjectProps);
// Add new symbol 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 symbol");
}
Debug.WriteLine("EditInstructionOperand: " + cs.Count + " changes");
if (cs.Count != 0) {
ApplyUndoableChanges(cs);
}
}
private void EditDataOperand() {
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,
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);