mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
732 lines
23 KiB
C#
732 lines
23 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.Threading;
|
|
using Mesen.Debugger.Windows;
|
|
using Avalonia.Input.Platform;
|
|
using System.Collections.Generic;
|
|
using Mesen.Controls;
|
|
using Mesen.Localization;
|
|
using System.Diagnostics;
|
|
using Avalonia.VisualTree;
|
|
using System.Text;
|
|
|
|
namespace Mesen.Windows
|
|
{
|
|
public class MainWindow : MesenWindow
|
|
{
|
|
private DispatcherTimer _timerBackgroundFlag = new DispatcherTimer();
|
|
private MainWindowViewModel _model = null!;
|
|
|
|
private NotificationListener? _listener = null;
|
|
private ShortcutHandler _shortcutHandler;
|
|
|
|
private MouseManager _mouseManager;
|
|
private ContentControl _audioPlayer;
|
|
private MainMenuView _mainMenu;
|
|
private CommandLineHelper? _cmdLine;
|
|
|
|
private bool _testModeEnabled;
|
|
private bool _needResume = false;
|
|
private bool _needCloseValidation = true;
|
|
|
|
private bool _preventFullscreenToggle = false;
|
|
|
|
private Panel _rendererPanel;
|
|
private NativeRenderer _renderer;
|
|
private SoftwareRendererView _softwareRenderer;
|
|
private Size _rendererSize;
|
|
private bool _usesSoftwareRenderer;
|
|
|
|
private FrameInfo _prevScreenSize;
|
|
|
|
private Size _originalSize;
|
|
private PixelPoint _originalPos;
|
|
private WindowState _prevWindowState;
|
|
|
|
//Used to suppress key-repeat keyup events on Linux
|
|
private Dictionary<Key, IDisposable> _pendingKeyUpEvents = new();
|
|
private bool _isLinux = false;
|
|
|
|
private Stopwatch _stopWatch = Stopwatch.StartNew();
|
|
private Dictionary<Key, long> _keyPressedStamp = new();
|
|
private bool _focusInMenu;
|
|
|
|
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 = OperatingSystem.IsLinux();
|
|
_usesSoftwareRenderer = ConfigManager.Config.Video.UseSoftwareRenderer || OperatingSystem.IsMacOS();
|
|
|
|
_model = new MainWindowViewModel();
|
|
DataContext = _model;
|
|
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);
|
|
|
|
_rendererPanel = this.GetControl<Panel>("RendererPanel");
|
|
_rendererPanel.LayoutUpdated += RendererPanel_LayoutUpdated;
|
|
|
|
_renderer = this.GetControl<NativeRenderer>("Renderer");
|
|
_softwareRenderer = this.GetControl<SoftwareRendererView>("SoftwareRenderer");
|
|
_audioPlayer = this.GetControl<ContentControl>("AudioPlayer");
|
|
_mainMenu = this.GetControl<MainMenuView>("MainMenu");
|
|
_mouseManager = new MouseManager(this, _usesSoftwareRenderer ? _softwareRenderer : _renderer, _mainMenu, _usesSoftwareRenderer);
|
|
ConfigManager.Config.MainWindow.LoadWindowSettings(this);
|
|
|
|
#if DEBUG
|
|
this.AttachDevTools();
|
|
#endif
|
|
}
|
|
|
|
private static void InitGlobalShortcuts()
|
|
{
|
|
if(Application.Current?.PlatformSettings == null) {
|
|
return;
|
|
}
|
|
|
|
PlatformHotkeyConfiguration hotkeyConfig = Application.Current.PlatformSettings.HotkeyConfiguration;
|
|
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));
|
|
}
|
|
|
|
protected override void OnClosing(WindowClosingEventArgs 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();
|
|
_listener?.Dispose();
|
|
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();
|
|
}
|
|
|
|
private void OnDrop(object? sender, DragEventArgs e)
|
|
{
|
|
string? filename = e.Data.GetFiles()?.FirstOrDefault()?.Path.LocalPath;
|
|
if(filename != null) {
|
|
if(File.Exists(filename)) {
|
|
LoadRomHelper.LoadFile(filename);
|
|
Activate();
|
|
} else {
|
|
DisplayMessageHelper.DisplayMessage("Error", ResourceHelper.GetMessage("FileNotFound", filename));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnOpened(EventArgs e)
|
|
{
|
|
base.OnOpened(e);
|
|
|
|
if(Design.IsDesignMode) {
|
|
return;
|
|
}
|
|
|
|
ConfigManager.Config.InitializeFontDefaults();
|
|
ConfigManager.Config.Preferences.ApplyFontOptions();
|
|
ConfigManager.Config.Debug.Fonts.ApplyConfig();
|
|
|
|
_timerBackgroundFlag.Interval = TimeSpan.FromMilliseconds(100);
|
|
_timerBackgroundFlag.Tick += timerUpdateBackgroundFlag;
|
|
_timerBackgroundFlag.Start();
|
|
|
|
//Give focus to panel to avoid menu being given focus by default
|
|
this.GetControl<Panel>("RendererPanel").Focus();
|
|
|
|
//Focus on the recent games dialog if it's visible
|
|
//This also enables keyboard/gamepad navigation on the selection screen without having to click it first
|
|
this.FindDescendantOfType<StateGrid>()?.Focus();
|
|
|
|
Task.Run(() => {
|
|
CommandLineHelper cmdLine = new CommandLineHelper(Program.CommandLineArgs, true);
|
|
_cmdLine = cmdLine;
|
|
|
|
EmuApi.InitializeEmu(
|
|
ConfigManager.HomeFolder,
|
|
TryGetPlatformHandle()?.Handle ?? IntPtr.Zero,
|
|
_renderer.Handle,
|
|
_usesSoftwareRenderer,
|
|
cmdLine.NoAudio,
|
|
cmdLine.NoVideo,
|
|
cmdLine.NoInput
|
|
);
|
|
|
|
ConfigManager.Config.RemoveObsoleteConfig();
|
|
|
|
//InitializeDefaults must be after InitializeEmu, otherwise keybindings will be empty
|
|
ConfigManager.Config.InitializeDefaults();
|
|
ConfigManager.Config.UpgradeConfig();
|
|
|
|
_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);
|
|
|
|
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);
|
|
ConfigManager.Config.ApplyConfig();
|
|
cmdLine.LoadFiles();
|
|
});
|
|
}
|
|
|
|
private void OnNotification(NotificationEventArgs e)
|
|
{
|
|
DebugWindowManager.ProcessNotification(e);
|
|
|
|
switch(e.NotificationType) {
|
|
case ConsoleNotificationType.GameLoaded:
|
|
CheatCodes.ApplyCheats();
|
|
RomInfo romInfo = EmuApi.GetRomInfo();
|
|
|
|
Dispatcher.UIThread.Post(() => {
|
|
bool wasAudioFile = _model.AudioPlayer != null;
|
|
bool updateConfig = _model.RomInfo.Format != romInfo.Format;
|
|
_model.RomInfo = romInfo;
|
|
|
|
if(updateConfig) {
|
|
//Make sure any config overrides (video filter/aspect ratio) are applied when loading a different file
|
|
ConfigManager.Config.Video.ApplyConfig();
|
|
}
|
|
|
|
bool isAudioFile = _model.AudioPlayer != null;
|
|
if(wasAudioFile != isAudioFile) {
|
|
//Force window size update when switching between an audio file and a regular rom
|
|
Dispatcher.UIThread.Post(() => {
|
|
ProcessResolutionChange();
|
|
});
|
|
}
|
|
});
|
|
|
|
GameConfig.LoadGameConfig(romInfo).ApplyConfig();
|
|
|
|
GameLoadedEventParams evtParams = Marshal.PtrToStructure<GameLoadedEventParams>(e.Parameter);
|
|
if(!evtParams.IsPowerCycle) {
|
|
Dispatcher.UIThread.Post(() => {
|
|
_model.RecentGames.Visible = false;
|
|
if(IsKeyboardFocusWithin) {
|
|
this.GetControl<Panel>("RendererPanel").Focus();
|
|
}
|
|
|
|
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.
|
|
_rendererSize = new Size();
|
|
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;
|
|
if(IsKeyboardFocusWithin) {
|
|
this.GetControl<Panel>("RendererPanel").Focus();
|
|
}
|
|
});
|
|
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.SufamiTurboFilePrompt: {
|
|
SufamiTurboFilePromptMessage msg = Marshal.PtrToStructure<SufamiTurboFilePromptMessage>(e.Parameter);
|
|
TaskCompletionSource tcs = new TaskCompletionSource();
|
|
Dispatcher.UIThread.Post(async () => {
|
|
if(await MesenMsgBox.Show(this, "PromptLoadSufamiTurbo", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) {
|
|
string? selectedFile = await FileDialogHelper.OpenFile(null, this, FileDialogHelper.SufamiTurboExt);
|
|
if(selectedFile != null) {
|
|
byte[] file = Encoding.UTF8.GetBytes(selectedFile);
|
|
Array.Copy(file, msg.Filename, file.Length);
|
|
Marshal.StructureToPtr<SufamiTurboFilePromptMessage>(msg, e.Parameter, false);
|
|
}
|
|
}
|
|
tcs.SetResult();
|
|
});
|
|
tcs.Task.Wait();
|
|
break;
|
|
}
|
|
|
|
case ConsoleNotificationType.BeforeGameLoad:
|
|
Dispatcher.UIThread.Post(() => {
|
|
ApplicationHelper.GetExistingWindow<HdPackBuilderWindow>()?.Close();
|
|
});
|
|
break;
|
|
|
|
case ConsoleNotificationType.RefreshSoftwareRenderer:
|
|
SoftwareRendererFrame frame = Marshal.PtrToStructure<SoftwareRendererFrame>(e.Parameter);
|
|
_softwareRenderer.UpdateSoftwareRenderer(frame);
|
|
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);
|
|
FrameInfo baseScreenSize = EmuApi.GetBaseScreenSize();
|
|
if(WindowState == WindowState.Normal) {
|
|
double menuHeight = ConfigManager.Config.Preferences.AutoHideMenu ? 0 : _mainMenu.Bounds.Height;
|
|
double height = ClientSize.Height - menuHeight - _audioPlayer.Bounds.Height;
|
|
if(baseScreenSize.Width == _prevScreenSize.Height && baseScreenSize.Height == _prevScreenSize.Width) {
|
|
//Rotation, swap sizes without changing scale
|
|
double xScale = ClientSize.Width * dpiScale / _prevScreenSize.Width;
|
|
double yScale = height * dpiScale / _prevScreenSize.Height;
|
|
SetScale(Math.Min(Math.Round(xScale), Math.Round(yScale)));
|
|
} else {
|
|
double xScale = ClientSize.Width * dpiScale / baseScreenSize.Width;
|
|
double yScale = height * dpiScale / baseScreenSize.Height;
|
|
SetScale(Math.Min(Math.Round(xScale), Math.Round(yScale)));
|
|
}
|
|
} else if(WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen) {
|
|
if(_rendererSize == default) {
|
|
ResizeRenderer();
|
|
} else {
|
|
double xScale = _rendererSize.Width * dpiScale / baseScreenSize.Width;
|
|
double yScale = _rendererSize.Height * dpiScale / baseScreenSize.Height;
|
|
SetScale(Math.Min(Math.Round(xScale, 2), Math.Round(yScale, 2)));
|
|
}
|
|
}
|
|
_prevScreenSize = baseScreenSize;
|
|
}
|
|
|
|
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 _rendererPanel's bounds are incorrect
|
|
InternalSetScale(scale);
|
|
InternalSetScale(scale);
|
|
}
|
|
|
|
private void InternalSetScale(double scale)
|
|
{
|
|
double dpiScale = LayoutHelper.GetLayoutScale(this);
|
|
double aspectRatio = EmuApi.GetAspectRatio();
|
|
|
|
FrameInfo screenSize = EmuApi.GetBaseScreenSize();
|
|
if(WindowState == WindowState.Normal) {
|
|
_rendererSize = new Size();
|
|
|
|
|
|
//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 * aspectRatio * scale) / dpiScale);
|
|
double height = Math.Max(MinHeight, screenSize.Height * scale / dpiScale);
|
|
ClientSize = new Size(width, height + menuHeight + _audioPlayer.Bounds.Height);
|
|
ResizeRenderer();
|
|
} else if(WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen) {
|
|
_rendererSize = new Size(Math.Round(screenSize.Width * scale * aspectRatio) / dpiScale, Math.Round(screenSize.Height * scale) / dpiScale);
|
|
ResizeRenderer();
|
|
}
|
|
}
|
|
|
|
private void ResizeRenderer()
|
|
{
|
|
_rendererPanel.InvalidateMeasure();
|
|
_rendererPanel.InvalidateArrange();
|
|
}
|
|
|
|
private void RendererPanel_LayoutUpdated(object? sender, EventArgs e)
|
|
{
|
|
double aspectRatio = EmuApi.GetAspectRatio();
|
|
double dpiScale = LayoutHelper.GetLayoutScale(this);
|
|
|
|
Size finalSize = _rendererSize == default ? _rendererPanel.Bounds.Size : _rendererSize;
|
|
double height = finalSize.Height;
|
|
double width = finalSize.Height * aspectRatio;
|
|
if(Math.Round(width) > Math.Round(finalSize.Width)) {
|
|
//Use renderer width to calculate the height instead of the opposite
|
|
//when current window dimensions would cause cropping horizontally
|
|
//if the screen width was calculated based on the height.
|
|
width = finalSize.Width;
|
|
height = width / aspectRatio;
|
|
}
|
|
|
|
if(ConfigManager.Config.Video.FullscreenForceIntegerScale && VisualRoot is Window wnd && (wnd.WindowState == WindowState.FullScreen || wnd.WindowState == WindowState.Maximized)) {
|
|
FrameInfo baseSize = EmuApi.GetBaseScreenSize();
|
|
double scale = height * dpiScale / baseSize.Height;
|
|
if(scale != Math.Floor(scale)) {
|
|
height = baseSize.Height * Math.Max(1, Math.Floor(scale / dpiScale));
|
|
width = height * aspectRatio;
|
|
}
|
|
}
|
|
|
|
uint realWidth = (uint)Math.Round(width * dpiScale);
|
|
uint realHeight = (uint)Math.Round(height * dpiScale);
|
|
EmuApi.SetRendererSize(realWidth, realHeight);
|
|
_model.RendererSize = new Size(realWidth, realHeight);
|
|
|
|
_renderer.Width = width;
|
|
_renderer.Height = height;
|
|
_model.SoftwareRenderer.Width = width;
|
|
_model.SoftwareRenderer.Height = height;
|
|
}
|
|
|
|
private void OnWindowStateChanged()
|
|
{
|
|
_rendererSize = new Size();
|
|
ResizeRenderer();
|
|
}
|
|
|
|
public void ToggleFullscreen()
|
|
{
|
|
if(_preventFullscreenToggle) {
|
|
return;
|
|
}
|
|
|
|
_preventFullscreenToggle = true;
|
|
if(WindowState == WindowState.FullScreen) {
|
|
Task.Run(() => {
|
|
if(ConfigManager.Config.Video.UseExclusiveFullscreen) {
|
|
EmuApi.SetExclusiveFullscreenMode(false, _renderer.Handle);
|
|
}
|
|
|
|
Dispatcher.UIThread.Post(() => {
|
|
WindowState = _prevWindowState;
|
|
if(_prevWindowState == WindowState.Normal) {
|
|
ClientSize = _originalSize;
|
|
Position = _originalPos;
|
|
}
|
|
_preventFullscreenToggle = false;
|
|
});
|
|
});
|
|
} else {
|
|
_originalSize = ClientSize;
|
|
_originalPos = Position;
|
|
_prevWindowState = WindowState;
|
|
|
|
if(ConfigManager.Config.Video.UseExclusiveFullscreen) {
|
|
if(!EmuApi.IsRunning()) {
|
|
//Prevent entering fullscreen mode until a game is loaded
|
|
_preventFullscreenToggle = false;
|
|
return;
|
|
}
|
|
|
|
Task.Run(() => {
|
|
EmuApi.SetExclusiveFullscreenMode(true, TryGetPlatformHandle()?.Handle ?? IntPtr.Zero);
|
|
_preventFullscreenToggle = false;
|
|
|
|
Dispatcher.UIThread.Post(() => {
|
|
WindowState = WindowState.FullScreen;
|
|
});
|
|
});
|
|
} else {
|
|
WindowState = WindowState.FullScreen;
|
|
_preventFullscreenToggle = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
} else if(key == Key.F7) {
|
|
RomTestHelper.RunGbMicroTests();
|
|
return true;
|
|
} else if(key == Key.F8) {
|
|
RomTestHelper.RunGambatteTests();
|
|
return true;
|
|
} else if(key == Key.F6) {
|
|
//For testing purposes (to test for memory leaks)
|
|
Task.Run(() => {
|
|
for(int i = 0; i < 50; i++) {
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
Thread.Sleep(10);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
if(_testModeEnabled && e.KeyModifiers == KeyModifiers.Alt && ProcessTestModeShortcuts(e.Key)) {
|
|
return;
|
|
}
|
|
|
|
if(OperatingSystem.IsMacOS()) {
|
|
//Keyhandler handles key internally on macOS
|
|
return;
|
|
}
|
|
|
|
if(_focusInMenu) {
|
|
return;
|
|
}
|
|
|
|
if(e.Key != Key.None) {
|
|
_keyPressedStamp[e.Key] = _stopWatch.ElapsedTicks;
|
|
|
|
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(OperatingSystem.IsMacOS()) {
|
|
//Keyhandler handles key internally on macOS
|
|
return;
|
|
}
|
|
|
|
if(e.Key != Key.None) {
|
|
if(e.Key.IsSpecialKey() && (!_keyPressedStamp.TryGetValue(e.Key, out long stamp) || ((_stopWatch.ElapsedTicks - stamp) * 1000 / Stopwatch.Frequency) < 10)) {
|
|
//Key up received without key down, or key pressed for less than 10 ms, pretend the key was pressed for 50ms
|
|
//Some special keys can behave this way (e.g printscreen)
|
|
InputApi.SetKeyState((UInt16)e.Key, true);
|
|
DispatcherTimer.RunOnce(() => InputApi.SetKeyState((UInt16)e.Key, false), TimeSpan.FromMilliseconds(50), DispatcherPriority.MaxValue);
|
|
_keyPressedStamp.Remove(e.Key);
|
|
return;
|
|
}
|
|
|
|
_keyPressedStamp.Remove(e.Key);
|
|
|
|
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 void timerUpdateBackgroundFlag(object? sender, EventArgs e)
|
|
{
|
|
Window? activeWindow = ApplicationHelper.GetActiveWindow();
|
|
|
|
PreferencesConfig cfg = ConfigManager.Config.Preferences;
|
|
|
|
bool focusInMenu = MenuHelper.IsFocusInMenu(_mainMenu.MainMenu);
|
|
if(focusInMenu && !_focusInMenu) {
|
|
InputApi.ResetKeyState();
|
|
}
|
|
_focusInMenu = focusInMenu;
|
|
|
|
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) {
|
|
//Don't resume if the load/save state dialog is opened
|
|
if(!_model.RecentGames.Visible) {
|
|
EmuApi.Resume();
|
|
_needResume = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|