Mesen2/UI/Windows/MainWindow.axaml.cs
2023-01-25 16:03:56 -05:00

548 lines
16 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Mesen.ViewModels;
using Mesen.Config;
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.IO;
using System.Linq;
using Mesen.Utilities;
using Mesen.Interop;
using Mesen.Views;
using Avalonia.Layout;
using Mesen.Debugger.Utilities;
using System.ComponentModel;
using System.Threading;
using Mesen.Debugger.Windows;
using Avalonia.Input.Platform;
using System.Collections.Generic;
namespace Mesen.Windows
{
public class MainWindow : Window
{
private DispatcherTimer _timerBackgroundFlag = new DispatcherTimer();
private MainWindowViewModel _model = null!;
private NotificationListener? _listener = null;
private ShortcutHandler _shortcutHandler;
private FrameInfo _baseScreenSize;
private MouseManager _mouseManager;
private NativeRenderer _renderer;
private ContentControl _audioPlayer;
private MainMenuView _mainMenu;
private CommandLineHelper? _cmdLine;
private bool _testModeEnabled;
//Used to suppress key-repeat keyup events on Linux
private Dictionary<Key, IDisposable> _pendingKeyUpEvents = new();
private bool _isLinux = false;
static MainWindow()
{
WindowStateProperty.Changed.AddClassHandler<MainWindow>((x, e) => x.OnWindowStateChanged());
IsActiveProperty.Changed.AddClassHandler<MainWindow>((x, e) => x.OnActiveChanged());
}
public MainWindow()
{
_testModeEnabled = System.Diagnostics.Debugger.IsAttached;
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
DataContext = new MainWindowViewModel();
InitGlobalShortcuts();
EmuApi.InitDll();
Directory.CreateDirectory(ConfigManager.HomeFolder);
Directory.SetCurrentDirectory(ConfigManager.HomeFolder);
InitializeComponent();
_shortcutHandler = new ShortcutHandler(this);
AddHandler(DragDrop.DropEvent, OnDrop);
//Allows us to catch LeftAlt/RightAlt key presses
AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, true);
AddHandler(InputElement.KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel, true);
_renderer = this.GetControl<NativeRenderer>("Renderer");
_audioPlayer = this.GetControl<ContentControl>("AudioPlayer");
_mainMenu = this.GetControl<MainMenuView>("MainMenu");
_mouseManager = new MouseManager(this, _renderer, _mainMenu);
ConfigManager.Config.MainWindow.LoadWindowSettings(this);
#if DEBUG
this.AttachDevTools();
#endif
}
private static void InitGlobalShortcuts()
{
PlatformHotkeyConfiguration hotkeyConfig = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
List<KeyGesture> gestures = hotkeyConfig.OpenContextMenu;
for(int i = gestures.Count - 1; i >= 0; i--) {
if(gestures[i].Key == Key.F10 && gestures[i].KeyModifiers == KeyModifiers.Shift) {
//Disable Shift-F10 shortcut to open context menu - interferes with default shortcut for step back
gestures.RemoveAt(i);
}
}
hotkeyConfig.Copy.Add(new KeyGesture(Key.Insert, KeyModifiers.Control));
hotkeyConfig.Paste.Add(new KeyGesture(Key.Insert, KeyModifiers.Shift));
hotkeyConfig.Cut.Add(new KeyGesture(Key.Delete, KeyModifiers.Shift));
}
protected override void ArrangeCore(Rect finalRect)
{
//TODOv2 why is this needed to make resizing the window by setting ClientSize work?
base.ArrangeCore(new Rect(ClientSize));
}
private bool _needCloseValidation = true;
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
if(_needCloseValidation) {
e.Cancel = true;
ValidateExit();
} else {
//Close all other windows first
DebugWindowManager.CloseAllWindows();
foreach(Window wnd in ApplicationHelper.GetOpenedWindows()) {
if(wnd != this) {
wnd.Close();
}
}
if(ApplicationHelper.GetOpenedWindows().Count > 1) {
e.Cancel = true;
return;
}
_timerBackgroundFlag.Stop();
EmuApi.Stop();
ConfigManager.Config.MainWindow.SaveWindowSettings(this);
ConfigManager.Config.Save();
}
}
private async void ValidateExit()
{
if(!ConfigManager.Config.Preferences.ConfirmExitResetPower || await MesenMsgBox.Show(null, "ConfirmExit", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) {
_needCloseValidation = false;
Close();
}
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_mouseManager.Dispose();
}
protected override void OnDataContextChanged(EventArgs e)
{
if(DataContext is MainWindowViewModel model) {
_model = model;
}
}
private void ResizeRenderer()
{
_renderer.InvalidateMeasure();
_renderer.InvalidateArrange();
}
private void OnDrop(object? sender, DragEventArgs e)
{
string? filename = e.Data.GetFileNames()?.FirstOrDefault();
if(filename != null) {
if(File.Exists(filename)) {
LoadRomHelper.LoadFile(filename);
Activate();
} else {
EmuApi.DisplayMessage("Error", "File not found: " + filename);
}
}
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if(Design.IsDesignMode) {
return;
}
ConfigManager.Config.Preferences.ApplyFontOptions();
ConfigManager.Config.Debug.Fonts.ApplyConfig();
_timerBackgroundFlag.Interval = TimeSpan.FromMilliseconds(200);
_timerBackgroundFlag.Tick += timerUpdateBackgroundFlag;
_timerBackgroundFlag.Start();
Task.Run(() => {
//Load all styles after 15ms to let the UI refresh once with the startup styles
System.Threading.Thread.Sleep(15);
Dispatcher.UIThread.Post(() => {
StyleHelper.ApplyTheme(ConfigManager.ActiveTheme);
});
});
Task.Run(() => {
_cmdLine = new CommandLineHelper(Program.CommandLineArgs, true);
EmuApi.InitializeEmu(ConfigManager.HomeFolder, PlatformImpl?.Handle.Handle ?? IntPtr.Zero, _renderer.Handle, _cmdLine.NoAudio, _cmdLine.NoVideo, _cmdLine.NoInput);
ConfigManager.Config.RemoveObsoleteConfig();
//InitializeDefaults must be after InitializeEmu, otherwise keybindings will be empty
ConfigManager.Config.InitializeDefaults();
_baseScreenSize = EmuApi.GetBaseScreenSize();
_listener = new NotificationListener();
_listener.OnNotification += OnNotification;
_model.Init(this);
ConfigManager.Config.ApplyConfig();
if(ConfigManager.Config.Preferences.OverrideGameFolder && Directory.Exists(ConfigManager.Config.Preferences.GameFolder)) {
EmuApi.AddKnownGameFolder(ConfigManager.Config.Preferences.GameFolder);
}
foreach(RecentItem recentItem in ConfigManager.Config.RecentFiles.Items) {
EmuApi.AddKnownGameFolder(recentItem.RomFile.Folder);
}
ConfigManager.Config.Preferences.UpdateFileAssociations();
SingleInstance.Instance.ArgumentsReceived += Instance_ArgumentsReceived;
Dispatcher.UIThread.Post(() => {
_cmdLine.LoadFiles();
_cmdLine?.OnAfterInit(this);
//Load the debugger window styles once everything else is done
StyleHelper.LoadDebuggerStyles();
if(ConfigManager.Config.Preferences.AutomaticallyCheckForUpdates) {
_model.MainMenu.CheckForUpdate(this, true);
}
});
});
}
private void Instance_ArgumentsReceived(object? sender, ArgumentsReceivedEventArgs e)
{
Dispatcher.UIThread.Post(() => {
CommandLineHelper cmdLine = new(e.Args, false);
cmdLine.LoadFiles();
});
}
private void OnNotification(NotificationEventArgs e)
{
DebugWindowManager.ProcessNotification(e);
switch(e.NotificationType) {
case ConsoleNotificationType.GameLoaded:
CheatCodes.ApplyCheats();
RomInfo romInfo = EmuApi.GetRomInfo();
GameConfig.LoadGameConfig(romInfo).ApplyConfig();
GameLoadedEventParams evtParams = Marshal.PtrToStructure<GameLoadedEventParams>(e.Parameter);
if(!evtParams.IsPowerCycle) {
Dispatcher.UIThread.Post(() => {
_model.RecentGames.Visible = false;
_model.RomInfo = romInfo;
DispatcherTimer.RunOnce(() => {
if(_cmdLine != null) {
_cmdLine?.ProcessPostLoadCommandSwitches(this);
_cmdLine = null;
}
if(WindowState == WindowState.FullScreen || WindowState == WindowState.Maximized) {
//Force resize of renderer when loading a game while in fullscreen
//Prevents some issues when fullscreen was turned on before loading a game, etc.
_renderer.Width = double.NaN;
_renderer.Height = double.NaN;
ResizeRenderer();
}
}, TimeSpan.FromMilliseconds(50));
});
}
Dispatcher.UIThread.Post(() => {
ApplicationHelper.GetExistingWindow<HdPackBuilderWindow>()?.Close();
});
break;
case ConsoleNotificationType.DebuggerResumed:
case ConsoleNotificationType.GameResumed:
Dispatcher.UIThread.Post(() => {
_model.RecentGames.Visible = false;
});
break;
case ConsoleNotificationType.RequestConfigChange:
Dispatcher.UIThread.Post(() => {
UpdateInputConfiguration();
});
break;
case ConsoleNotificationType.EmulationStopped:
Dispatcher.UIThread.Post(() => {
_model.RomInfo = new RomInfo();
_model.RecentGames.Init(GameScreenMode.RecentGames);
});
break;
case ConsoleNotificationType.ResolutionChanged:
Dispatcher.UIThread.Post(() => {
ProcessResolutionChange();
});
break;
case ConsoleNotificationType.ExecuteShortcut:
ExecuteShortcutParams p = Marshal.PtrToStructure<ExecuteShortcutParams>(e.Parameter);
Dispatcher.UIThread.Post(() => {
_shortcutHandler.ExecuteShortcut(p.Shortcut);
});
break;
case ConsoleNotificationType.MissingFirmware:
MissingFirmwareMessage msg = Marshal.PtrToStructure<MissingFirmwareMessage>(e.Parameter);
TaskCompletionSource tcs = new TaskCompletionSource();
Dispatcher.UIThread.Post(async () => {
await FirmwareHelper.RequestFirmwareFile(msg);
tcs.SetResult();
});
tcs.Task.Wait();
break;
case ConsoleNotificationType.BeforeGameLoad:
Dispatcher.UIThread.Post(() => {
ApplicationHelper.GetExistingWindow<HdPackBuilderWindow>()?.Close();
});
break;
}
}
private static void UpdateInputConfiguration()
{
//Used to update input devices when the core requests changes (NES-only for now)
ConfigManager.Config.Nes.UpdateInputFromCoreConfig();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ProcessResolutionChange()
{
double dpiScale = LayoutHelper.GetLayoutScale(this);
double xScale = ClientSize.Width * dpiScale / _baseScreenSize.Width;
double yScale = ClientSize.Height * dpiScale / _baseScreenSize.Height;
SetScale(Math.Min(Math.Round(xScale), Math.Round(yScale)));
_baseScreenSize = EmuApi.GetBaseScreenSize();
}
public void SetScale(double scale)
{
//TODOv2 - Calling this twice seems to fix what might be an issue in Avalonia?
//On the first call, when DPI > 100%, sometimes the "finalSize" received by
//ArrangeOverride in NativeRenderer does not match what was given here
InternalSetScale(scale);
InternalSetScale(scale);
}
private void InternalSetScale(double scale)
{
double dpiScale = LayoutHelper.GetLayoutScale(this);
scale /= dpiScale;
FrameInfo screenSize = EmuApi.GetBaseScreenSize();
if(WindowState == WindowState.Normal) {
_renderer.Width = double.NaN;
_renderer.Height = double.NaN;
double aspectRatio = EmuApi.GetAspectRatio();
//When menu is set to auto-hide, don't count its height when calculating the window's final size
double menuHeight = ConfigManager.Config.Preferences.AutoHideMenu ? 0 : _mainMenu.Bounds.Height;
double width = Math.Max(MinWidth, Math.Round(screenSize.Height * scale * aspectRatio));
double height = Math.Max(MinHeight, Math.Round(screenSize.Height * scale));
ClientSize = new Size(width, height + menuHeight + _audioPlayer.Bounds.Height);
ResizeRenderer();
} else if(WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen) {
_renderer.Width = Math.Round(screenSize.Width * scale);
_renderer.Height = Math.Round(screenSize.Height * scale);
}
}
private void OnWindowStateChanged()
{
if(WindowState == WindowState.Normal) {
_renderer.Width = double.NaN;
_renderer.Height = double.NaN;
ResizeRenderer();
}
}
public void ToggleFullscreen()
{
if(WindowState == WindowState.FullScreen) {
WindowState = WindowState.Normal;
if(ConfigManager.Config.Video.UseExclusiveFullscreen) {
EmuApi.SetExclusiveFullscreenMode(false, _renderer.Handle);
}
RestoreOriginalWindowSize();
} else {
_originalSize = ClientSize;
_originalPos = Position;
if(ConfigManager.Config.Video.UseExclusiveFullscreen) {
if(!EmuApi.IsRunning()) {
//Prevent entering fullscreen mode until a game is loaded
return;
}
EmuApi.SetExclusiveFullscreenMode(true, PlatformImpl?.Handle.Handle ?? IntPtr.Zero);
}
WindowState = WindowState.FullScreen;
}
}
private void RestoreOriginalWindowSize()
{
Task.Run(() => {
Thread.Sleep(30);
Dispatcher.UIThread.Post(() => {
if(WindowState == WindowState.Normal) {
ClientSize = _originalSize;
Position = _originalPos;
}
});
Thread.Sleep(100);
Dispatcher.UIThread.Post(() => {
if(WindowState == WindowState.Normal) {
ClientSize = _originalSize;
Position = _originalPos;
}
});
});
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
if(WindowState == WindowState.FullScreen && ConfigManager.Config.Video.UseExclusiveFullscreen) {
ToggleFullscreen();
}
}
private bool ProcessTestModeShortcuts(Key key)
{
if(key == Key.F1) {
if(TestApi.RomTestRecording()) {
TestApi.RomTestStop();
} else {
RomTestHelper.RecordTest();
}
return true;
} else if(key == Key.F2) {
RomTestHelper.RunTest();
return true;
} else if(key == Key.F3) {
RomTestHelper.RunAllTests();
return true;
}
return false;
}
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
{
if(_testModeEnabled && e.KeyModifiers == KeyModifiers.Alt && ProcessTestModeShortcuts(e.Key)) {
return;
}
if(e.Key != Key.None) {
if(_isLinux && _pendingKeyUpEvents.TryGetValue(e.Key, out IDisposable? cancelTimer)) {
//Cancel any pending key up event
cancelTimer.Dispose();
_pendingKeyUpEvents.Remove(e.Key);
}
InputApi.SetKeyState((UInt16)e.Key, true);
}
if(e.Key == Key.Tab || e.Key == Key.F10) {
//Prevent menu/window from handling these keys to avoid issue with custom shortcuts
e.Handled = true;
}
}
private void OnPreviewKeyUp(object? sender, KeyEventArgs e)
{
if(e.Key != Key.None) {
if(_isLinux) {
//Process keyup events after 1ms on Linux to prevent key repeat from triggering key up/down repeatedly
IDisposable cancelTimer = DispatcherTimer.RunOnce(() => InputApi.SetKeyState((UInt16)e.Key, false), TimeSpan.FromMilliseconds(1), DispatcherPriority.MaxValue);
_pendingKeyUpEvents[e.Key] = cancelTimer;
} else {
InputApi.SetKeyState((UInt16)e.Key, false);
}
}
}
private void OnActiveChanged()
{
ConfigApi.SetEmulationFlag(EmulationFlags.InBackground, !IsActive);
InputApi.ResetKeyState();
}
private bool _needResume = false;
private Size _originalSize;
private PixelPoint _originalPos;
private void timerUpdateBackgroundFlag(object? sender, EventArgs e)
{
Window? activeWindow = ApplicationHelper.GetActiveWindow();
PreferencesConfig cfg = ConfigManager.Config.Preferences;
bool needPause = activeWindow == null && cfg.PauseWhenInBackground;
if(activeWindow != null) {
bool isConfigWindow = (activeWindow != this) && !DebugWindowManager.IsDebugWindow(activeWindow);
needPause |= cfg.PauseWhenInMenusAndConfig && !isConfigWindow && _mainMenu.MainMenu.IsOpen; //in main menu
needPause |= cfg.PauseWhenInMenusAndConfig && isConfigWindow; //in a window that's neither the main window nor a debug tool
}
if(needPause) {
if(!EmuApi.IsPaused()) {
_needResume = true;
DebuggerWindow? wnd = DebugWindowManager.GetDebugWindow<DebuggerWindow>(x => x.CpuType == _model.RomInfo.ConsoleType.GetMainCpuType());
if(wnd != null) {
//If the debugger window for the main cpu is opened, suppress the "bring to front on break" behavior
wnd.SuppressBringToFront();
}
EmuApi.Pause();
}
} else if(_needResume) {
EmuApi.Resume();
_needResume = false;
}
}
}
}