/*
* 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.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using Asm65;
using CommonUtil;
namespace SourceGen.WpfGui {
///
/// Edit Address Region dialog.
///
public partial class EditAddress : Window, INotifyPropertyChanged {
///
/// Updated address map entry. Will be null if we want to delete the existing entry.
///
public AddressMap.AddressMapEntry ResultEntry { get; private set; }
///
/// Dialog header.
///
public string OperationStr {
get { return mOperationStr; }
set { mOperationStr = value; OnPropertyChanged(); }
}
private string mOperationStr;
///
/// Initial address. (Does not change.)
///
public string RegionAddressStr {
get { return "$" + mFormatter.FormatAddress(mRegionAddress, mShowBank); }
}
private int mRegionAddress;
///
/// Offset of first selected byte. (Does not change.)
///
public string RegionStartOffsetStr {
get { return mFormatter.FormatOffset24(mRegionStartOffset); }
}
private int mRegionStartOffset;
///
/// Offset of last selected byte. (Does not change.)
///
public string RegionEndOffsetStr {
get { return mFormatter.FormatOffset24(mRegionEndOffset); }
}
private int mRegionEndOffset;
public string RegionLengthStr {
get {
int count = mRegionEndOffset - mRegionStartOffset + 1;
return FormatLength(count);
}
}
///
/// Set to true to show the offset/length stats for a current region.
///
public bool ShowExistingRegion {
get { return mShowExistingRegion; }
set { mShowExistingRegion = value; OnPropertyChanged(); }
}
private bool mShowExistingRegion;
public bool ShowOption1 {
get { return mShowOption1; }
set { mShowOption1 = value; OnPropertyChanged(); }
}
private bool mShowOption1;
public bool ShowOption2 {
get { return mShowOption2; }
set { mShowOption2 = value; OnPropertyChanged(); }
}
private bool mShowOption2;
public bool EnableOption1 {
get { return mEnableOption1; }
set { mEnableOption1 = value; OnPropertyChanged(); }
}
private bool mEnableOption1;
public bool EnableOption2 {
get { return mEnableOption2; }
set { mEnableOption2 = value; OnPropertyChanged(); }
}
private bool mEnableOption2;
public bool CheckOption1 {
get { return mCheckOption1; }
set { mCheckOption1 = value; OnPropertyChanged(); }
}
private bool mCheckOption1;
public bool CheckOption2 {
get { return mCheckOption2; }
set { mCheckOption2 = value; OnPropertyChanged(); }
}
private bool mCheckOption2;
///
/// Address at which a pre-label would be placed. This is determined by the parent
/// region, so its value is fixed.
///
private int mPreLabelAddress;
public string PreLabelAddressStr {
get {
if (mPreLabelAddress == Address.NON_ADDR) {
return Address.NON_ADDR_STR;
} else {
return "$" + mFormatter.FormatAddress(mPreLabelAddress, mShowBank);
}
}
}
private bool mShowBank;
public bool ShowErrorMessage {
get { return mShowErrorMessage; }
set { mShowErrorMessage = value; OnPropertyChanged(); }
}
private bool mShowErrorMessage;
public string ErrorMessageStr {
get { return mErrorMessageStr; }
set { mErrorMessageStr = value; OnPropertyChanged(); }
}
private string mErrorMessageStr;
///
/// Address input TextBox.
///
public string AddressText {
get { return mAddressText; }
set { mAddressText = value; OnPropertyChanged(); UpdateControls(); }
}
private string mAddressText;
public bool UseRelativeAddressing {
get { return mUseRelativeAddressing; }
set { mUseRelativeAddressing = value; OnPropertyChanged(); UpdateControls(); }
}
private bool mUseRelativeAddressing;
public bool DisallowInwardRes {
get { return mDisallowInwardRes; }
set { mDisallowInwardRes = value; OnPropertyChanged(); UpdateControls(); }
}
private bool mDisallowInwardRes;
public bool DisallowOutwardRes {
get { return mDisallowOutwardRes; }
set { mDisallowOutwardRes = value; OnPropertyChanged(); UpdateControls(); }
}
private bool mDisallowOutwardRes;
///
/// Pre-label input TextBox.
///
public string PreLabelText {
get { return mPreLabelText; }
set { mPreLabelText = value; OnPropertyChanged(); UpdateControls(); }
}
private string mPreLabelText;
///
/// Set to true when input is valid. Controls whether the OK button is enabled.
///
public bool IsValid {
get { return mIsValid; }
set { mIsValid = value; OnPropertyChanged(); }
}
private bool mIsValid;
///
/// Set to true unless there are no valid options (e.g. invalid new region).
///
public bool EnableAttributeControls {
get { return mEnableAttributeControls; }
set { mEnableAttributeControls = value; OnPropertyChanged(); }
}
private bool mEnableAttributeControls;
///
/// Set to true if the region has a floating end point.
///
public bool IsFloating {
get { return mIsFloating; }
set { mIsFloating = value; OnPropertyChanged(); }
}
private bool mIsFloating;
///
/// Set to true if the region is not new, and thus can be deleted.
///
public bool CanDeleteRegion {
get { return mCanDeleteRegion; }
set { mCanDeleteRegion = value; OnPropertyChanged(); }
}
private bool mCanDeleteRegion;
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// Non-error color for labels.
private Brush mDefaultLabelColor = SystemColors.WindowTextBrush;
///
/// Initial value for pre-label.
///
private string mOrigPreLabel;
///
/// True if parent region is non-addressable.
///
private bool mParentNonAddr;
///
/// Result for option #1.
///
private AddressMap.AddressMapEntry mResultEntry1;
///
/// Result for option #2.
///
private AddressMap.AddressMapEntry mResultEntry2;
///
/// Maximum allowed address value, based on CPU type.
///
private int mMaxAddressValue;
///
/// Reference to project. We need the address map and symbol table.
///
private DisasmProject mProject;
///
/// Reference to text formatter.
///
private Formatter mFormatter;
///
/// Constructor.
///
/// Parent window.
/// Current region; will be null for new entries.
/// Prototype entry to create.
/// Length, in bytes, of the selection.
/// True if the selection is a single line.
/// Project reference.
/// Text formatter object.
public EditAddress(Window owner, AddressMap.AddressRegion curRegion,
AddressMap.AddressMapEntry newEntry, int selectionLen, bool isSingleLine,
DisasmProject project, Formatter formatter) {
InitializeComponent();
Owner = owner;
DataContext = this;
Debug.Assert((curRegion == null) ^ (newEntry == null)); // exactly one must be true
mProject = project;
mMaxAddressValue = project.CpuDef.MaxAddressValue;
mShowBank = !project.CpuDef.HasAddr16;
mFormatter = formatter;
Configure(curRegion, newEntry, selectionLen, isSingleLine);
UpdateControls();
}
private void Configure(AddressMap.AddressRegion curRegion,
AddressMap.AddressMapEntry newEntry, int selectionLen, bool isSingleLine) {
Debug.WriteLine("Configuring AR: reg=" + curRegion + " newEnt=" + newEntry +
" selLen=" + selectionLen + " isSingle=" + isSingleLine);
ShowOption1 = ShowOption2 = true;
EnableOption1 = EnableOption2 = true;
CheckOption1 = true;
EnableAttributeControls = true;
string option1Summ;
string option1Msg;
string option2Summ;
string option2Msg;
if (curRegion != null) {
// Editing an existing region.
CanDeleteRegion = true;
ShowExistingRegion = true;
mOrigPreLabel = curRegion.PreLabel;
mParentNonAddr = (curRegion.PreLabelAddress == Address.NON_ADDR);
if (curRegion.Address == Address.NON_ADDR) {
AddressText = Address.NON_ADDR_STR;
} else {
AddressText = Asm65.Address.AddressToString(curRegion.Address, false);
}
PreLabelText = curRegion.PreLabel;
DisallowInwardRes = curRegion.DisallowInward;
DisallowOutwardRes = curRegion.DisallowOutward;
UseRelativeAddressing = curRegion.IsRelative;
OperationStr = (string)FindResource("str_HdrEdit");
mRegionAddress = curRegion.Address;
mRegionStartOffset = curRegion.Offset;
mRegionEndOffset = curRegion.Offset + curRegion.ActualLength - 1;
mPreLabelAddress = curRegion.PreLabelAddress;
if (isSingleLine) {
// Only thing selected was arstart/arend. First action is to edit
// the region properties, second action is to convert floating end
// to fixed.
mResultEntry1 = new AddressMap.AddressMapEntry(curRegion.Offset,
curRegion.Length, curRegion.Address, curRegion.PreLabel,
curRegion.DisallowInward, curRegion.DisallowOutward,
curRegion.IsRelative);
option1Summ = (string)FindResource("str_OptEditAsIsSummary");
option1Msg = (string)FindResource("str_OptEditAsIs");
if (curRegion.IsFloating) {
option2Summ = (string)FindResource("str_OptEditAndFixSummary");
option2Msg = (string)FindResource("str_OptEditAndFix");
mResultEntry2 = new AddressMap.AddressMapEntry(curRegion.Offset,
curRegion.ActualLength, curRegion.Address, curRegion.PreLabel,
curRegion.DisallowInward, curRegion.DisallowOutward,
curRegion.IsRelative);
} else {
option2Summ = string.Empty;
option2Msg = (string)FindResource("str_EditFixedAlreadyFixed");
mResultEntry2 = null;
EnableOption2 = false; // show it, but disabled
}
} else {
// Selection started with arstart and included multiple lines. First
// action is to resize region. Second action is edit without resize.
// If resize is illegal (e.g. new region exactly overlaps another),
// first action is disabled.
mResultEntry1 = new AddressMap.AddressMapEntry(curRegion.Offset,
selectionLen, curRegion.Address, curRegion.PreLabel,
curRegion.DisallowInward, curRegion.DisallowOutward, curRegion.IsRelative);
mResultEntry2 = new AddressMap.AddressMapEntry(curRegion.Offset,
curRegion.Length, curRegion.Address, curRegion.PreLabel,
curRegion.DisallowInward, curRegion.DisallowOutward, curRegion.IsRelative);
option1Summ = (string)FindResource("str_OptResizeSummary");
string fmt = (string)FindResource("str_OptResize");
option1Msg = string.Format(fmt,
mFormatter.FormatOffset24(curRegion.Offset + selectionLen - 1),
FormatLength(selectionLen));
option2Summ = (string)FindResource("str_OptEditAsIsSummary");
option2Msg = (string)FindResource("str_OptEditAsIs");
Debug.Assert(selectionLen > 0);
AddressMap.AddResult ares;
TryCreateRegion(curRegion, curRegion.Offset, selectionLen,
curRegion.Address, out ares);
if (ares != AddressMap.AddResult.Okay) {
// Can't resize the new region, so disable that option (still visible).
option1Summ = string.Empty;
string fmta = (string)FindResource("str_OptResizeFail");
option1Msg = string.Format(fmta, GetErrorString(ares));
EnableOption1 = false;
CheckOption2 = true;
}
if (curRegion.ActualLength == selectionLen) {
// The selection size matches the region's length, which means they
// have the entire region selected, so "resize" and "edit" do the same
// thing. No real need to disable the resize option, but we can default
// to "edit only" to emphasize that there's no actual change.
CheckOption2 = true;
}
}
} else {
// Creating a new region. Prototype entry specifies offset, length, and address.
// First action is to create a fixed-length region, second action is to create
// a floating region. Default changes for single-item selections.
CanDeleteRegion = false;
ShowExistingRegion = false;
mOrigPreLabel = string.Empty;
if (newEntry.Address == Address.NON_ADDR) {
AddressText = Address.NON_ADDR_STR;
} else {
AddressText = Asm65.Address.AddressToString(newEntry.Address, false);
}
PreLabelText = string.Empty;
DisallowInwardRes = false;
DisallowOutwardRes = false;
UseRelativeAddressing = false;
OperationStr = (string)FindResource("str_HdrCreate");
AddressMap.AddResult ares1;
AddressMap.AddressRegion newRegion1 = TryCreateRegion(null, newEntry.Offset,
newEntry.Length, newEntry.Address, out ares1);
AddressMap.AddResult ares2;
AddressMap.AddressRegion newRegion2 = TryCreateRegion(null, newEntry.Offset,
AddressMap.FLOATING_LEN, newEntry.Address, out ares2);
if (isSingleLine) {
// For single-line selection, create a floating region by default.
CheckOption2 = true;
}
// If it failed, report the error. Most common reason will be a start offset
// that overlaps an existing region. You can create a fixed region inside
// a fixed region with the same start offset, but can't create a float there.
if (ares1 == AddressMap.AddResult.Okay) {
mResultEntry1 = new AddressMap.AddressMapEntry(newEntry.Offset,
newRegion1.ActualLength, newEntry.Address,
string.Empty, false, false, false);
option1Summ = (string)FindResource("str_CreateFixedSummary");
string fmt = (string)FindResource("str_CreateFixed");
option1Msg = string.Format(fmt,
mFormatter.FormatOffset24(newEntry.Offset),
FormatLength(newRegion1.ActualLength));
mPreLabelAddress = newRegion1.PreLabelAddress;
mParentNonAddr = (newRegion1.PreLabelAddress == Address.NON_ADDR);
} else {
option1Summ = string.Empty;
if (ares1 == AddressMap.AddResult.StraddleExisting) {
option1Msg = (string)FindResource("str_CreateFixedFailStraddle");
} else {
option1Msg = (string)FindResource("str_CreateFixedFail");
}
CheckOption2 = true;
EnableOption1 = false;
}
if (ares2 == AddressMap.AddResult.Okay) {
mResultEntry2 = new AddressMap.AddressMapEntry(newEntry.Offset,
AddressMap.FLOATING_LEN, newEntry.Address,
string.Empty, false, false, false);
option2Summ = (string)FindResource("str_CreateFloatingSummary");
string fmt = (string)FindResource("str_CreateFloating");
option2Msg = string.Format(fmt,
mFormatter.FormatOffset24(newEntry.Offset),
FormatLength(newRegion2.ActualLength));
mPreLabelAddress = newRegion2.PreLabelAddress;
mParentNonAddr = (newRegion2.PreLabelAddress == Address.NON_ADDR);
} else {
option2Summ = string.Empty;
option2Msg = (string)FindResource("str_CreateFloatingFail");
CheckOption1 = true;
CheckOption2 = false; // required for some reason
EnableOption2 = false;
}
if (ares1 != AddressMap.AddResult.Okay && ares2 != AddressMap.AddResult.Okay) {
// Unable to create region here. Explain why not.
EnableAttributeControls = false;
CheckOption1 = CheckOption2 = false;
mPreLabelAddress = Address.NON_ADDR;
SetErrorString(ares1);
}
}
TextBlock tb1 = option1TextBlock;
tb1.Inlines.Clear();
if (!string.IsNullOrEmpty(option1Summ)) {
tb1.Inlines.Add(new Run(option1Summ + " ") { FontWeight = FontWeights.Bold });
}
tb1.Inlines.Add(option1Msg);
TextBlock tb2 = option2TextBlock;
tb2.Inlines.Clear();
if (!string.IsNullOrEmpty(option2Summ)) {
tb2.Inlines.Add(new Run(option2Summ + " ") { FontWeight = FontWeights.Bold });
}
tb2.Inlines.Add(option2Msg);
}
private string FormatLength(int len) {
return len + " (" + mFormatter.FormatHexValue(len, 2) + ")";
}
private AddressMap.AddressRegion TryCreateRegion(AddressMap.AddressRegion delRegion,
int offset, int length, int addr, out AddressMap.AddResult result) {
AddressMap tmpMap = mProject.AddrMap.Clone();
if (delRegion != null && !tmpMap.RemoveEntry(delRegion.Offset, delRegion.Length)) {
Debug.Assert(false, "Failed to remove existing region");
result = AddressMap.AddResult.InternalError;
return null;
}
result = tmpMap.AddEntry(offset, length, addr);
if (result != AddressMap.AddResult.Okay) {
return null;
}
AddressMap.AddressRegion newRegion = tmpMap.FindRegion(offset, length);
if (newRegion == null) {
// Shouldn't happen.
Debug.Assert(false, "Failed to find region we just created");
result = AddressMap.AddResult.InternalError;
return null;
}
return newRegion;
}
private string GetErrorString(AddressMap.AddResult result) {
string rsrc;
switch (result) {
case AddressMap.AddResult.InternalError:
rsrc = "str_ErrInternal";
break;
case AddressMap.AddResult.InvalidValue:
rsrc = "str_ErrInvalidValue";
break;
case AddressMap.AddResult.OverlapExisting:
rsrc = "str_ErrOverlapExisting";
break;
case AddressMap.AddResult.OverlapFloating:
rsrc = "str_ErrOverlapFloating";
break;
case AddressMap.AddResult.StraddleExisting:
rsrc = "str_ErrStraddleExisting";
break;
default:
Debug.Assert(false);
rsrc = "str_ErrInternal";
break;
}
return(string)FindResource(rsrc); // throws exception on failure
}
private void SetErrorString(AddressMap.AddResult result) {
ErrorMessageStr = GetErrorString(result);
ShowErrorMessage = true;
}
private void Window_ContentRendered(object sender, EventArgs e) {
addrTextBox.SelectAll();
addrTextBox.Focus();
}
///
/// Handles a TextChanged event on the address text box.
///
///
/// Must have UpdateSourceTrigger=PropertyChanged set for this to work. The default
/// for TextBox is LostFocus.
///
private void UpdateControls() {
bool addrOkay = ParseAddress(out int unused);
if (addrOkay) {
enterAddressLabel.Foreground = mDefaultLabelColor;
} else {
enterAddressLabel.Foreground = Brushes.Red;
}
bool preLabelOkay = PreLabelTextChanged();
IsValid = EnableAttributeControls && addrOkay && preLabelOkay;
}
///
/// Validates pre-label.
///
private bool PreLabelTextChanged() {
string label = PreLabelText;
parentNonAddrLabel.Foreground = mDefaultLabelColor;
if (string.IsNullOrEmpty(label)) {
return true;
}
if (mParentNonAddr) {
parentNonAddrLabel.Foreground = Brushes.Blue;
}
// Check syntax. We don't care about the details.
bool isValid = Asm65.Label.ValidateLabelDetail(label, out bool unused1,
out bool unused2);
if (!isValid) {
validSyntaxLabel.Foreground = Brushes.Red;
return false;
} else {
validSyntaxLabel.Foreground = mDefaultLabelColor;
}
// Check for duplicates.
notDuplicateLabel.Foreground = mDefaultLabelColor;
if (label == mOrigPreLabel) {
return true;
}
if (mProject.SymbolTable.TryGetValue(label, out Symbol sym)) {
notDuplicateLabel.Foreground = Brushes.Red;
return false;
}
return true;
}
private void OkButton_Click(object sender, RoutedEventArgs e) {
bool ok = ParseAddress(out int addr);
Debug.Assert(ok);
AddressMap.AddressMapEntry baseEntry;
if (CheckOption1) {
baseEntry = mResultEntry1;
} else {
baseEntry = mResultEntry2;
}
// Combine base entry with pre-label string and relative addressing checkbox.
ResultEntry = new AddressMap.AddressMapEntry(baseEntry.Offset,
baseEntry.Length, addr, PreLabelText,
DisallowInwardRes, DisallowOutwardRes, UseRelativeAddressing);
Debug.WriteLine("Dialog result: " + ResultEntry);
DialogResult = true;
}
///
/// Parses the address out of the AddressText text box.
///
/// Receives the parsed address. Will be NON_ADDR for "NA".
/// True if the string parsed successfully.
private bool ParseAddress(out int addr) {
// "NA" for non-addressable?
string upper = AddressText.ToUpper();
if (upper == Address.NON_ADDR_STR) {
addr = Address.NON_ADDR;
return true;
}
// Parse numerically.
return Asm65.Address.ParseAddress(AddressText, mMaxAddressValue, out addr);
}
private void DeleteRegion_Click(object sender, RoutedEventArgs e) {
ResultEntry = null;
DialogResult = true;
}
}
}