mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
+ Automatically move to current address when clicking on the source view tab (so that the active row is shown after manually loading a symbol file, etc.)
440 lines
15 KiB
C#
440 lines
15 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Input;
|
|
using Avalonia.LogicalTree;
|
|
using Avalonia.Markup.Xaml;
|
|
using Avalonia.Threading;
|
|
using Dock.Avalonia.Controls;
|
|
using Mesen.Config;
|
|
using Mesen.Debugger.Controls;
|
|
using Mesen.Debugger.Disassembly;
|
|
using Mesen.Debugger.Integration;
|
|
using Mesen.Debugger.Labels;
|
|
using Mesen.Debugger.Utilities;
|
|
using Mesen.Debugger.ViewModels;
|
|
using Mesen.Debugger.ViewModels.DebuggerDock;
|
|
using Mesen.Debugger.Windows;
|
|
using Mesen.Interop;
|
|
using Mesen.Utilities;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace Mesen.Debugger.Views
|
|
{
|
|
public class SourceViewView : MesenUserControl
|
|
{
|
|
private SourceViewViewModel Model => (SourceViewViewModel)DataContext!;
|
|
private LocationInfo ActionLocation => _selectionHandler?.ActionLocation ?? new LocationInfo();
|
|
private bool IsMarginClick => _selectionHandler?.IsMarginClick ?? false;
|
|
private CpuType CpuType => Model.CpuType;
|
|
|
|
private SourceViewViewModel? _model;
|
|
private CodeViewerSelectionHandler? _selectionHandler;
|
|
private DisassemblyViewer _viewer;
|
|
private BaseToolContainerViewModel? _parentModel;
|
|
|
|
public SourceViewView()
|
|
{
|
|
InitializeComponent();
|
|
_viewer = this.GetControl<DisassemblyViewer>("disViewer");
|
|
_viewer.GetPropertyChangedObservable(DisassemblyViewer.VisibleRowCountProperty).Subscribe(x => {
|
|
SourceViewViewModel? model = Model;
|
|
if(model == null) {
|
|
return;
|
|
}
|
|
|
|
int rowCount = _viewer.VisibleRowCount;
|
|
int prevCount = model.VisibleRowCount;
|
|
if(prevCount != rowCount) {
|
|
int pos = model.ScrollPosition + (prevCount / 2);
|
|
model.VisibleRowCount = rowCount;
|
|
model.ScrollToRowNumber(pos);
|
|
model.Refresh();
|
|
}
|
|
});
|
|
|
|
InitContextMenu();
|
|
}
|
|
|
|
protected override void OnDataContextChanged(EventArgs e)
|
|
{
|
|
if(DataContext is SourceViewViewModel model && _model != model) {
|
|
if(_model != null) {
|
|
model.VisibleRowCount = _model.VisibleRowCount;
|
|
}
|
|
_model = model;
|
|
_model.SetViewer(_viewer);
|
|
_selectionHandler?.Dispose();
|
|
if(model != null) {
|
|
_selectionHandler = new CodeViewerSelectionHandler(_viewer, _model, (rowIndex, rowAddress) => rowIndex + _model.ScrollPosition, true);
|
|
}
|
|
}
|
|
base.OnDataContextChanged(e);
|
|
}
|
|
|
|
private void InitContextMenu()
|
|
{
|
|
List<ContextMenuAction> actions = new List<ContextMenuAction> {
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.Copy,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Copy),
|
|
IsVisible = () => !IsMarginClick,
|
|
OnClick = () => Model.CopySelection()
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => !IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.ToggleBreakpoint,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_ToggleBreakpoint),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.RelAddress != null || ActionLocation.AbsAddress != null,
|
|
OnClick = () => {
|
|
LocationInfo loc = ActionLocation;
|
|
if(loc.AbsAddress != null) {
|
|
BreakpointManager.ToggleBreakpoint(loc.AbsAddress.Value, CpuType);
|
|
} else if(loc.RelAddress != null) {
|
|
BreakpointManager.ToggleBreakpoint(loc.RelAddress.Value, CpuType);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.AddWatch,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_AddToWatch),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.Label != null || ActionLocation.RelAddress != null,
|
|
OnClick = () => {
|
|
LocationInfo loc = ActionLocation;
|
|
if(loc.Label != null) {
|
|
if(loc.LabelAddressOffset != null) {
|
|
WatchManager.GetWatchManager(CpuType).AddWatch($"[{loc.Label.Label}+{loc.LabelAddressOffset}]");
|
|
} else {
|
|
WatchManager.GetWatchManager(CpuType).AddWatch("[" + loc.Label.Label + "]");
|
|
}
|
|
} else if(loc.RelAddress != null) {
|
|
WatchManager.GetWatchManager(CpuType).AddWatch("[$" + loc.RelAddress.Value.Address.ToString(GetFormatString()) + "]");
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.EditLabel,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_EditLabel),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.Label != null || ActionLocation.AbsAddress != null,
|
|
OnClick = () => {
|
|
LocationInfo loc = ActionLocation;
|
|
CodeLabel? label = loc.Label ?? (loc.AbsAddress.HasValue ? LabelManager.GetLabel(loc.AbsAddress.Value) : null);
|
|
if(label != null) {
|
|
LabelEditWindow.EditLabel(CpuType, this, label);
|
|
} else if(loc.AbsAddress != null) {
|
|
LabelEditWindow.EditLabel(CpuType, this, new CodeLabel(loc.AbsAddress.Value));
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.ViewInMemoryViewer,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_ViewInMemoryViewer),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.RelAddress != null || ActionLocation.AbsAddress != null,
|
|
OnClick = () => {
|
|
LocationInfo loc = ActionLocation;
|
|
if(loc.RelAddress != null) {
|
|
MemoryToolsWindow.ShowInMemoryTools(loc.RelAddress.Value.Type, loc.RelAddress.Value.Address);
|
|
} else if(loc.AbsAddress != null) {
|
|
MemoryToolsWindow.ShowInMemoryTools(loc.AbsAddress.Value.Type, loc.AbsAddress.Value.Address);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => !IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.FindOccurrences,
|
|
HintText = () => GetSearchString() ?? "",
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => GetSearchString() != null,
|
|
OnClick = () => {
|
|
if(_model != null) {
|
|
string? searchString = GetSearchString();
|
|
if(searchString != null) {
|
|
DisassemblySearchOptions options = new() { MatchWholeWord = true, MatchCase = true };
|
|
_model.Debugger.FindAllOccurrences(searchString, options);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => !IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.MoveProgramCounter,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_MoveProgramCounter),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.RelAddress != null && DebugApi.GetDebuggerFeatures(CpuType).ChangeProgramCounter,
|
|
OnClick = () => {
|
|
LocationInfo loc = ActionLocation;
|
|
if(loc.RelAddress != null) {
|
|
Model.Debugger.UpdateConsoleState();
|
|
DebugApi.SetProgramCounter(CpuType, (uint)loc.RelAddress.Value.Address);
|
|
Model.Debugger.ConsoleStatus?.UpdateUiState();
|
|
Model.Debugger.UpdateDisassembly(true);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.RunToLocation,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_RunToLocation),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.RelAddress != null || ActionLocation.AbsAddress != null,
|
|
OnClick = () => {
|
|
Model.Debugger.RunToLocation(ActionLocation);
|
|
}
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => !IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.GoToLocation,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_GoToLocation),
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => ActionLocation.Symbol != null || ActionLocation.RelAddress != null,
|
|
OnClick = () => GoToLocation(ActionLocation)
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => !IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.NavigateBack,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_NavigateBack),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => Model.History.CanGoBack(),
|
|
OnClick = () => {
|
|
Model.GoBack();
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.NavigateForward,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.CodeWindow_NavigateForward),
|
|
IsVisible = () => !IsMarginClick,
|
|
IsEnabled = () => Model.History.CanGoForward(),
|
|
OnClick = () => {
|
|
Model.GoForward();
|
|
}
|
|
},
|
|
};
|
|
|
|
actions.AddRange(GetBreakpointContextMenu());
|
|
AddDisposables(DebugShortcutManager.CreateContextMenu(_viewer, actions));
|
|
}
|
|
|
|
private void GoToLocation(LocationInfo loc)
|
|
{
|
|
if(loc.Symbol != null) {
|
|
AddressInfo? addr = DebugWorkspaceManager.SymbolProvider?.GetSymbolAddressInfo(loc.Symbol.Value);
|
|
if(addr?.Address > 0) {
|
|
SourceCodeLocation? srcLoc = DebugWorkspaceManager.SymbolProvider?.GetSourceCodeLineInfo(addr.Value);
|
|
if(srcLoc != null) {
|
|
_model?.ScrollToLocation(srcLoc.Value, true);
|
|
}
|
|
}
|
|
} else if(loc.RelAddress != null) {
|
|
_model?.GoToRelativeAddress(loc.RelAddress.Value.Address, true);
|
|
}
|
|
}
|
|
|
|
private string? GetSearchString()
|
|
{
|
|
CodeSegmentInfo? segment = _selectionHandler?.MouseOverSegment;
|
|
if(segment == null || !AllowSearch(segment.Type)) {
|
|
LocationInfo loc = ActionLocation;
|
|
if(loc.RelAddress?.Address >= 0) {
|
|
CodeLabel? label = LabelManager.GetLabel(loc.RelAddress.Value);
|
|
return label?.Label ?? ("$" + loc.RelAddress.Value.Address.ToString("X" + CpuType.GetAddressSize()));
|
|
}
|
|
return null;
|
|
}
|
|
return segment.Text.Trim(' ', '[', ']', '=', ':', '.', '+');
|
|
}
|
|
|
|
private bool AllowSearch(CodeSegmentType? type)
|
|
{
|
|
if(type == null) {
|
|
return false;
|
|
}
|
|
|
|
switch(type) {
|
|
case CodeSegmentType.OpCode:
|
|
case CodeSegmentType.Token:
|
|
case CodeSegmentType.Address:
|
|
case CodeSegmentType.Label:
|
|
case CodeSegmentType.ImmediateValue:
|
|
case CodeSegmentType.LabelDefinition:
|
|
case CodeSegmentType.EffectiveAddress:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private List<ContextMenuAction> GetBreakpointContextMenu()
|
|
{
|
|
Breakpoint? GetBreakpoint()
|
|
{
|
|
return ActionLocation.AbsAddress != null ? BreakpointManager.GetMatchingBreakpoint(ActionLocation.AbsAddress.Value, CpuType) : null;
|
|
}
|
|
|
|
return new List<ContextMenuAction> {
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.SetBreakpoint,
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => GetBreakpoint() == null && IsMarginClick,
|
|
OnClick = () => {
|
|
if(ActionLocation.AbsAddress != null) {
|
|
BreakpointManager.ToggleBreakpoint(ActionLocation.AbsAddress.Value, CpuType);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => GetBreakpoint() == null && IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.CodeWindowEditBreakpoint,
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => IsMarginClick,
|
|
IsEnabled = () => GetBreakpoint() != null,
|
|
OnClick = () => {
|
|
if(GetBreakpoint() is Breakpoint bp) {
|
|
BreakpointEditWindow.EditBreakpoint(bp, this);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.EnableBreakpoint,
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => GetBreakpoint()?.Enabled == false && IsMarginClick,
|
|
IsEnabled = () => GetBreakpoint()?.Enabled == false,
|
|
OnClick = () => {
|
|
if(ActionLocation.AbsAddress != null) {
|
|
BreakpointManager.EnableDisableBreakpoint(ActionLocation.AbsAddress.Value, CpuType);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.DisableBreakpoint,
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => GetBreakpoint()?.Enabled != false && IsMarginClick,
|
|
IsEnabled = () => GetBreakpoint()?.Enabled == true,
|
|
OnClick = () => {
|
|
if(ActionLocation.AbsAddress != null) {
|
|
BreakpointManager.EnableDisableBreakpoint(ActionLocation.AbsAddress.Value, CpuType);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => GetBreakpoint() != null && IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.RemoveBreakpoint,
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => GetBreakpoint() != null && IsMarginClick,
|
|
OnClick = () => {
|
|
if(ActionLocation.AbsAddress != null) {
|
|
BreakpointManager.ToggleBreakpoint(ActionLocation.AbsAddress.Value, CpuType);
|
|
}
|
|
}
|
|
},
|
|
new ContextMenuSeparator() { IsVisible = () => IsMarginClick },
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.ToggleForbidBreakpoint,
|
|
HintText = () => GetHint(ActionLocation),
|
|
IsVisible = () => IsMarginClick,
|
|
OnClick = () => {
|
|
if(ActionLocation.AbsAddress != null) {
|
|
BreakpointManager.ToggleForbidBreakpoint(ActionLocation.AbsAddress.Value, CpuType);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private string GetFormatString()
|
|
{
|
|
return CpuType.ToMemoryType().GetFormatString();
|
|
}
|
|
|
|
private string GetHint(LocationInfo? loc)
|
|
{
|
|
if(loc == null) {
|
|
return string.Empty;
|
|
}
|
|
|
|
if(loc?.Symbol != null) {
|
|
return loc.Symbol.Value.Name + (loc.LabelAddressOffset > 0 ? ("+" + loc.LabelAddressOffset) : "");
|
|
} else if(loc?.RelAddress != null) {
|
|
return "$" + loc.RelAddress.Value.Address.ToString(GetFormatString());
|
|
} else if(loc?.AbsAddress != null) {
|
|
return "[$" + loc.AbsAddress.Value.Address.ToString(GetFormatString()) + "]";
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
AvaloniaXamlLoader.Load(this);
|
|
}
|
|
|
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
if(this.FindLogicalAncestorOfType<DockableControl>()?.DataContext is BaseToolContainerViewModel parentModel) {
|
|
_parentModel = parentModel;
|
|
_parentModel.Selected += Parent_Selected;
|
|
}
|
|
|
|
_model?.SetViewer(_viewer);
|
|
if(_model?.ActiveAddress != null) {
|
|
//Go to active address when clicking on the source view tab
|
|
_model?.GoToRelativeAddress(_model.ActiveAddress.Value, true);
|
|
}
|
|
FocusViewer();
|
|
}
|
|
|
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
if(_parentModel != null) {
|
|
_parentModel.Selected -= Parent_Selected;
|
|
_parentModel = null;
|
|
}
|
|
|
|
_model?.SetViewer(null);
|
|
}
|
|
|
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
|
{
|
|
base.OnPointerPressed(e);
|
|
|
|
//Navigate on double-click left click
|
|
if(e.Source is DisassemblyViewer) {
|
|
PointerPointProperties props = e.GetCurrentPoint(this).Properties;
|
|
if(_selectionHandler?.IsMarginClick == false && props.IsLeftButtonPressed && e.ClickCount == 2) {
|
|
GoToLocation(ActionLocation);
|
|
} else if(props.IsXButton1Pressed) {
|
|
Model.GoBack();
|
|
} else if(props.IsXButton2Pressed) {
|
|
Model.GoForward();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FocusViewer()
|
|
{
|
|
Dispatcher.UIThread.Post(() => {
|
|
//Focus source view when selected by code
|
|
if(_viewer.IsParentWindowFocused()) {
|
|
_viewer.Focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void Parent_Selected(object? sender, EventArgs e)
|
|
{
|
|
this.FocusViewer();
|
|
}
|
|
}
|
|
}
|