/*
* 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.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using Asm65;
namespace SourceGen.WpfGui {
///
/// Instruction operand editor.
///
public partial class EditInstructionOperand : Window, INotifyPropertyChanged {
///
/// Updated format descriptor. Will be null if the user selected "default".
///
public FormatDescriptor FormatDescriptorResult { get; private set; }
///
/// Updated local variable table. Will be null if no changes were made.
///
public LocalVariableTable LocalVariableResult { get; private set; }
///
/// Offset of the local variable table we updated in LocalVariableResult.
///
public int LocalVariableTableOffsetResult { get; private set; }
///
/// Offset of label that was edited. A non-negative value here indicates that an
/// edit has been made.
///
public int SymbolEditOffsetResult { get; private set; }
///
/// Project symbol for this operand, when this dialog was opened. Will be null if
/// there was no matching project symbol. This is used as the "before" value for
/// updates to the project symbol set.
///
public DefSymbol OrigProjectSymbolResult { get; private set; }
///
/// Edited project symbol, or null if no changes were made. This is used as the "after"
/// value for updates to the project symbol. This dialog is not allowed to delete
/// project symbols, so if this is null there's nothing to do.
///
public DefSymbol ProjectSymbolResult { get; private set; }
///
/// Updated label.
///
public Symbol SymbolEditResult { get; private set; }
private readonly string SYMBOL_NOT_USED;
private readonly string SYMBOL_UNKNOWN;
private readonly string SYMBOL_INVALID;
private readonly string CREATE_LOCAL_VARIABLE;
private readonly string EDIT_LOCAL_VARIABLE;
private readonly string LV_MATCH_FOUND_ADDRESS;
private readonly string LV_MATCH_FOUND_CONSTANT;
private readonly string CREATE_LABEL;
private readonly string EDIT_LABEL;
private readonly string CREATE_PROJECT_SYMBOL;
private readonly string EDIT_PROJECT_SYMBOL;
private readonly string CURRENT_LABEL;
private readonly string CURRENT_LABEL_ADJUSTED_FMT;
///
/// Project reference.
///
private DisasmProject mProject;
///
/// Offset of instruction being edited.
///
private int mOffset;
///
/// Format object.
///
private Formatter mFormatter;
///
/// Operation definition, from file data.
///
private OpDef mOpDef;
///
/// Status flags at the point where the instruction is defined. This tells us whether
/// an operand is 8-bit or 16-bit.
///
private StatusFlags mOpStatusFlags;
///
/// Operand value, extracted from file data. For a relative branch, this will be
/// an address instead.
///
private int mOperandValue;
///
/// True when the input is valid. Controls whether the OK button is enabled.
///
public bool IsValid {
get { return mIsValid; }
set { mIsValid = value; OnPropertyChanged(); }
}
private bool mIsValid;
///
/// Set when our load-time initialization is complete.
///
private bool mLoadDone;
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
///
/// Constructor.
///
/// Parent window.
/// Project reference.
/// File offset of instruction start.
/// Formatter object, for preview window.
public EditInstructionOperand(Window owner, DisasmProject project, int offset,
Formatter formatter) {
InitializeComponent();
Owner = owner;
DataContext = this;
mProject = project;
mOffset = offset;
mFormatter = formatter;
SYMBOL_NOT_USED = (string)FindResource("str_SymbolNotUsed");
SYMBOL_INVALID = (string)FindResource("str_SymbolNotValid");
SYMBOL_UNKNOWN = (string)FindResource("str_SymbolUnknown");
CREATE_LOCAL_VARIABLE = (string)FindResource("str_CreateLocalVariable");
EDIT_LOCAL_VARIABLE = (string)FindResource("str_EditLocalVariable");
LV_MATCH_FOUND_ADDRESS = (string)FindResource("str_LvMatchFoundAddress");
LV_MATCH_FOUND_CONSTANT = (string)FindResource("str_LvMatchFoundConstant");
CREATE_LABEL = (string)FindResource("str_CreateLabel");
EDIT_LABEL = (string)FindResource("str_EditLabel");
CREATE_PROJECT_SYMBOL = (string)FindResource("str_CreateProjectSymbol");
EDIT_PROJECT_SYMBOL = (string)FindResource("str_EditProjectSymbol");
CURRENT_LABEL = (string)FindResource("str_CurrentLabel");
CURRENT_LABEL_ADJUSTED_FMT = (string)FindResource("str_CurrentLabelAdjustedFmt");
Debug.Assert(offset >= 0 && offset < project.FileDataLength);
mOpDef = project.CpuDef.GetOpDef(project.FileData[offset]);
Anattrib attr = project.GetAnattrib(offset);
mOpStatusFlags = attr.StatusFlags;
Debug.Assert(offset + mOpDef.GetLength(mOpStatusFlags) <= project.FileDataLength);
if (attr.OperandAddress >= 0) {
// Use this as the operand value when available. This lets us present
// relative branch instructions in the expected form.
mOperandValue = attr.OperandAddress;
} else {
// For BlockMove this will have both parts.
mOperandValue = mOpDef.GetOperand(project.FileData, offset, attr.StatusFlags);
}
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
BasicFormat_Loaded();
NumericReferences_Loaded();
LocalVariables_Loaded();
mLoadDone = true;
}
private void Window_ContentRendered(object sender, EventArgs e) {
UpdateControls();
symbolTextBox.SelectAll();
symbolTextBox.Focus();
}
private void OkButton_Click(object sender, RoutedEventArgs e) {
FormatDescriptorResult = CreateDescriptorFromControls();
// Export the updated local variable table if we made changes.
if (mEditedLvTable != null) {
LocalVariableTable lvt = mProject.LvTables[mLvTableOffset];
if (mEditedLvTable != lvt) {
LocalVariableResult = mEditedLvTable;
LocalVariableTableOffsetResult = mLvTableOffset;
Debug.WriteLine("NEW TABLE:");
mEditedLvTable.DebugDump(mLvTableOffset);
} else {
Debug.WriteLine("No change to LvTable, not exporting");
}
}
if (mLabelHasBeenEdited) {
SymbolEditOffsetResult = mEditedLabelOffset;
SymbolEditResult = mEditedLabel;
}
ProjectSymbolResult = mEditedProjectSymbol;
DialogResult = true;
}
///
/// Looks up the symbol in the symbol table. If not found there, it checks for a
/// match against the existing or edited project symbol.
///
private bool LookupSymbol(string label, bool isNonUnique, out Symbol sym) {
if (isNonUnique) {
// Only applies to labels, so no need to check mEditedProjectSymbol. We
// could check mEditedLabel, but there's no reason to add a symbolic reference
// on top of the numeric reference.
int targetOffset;
Anattrib attr = mProject.GetAnattrib(mOffset);
if (attr.OperandOffset >= 0) {
targetOffset = attr.OperandOffset;
} else {
targetOffset = mOffset;
}
sym = mProject.FindBestNonUniqueLabel(label, targetOffset);
if (sym != null) {
return true;
}
} else {
if (mProject.SymbolTable.TryGetValue(label, out sym)) {
return true;
}
if (mEditedProjectSymbol != null && label.Equals(mEditedProjectSymbol.Label)) {
sym = mEditedProjectSymbol;
return true;
}
}
return false;
}
///
/// Updates the state of the UI controls as the user interacts with the dialog.
///
private void UpdateControls() {
if (!mLoadDone) {
return;
}
// Parts panel IsEnabled depends directly on formatSymbolButton.IsChecked.
IsValid = true;
IsSymbolAuto = false;
IsSymbolVar = false;
IsPartPanelEnabled = false;
SymbolValueDecimal = string.Empty;
if (FormatSymbol) {
IsPartPanelEnabled = mOpDef.IsExtendedImmediate;
string trimLabel = Symbol.TrimAndValidateLabel(SymbolLabel,
mFormatter.NonUniqueLabelPrefix, out bool isValid, out bool unused1,
out bool unused2, out bool hasNonUniquePrefix,
out Symbol.LabelAnnotation unused3);
if (!isValid) {
SymbolValueHex = SYMBOL_INVALID;
IsValid = false;
} else if (LookupSymbol(trimLabel, hasNonUniquePrefix, out Symbol sym)) {
if (sym.SymbolSource == Symbol.Source.Auto) {
// We try to block references to auto labels, but it's possible to get
// around it because FormatDescriptors are weak references (replace auto
// label with user label, reference non-existent auto label, remove user
// label). We could try harder, but currently not necessary.
//
// Referencing an auto label is unwise because we use weak references
// by name, and auto labels can appear, disappear, or be renamed.
IsValid = false;
IsSymbolAuto = true;
} else if (sym.SymbolSource == Symbol.Source.Variable) {
// Local variables can be de-duplicated and uniquified, so referring to
// them by name doesn't make sense. The numeric operand formatter will
// disregard attempts to use them in this way.
IsValid = false;
IsSymbolVar = true;
}
SymbolValueHex = mFormatter.FormatHexValue(sym.Value, 4);
SymbolValueDecimal = mFormatter.FormatDecimalValue(sym.Value);
} else {
// Valid but unknown symbol. This is fine -- symbols don't have to exist --
// but it's a little weird for non-unique symbols.
SymbolValueHex = SYMBOL_UNKNOWN;
}
} else {
SymbolValueHex = SYMBOL_NOT_USED;
}
// We want to disable the create/edit label button if a symbol has been
// specified, because the label being edited is the one that the numeric
// reference points to, not the one the symbol points to.
// TODO(maybe): leave it enabled if the symbolic ref matches the numeric ref.
IsDiddleLabelEnabled = !FormatSymbol;
UpdatePreview();
UpdateCopyToOperand();
}
///
/// Updates the contents of the preview text box.
///
private void UpdatePreview() {
// Generate a descriptor from the controls. This isn't strictly necessary, but it
// gets all of the data in one small package.
FormatDescriptor dfd = CreateDescriptorFromControls();
if (dfd == null) {
// Showing the right thing for the default format is surprisingly hard. There
// are a bunch of complicated steps that are performed in sequence, including
// the "nearby label" lookups, the elision of hidden symbols, and other
// obscure bits that may get tweaked from time to time. These things are not
// easy to factor out because we're slicing the data at a different angle: the
// initial pass walks the entire file looking for one thing at a point before
// analysis has completed, while here we're trying to mimic all of the
// steps for a single offset, after analysis has finished. It's a lot of work
// to show text that they'll see as soon as they hit "OK".
PreviewText = string.Empty;
return;
}
StringBuilder sb = new StringBuilder(16);
// Show the opcode. Don't bother trying to figure out width disambiguation here.
sb.Append(mFormatter.FormatOpcode(mOpDef, OpDef.WidthDisambiguation.None));
sb.Append(' ');
bool showHashPrefix = mOpDef.IsImmediate ||
mOpDef.AddrMode == OpDef.AddressMode.BlockMove;
if (showHashPrefix) {
sb.Append('#');
}
Anattrib attr = mProject.GetAnattrib(mOffset);
int previewHexDigits = (attr.Length - 1) * 2;
int operandValue = mOperandValue;
bool isPcRelative = false;
bool isBlockMove = false;
if (attr.OperandAddress >= 0) {
if (mOpDef.AddrMode == OpDef.AddressMode.PCRel ||
mOpDef.AddrMode == OpDef.AddressMode.DPPCRel) {
previewHexDigits = 4; // show branches as $xxxx even when on zero page
isPcRelative = true;
} else if (mOpDef.AddrMode == OpDef.AddressMode.PCRelLong ||
mOpDef.AddrMode == OpDef.AddressMode.StackPCRelLong) {
isPcRelative = true;
}
} else {
if (mOpDef.AddrMode == OpDef.AddressMode.BlockMove) {
// MVN and MVP screw things up by having two operands in one instruction.
// We deal with this by passing in the value from the second byte
// (source bank) as the value, and applying the chosen format to both bytes.
isBlockMove = true;
operandValue = mOperandValue >> 8;
previewHexDigits = 2;
}
}
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.Hex:
sb.Append(mFormatter.FormatHexValue(operandValue, previewHexDigits));
break;
case FormatDescriptor.SubType.Decimal:
sb.Append(mFormatter.FormatDecimalValue(operandValue));
break;
case FormatDescriptor.SubType.Binary:
sb.Append(mFormatter.FormatBinaryValue(operandValue, 8));
break;
case FormatDescriptor.SubType.Ascii:
case FormatDescriptor.SubType.HighAscii:
case FormatDescriptor.SubType.C64Petscii:
case FormatDescriptor.SubType.C64Screen:
CharEncoding.Encoding enc = PseudoOp.SubTypeToEnc(dfd.FormatSubType);
sb.Append(mFormatter.FormatCharacterValue(operandValue, enc));
break;
case FormatDescriptor.SubType.Symbol:
string trimLabel = Symbol.TrimAndValidateLabel(SymbolLabel,
mFormatter.NonUniqueLabelPrefix, out bool isValid, out bool unused1,
out bool unused2, out bool hasNonUniquePrefix,
out Symbol.LabelAnnotation unused3);
if (string.IsNullOrEmpty(trimLabel)) {
sb.Append("?");
} else if (LookupSymbol(trimLabel, hasNonUniquePrefix, out Symbol sym)) {
// Block move is a little weird. "MVN label1,label2" is supposed to use
// the bank byte, while "MVN #const1,#const2" uses the entire symbol.
// The easiest thing to do is require the user to specify the "bank"
// part for 24-bit symbols, and always generate this as an immediate.
//
// MVN/MVP are also the only instructions with two operands, something
// we don't really handle.
// TODO(someday): allow a different symbol for each part of the operand.
// Hack to make relative branches look right in the preview window.
// Otherwise they show up like "