using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.Threading; using DynamicData; using FluentAvalonia.UI.Controls; using Gommon; using LibHac.Ns; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.Systems; using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Systems.AppLibrary; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration.UI; using Ryujinx.Common; using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Common.UI; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL2; using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Windows { public partial class MainWindow : StyleableAppWindow { public MainWindowViewModel ViewModel { get; } internal readonly AvaHostUIHandler UiHandler; private bool _isLoading; private bool _applicationsLoadedOnce; private UserChannelPersistence _userChannelPersistence; private static bool _deferLoad; private static string _launchPath; private static string _launchApplicationId; private static bool _startFullscreen; private IDisposable _appLibraryAppsSubscription; public VirtualFileSystem VirtualFileSystem { get; private set; } public ContentManager ContentManager { get; private set; } public AccountManager AccountManager { get; private set; } public LibHacHorizonManager LibHacHorizonManager { get; private set; } public InputManager InputManager { get; private set; } public SettingsWindow SettingsWindow { get; set; } public static bool ShowKeyErrorOnLoad { get; set; } public ApplicationLibrary ApplicationLibrary { get; set; } // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) public readonly double TitleBarHeight; public readonly double StatusBarHeight; public readonly double MenuBarHeight; public MainWindow() : base(useCustomTitleBar: true) { DataContext = ViewModel = new MainWindowViewModel { Window = this }; InitializeComponent(); Load(); UiHandler = new AvaHostUIHandler(this); ViewModel.Title = RyujinxApp.FormatTitle(); // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. StatusBarHeight = StatusBarView.StatusBar.MinHeight; MenuBarHeight = MenuBar.MinHeight; TitleBar.Height = MenuBarHeight; // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) TitleBarHeight = (ConfigurationState.Instance.ShowOldUI ? TitleBar.Height : 0); ApplicationList.DataContext = DataContext; ApplicationGrid.DataContext = DataContext; SetWindowSizePosition(); if (Program.PreviewerDetached) { InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); _ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it); this.ScalingChanged += OnScalingChanged; } } /// /// Event handler for detecting OS theme change when using "Follow OS theme" option /// private static void OnPlatformColorValuesChanged(object sender, PlatformColorValues e) { if (Application.Current is RyujinxApp app) app.ApplyConfiguredTheme(ConfigurationState.Instance.UI.BaseStyle); } protected override void OnClosed(EventArgs e) { base.OnClosed(e); if (PlatformSettings != null) { PlatformSettings.ColorValuesChanged -= OnPlatformColorValuesChanged; } } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); NotificationHelper.SetNotificationManager(this); Executor.ExecuteBackgroundAsync(async () => { await ShowIntelMacWarningAsync(); FilePath firmwarePath = CommandLineState.FirmwareToInstallPathArg; if (firmwarePath is not null) { if ((firmwarePath.ExistsAsFile && firmwarePath.Extension is "xci" or "zip") || firmwarePath.ExistsAsDirectory) { await Dispatcher.UIThread.InvokeAsync(() => ViewModel.HandleFirmwareInstallation(firmwarePath)); CommandLineState.FirmwareToInstallPathArg = null; } else Logger.Notice.Print(LogClass.UI, "Invalid firmware type provided. Path must be a directory, or a .zip or .xci file."); } }); } private void OnScalingChanged(object sender, EventArgs e) { Program.DesktopScaleFactor = this.RenderScaling; } private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound); Dispatcher.UIThread.Post(() => { ViewModel.StatusBarProgressValue = e.NumAppsLoaded; ViewModel.StatusBarProgressMaximum = e.NumAppsFound; if (e.NumAppsFound == 0) { StatusBarView.LoadProgressBar.IsVisible = false; } if (e.NumAppsLoaded == e.NumAppsFound) { StatusBarView.LoadProgressBar.IsVisible = false; } }); } private void ApplicationLibrary_LdnGameDataReceived(LdnGameDataReceivedEventArgs e) { Dispatcher.UIThread.Post(() => { ViewModel.LdnData.Clear(); foreach (ApplicationData application in ViewModel.Applications.Where(it => it.HasControlHolder)) { ref ApplicationControlProperty controlHolder = ref application.ControlHolder.Value; ViewModel.LdnData[application.IdString] = e.LdnData.Where(ref controlHolder); UpdateApplicationWithLdnData(application); } ViewModel.RefreshView(); }); } private void UpdateApplicationWithLdnData(ApplicationData application) { if (application.HasControlHolder && ViewModel.LdnData.TryGetValue(application.IdString, out LdnGameData.Array ldnGameDatas)) { application.PlayerCount = ldnGameDatas.PlayerCount; application.GameCount = ldnGameDatas.GameCount; } else { application.PlayerCount = 0; application.GameCount = 0; } } public void Application_Opened(object sender, ApplicationOpenedEventArgs args) { if (args.Application != null) { ViewModel.SelectedIcon = args.Application.Icon; ViewModel.LoadApplication(args.Application).Wait(); } args.Handled = true; } internal static void DeferLoadApplication(string launchPathArg, string launchApplicationId, bool startFullscreenArg) { _deferLoad = true; _launchPath = launchPathArg; _launchApplicationId = launchApplicationId; _startFullscreen = startFullscreenArg; } public void SwitchToGameControl(bool startFullscreen = false) { ViewModel.ShowLoadProgress = false; ViewModel.ShowContent = true; ViewModel.IsLoadingIndeterminate = false; if (startFullscreen && ViewModel.WindowState is not WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } } public void ShowLoading(bool startFullscreen = false) { ViewModel.ShowContent = false; ViewModel.ShowLoadProgress = true; ViewModel.IsLoadingIndeterminate = true; if (startFullscreen && ViewModel.WindowState is not WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } } private void Initialize() { _userChannelPersistence = new UserChannelPersistence(); VirtualFileSystem = VirtualFileSystem.CreateInstance(); LibHacHorizonManager = new LibHacHorizonManager(); ContentManager = new ContentManager(VirtualFileSystem); LibHacHorizonManager.InitializeFsServer(VirtualFileSystem); LibHacHorizonManager.InitializeArpServer(); LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, ConfigurationState.Instance.System.IntegrityCheckLevel) { DesiredLanguage = ConfigurationState.Instance.System.Language, }; // Save data created before we supported extra data in directory save data will not work properly if // given empty extra data. Luckily some of that extra data can be created using the data from the // save data indexer, which should be enough to check access permissions for user saves. // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened. // Consider removing this at some point in the future when we don't need to worry about old saves. VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient); AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, CommandLineState.Profile); VirtualFileSystem.ReloadKeySet(); ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient); } [SupportedOSPlatform("linux")] private static async Task ShowVmMaxMapCountWarning() { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary, LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextPrimary], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary] ); } [SupportedOSPlatform("linux")] private static async Task ShowVmMaxMapCountDialog() { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary, LinuxHelper.RecommendedVmMaxMapCount); UserResult response = await ContentDialogHelper.ShowTextDialog( RyujinxApp.FormatTitle(LocaleKeys.LinuxVmMaxMapCountDialogTitle, false), LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonPersistent], LocaleManager.Instance[LocaleKeys.InputDialogNo], (int)Symbol.Help ); int rc; switch (response) { case UserResult.Ok: rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}"); if (rc == 0) { Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart."); } else { Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}"); } break; case UserResult.No: rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}"); if (rc == 0) { Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}"); } else { Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}"); } break; } } private async Task CheckLaunchState() { if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount) { Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})"); if (LinuxHelper.PkExecPath is not null) { await Dispatcher.UIThread.InvokeAsync(ShowVmMaxMapCountDialog); } else { await Dispatcher.UIThread.InvokeAsync(ShowVmMaxMapCountWarning); } } if (!ShowKeyErrorOnLoad) { if (_deferLoad) { _deferLoad = false; if (ApplicationLibrary.TryGetApplicationsFromFile(_launchPath, out List applications)) { ApplicationData applicationData; if (_launchApplicationId != null) { applicationData = applications.FirstOrDefault(application => application.IdString == _launchApplicationId); if (applicationData != null) { ViewModel.SelectedApplication = applicationData; await ViewModel.LoadApplication(applicationData, _startFullscreen); } else { Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{_launchApplicationId}' in '{_launchPath}'."); await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.ApplicationNotFound)); } } else { applicationData = applications[0]; ViewModel.SelectedApplication = applicationData; await ViewModel.LoadApplication(applicationData, _startFullscreen); } } else { Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{_launchPath}'."); await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.ApplicationNotFound)); } } } else { ShowKeyErrorOnLoad = false; await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); } if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates) return; switch (ConfigurationState.Instance.UpdateCheckerType.Value) { case UpdaterType.PromptAtStartup: await Updater.BeginUpdateAsync() .Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}")); break; case UpdaterType.CheckInBackground: if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions)) { Dispatcher.UIThread.Post(() => RyujinxApp.MainWindow.ViewModel.UpdateAvailable = versions.Current < versions.Incoming); } break; } } private void Load() { StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; ApplicationGrid.DataContext = ApplicationList.DataContext = ViewModel; ApplicationGrid.ApplicationOpened += Application_Opened; ApplicationList.ApplicationOpened += Application_Opened; } private void SetWindowSizePosition() { if (!ConfigurationState.Instance.RememberWindowState) { // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight + TitleBarHeight) * Program.WindowScaleFactor; ViewModel.WindowWidth = 1280 * Program.WindowScaleFactor; WindowState = WindowState.Normal; WindowStartupLocation = WindowStartupLocation.CenterScreen; return; } PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX, ConfigurationState.Instance.UI.WindowStartup.WindowPositionY); ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor; ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth * Program.WindowScaleFactor; ViewModel.WindowState = ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal; if (Screens.All.Any(screen => screen.Bounds.Contains(savedPoint))) { Position = savedPoint; } else { Logger.Warning?.Print(LogClass.Application, "Failed to find valid start-up coordinates. Defaulting to primary monitor center."); WindowStartupLocation = WindowStartupLocation.CenterScreen; } } private void SaveWindowSizePosition() { ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value = WindowState == WindowState.Maximized; // Only save rectangle properties if the window is not in a maximized state. if (WindowState != WindowState.Maximized) { // Since scaling is being applied to the loaded settings from disk (see SetWindowSizePosition() above), scaling should be removed from width/height before saving out to disk // as well - otherwise anyone not using a 1.0 scale factor their window will increase in size with every subsequent launch of the program when scaling is applied (Nov. 14, 2024) ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)(Height / Program.WindowScaleFactor); ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)(Width / Program.WindowScaleFactor); ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; } MainWindowViewModel.SaveConfig(); } protected override void OnOpened(EventArgs e) { base.OnOpened(e); Initialize(); PlatformSettings!.ColorValuesChanged += OnPlatformColorValuesChanged; ViewModel.Initialize( ContentManager, StorageProvider, ApplicationLibrary, VirtualFileSystem, AccountManager, InputManager, _userChannelPersistence, LibHacHorizonManager, UiHandler, ShowLoading, SwitchToGameControl, SetMainContent, this); ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; _appLibraryAppsSubscription?.Dispose(); _appLibraryAppsSubscription = ApplicationLibrary.Applications .Connect() .ObserveOn(SynchronizationContext.Current!) .Bind(ViewModel.Applications) .OnItemAdded(UpdateApplicationWithLdnData) .Subscribe(); ApplicationLibrary.LdnGameDataReceived += ApplicationLibrary_LdnGameDataReceived; ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, evt) => { _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); }; ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) => { _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); }; _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); ViewModel.RefreshFirmwareStatus(); // Load applications if no application was requested by the command line if (!_deferLoad) { LoadApplications(); } _ = CheckLaunchState(); } private void SetMainContent(Control content = null) { content ??= GameLibrary; if (MainContent.Content != content) { // Load applications while switching to the GameLibrary if we haven't done that yet if (!_applicationsLoadedOnce && content == GameLibrary) { LoadApplications(); } MainContent.Content = content; } } public static void UpdateGraphicsConfig() { #pragma warning disable IDE0055 // Disable formatting GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale; GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; GraphicsConfig.FixOcclusionCulling = ConfigurationState.Instance.Tweaks.FixOcclusionCulling; #pragma warning restore IDE0055 } private void VolumeStatus_CheckedChanged(object sender, RoutedEventArgs e) { if (ViewModel.IsGameRunning && sender is ToggleSplitButton volumeSplitButton) { if (!volumeSplitButton.IsChecked) { ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute); } else { ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume(); ViewModel.AppHost.Device.SetVolume(0); } ViewModel.Volume = ViewModel.AppHost.Device.GetVolume(); } } protected override void OnClosing(WindowClosingEventArgs e) { if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit) { e.Cancel = true; ConfirmExit(); return; } ViewModel.IsClosing = true; if (ViewModel.AppHost != null) { ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit; ViewModel.AppHost.AppExit += (_, _) => { ViewModel.AppHost = null; Dispatcher.UIThread.Post(() => { MainContent = null; Close(); }); }; ViewModel.AppHost?.Stop(); e.Cancel = true; return; } if (ConfigurationState.Instance.RememberWindowState) { SaveWindowSizePosition(); } ApplicationLibrary.CancelLoading(); InputManager.Dispose(); _appLibraryAppsSubscription?.Dispose(); Program.Exit(); base.OnClosing(e); } private void ConfirmExit() { Dispatcher.UIThread.InvokeAsync(async () => { ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog(); if (ViewModel.IsClosing) { Close(); } }); } public void LoadApplications() { _applicationsLoadedOnce = true; StatusBarView.LoadProgressBar.IsVisible = true; ViewModel.StatusBarProgressMaximum = 0; ViewModel.StatusBarProgressValue = 0; LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); ReloadGameList(); } public void ToggleFileType(string fileType) { switch (fileType) { case "NSP": ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); break; case "PFS0": ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); break; case "XCI": ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); break; case "NCA": ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); break; case "NRO": ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); break; case "NSO": ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); break; default: throw new ArgumentOutOfRangeException(fileType); } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); LoadApplications(); } private void ReloadGameList() { if (_isLoading) { return; } _isLoading = true; Thread applicationLibraryThread = new(() => { ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs); List autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value; autoloadDirs.ForEach(dir => Logger.Info?.Print(LogClass.Application, $"Auto loading DLC & updates from: {dir}")); if (autoloadDirs.Count > 0) { int updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs, out int updatesRemoved); int dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs, out int dlcRemoved); ShowNewContentAddedDialog(dlcLoaded, dlcRemoved, updatesLoaded, updatesRemoved); } _isLoading = false; }) { Name = "GUI.ApplicationLibraryThread", IsBackground = true, }; applicationLibraryThread.Start(); } private void ShowNewContentAddedDialog(int numDlcAdded, int numDlcRemoved, int numUpdatesAdded, int numUpdatesRemoved) { string[] messages = [ numDlcRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcRemovedMessage], numDlcRemoved): null, numDlcAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded): null, numUpdatesRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateRemovedMessage], numUpdatesRemoved): null, numUpdatesAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded) : null ]; string msg = String.Join("\r\n", messages); if (String.IsNullOrWhiteSpace(msg)) return; Dispatcher.UIThread.InvokeAsync(async () => { await ContentDialogHelper.ShowTextDialog( LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, string.Empty, string.Empty, string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); }); } private static bool _intelMacWarningShown = !RunningPlatform.IsIntelMac; public static async Task ShowIntelMacWarningAsync() { if (_intelMacWarningShown) return; await Dispatcher.UIThread.InvokeAsync(async () => await ContentDialogHelper.CreateWarningDialog( "Intel Mac Warning", "Intel Macs are not supported and will not work properly.\nIf you continue, do not come to our Discord asking for support;\nand do not report bugs on the GitHub. They will be closed.")); _intelMacWarningShown = true; } private void InputElement_OnGotFocus(object sender, GotFocusEventArgs e) { if (ViewModel.AppHost is null) return; if (!_focusLoss.Active) return; switch (_focusLoss.Type) { case FocusLostType.BlockInput: { if (!ViewModel.AppHost.NpadManager.InputUpdatesBlocked) { _focusLoss = default; return; } ViewModel.AppHost.NpadManager.UnblockInputUpdates(); _focusLoss = default; break; } case FocusLostType.MuteAudio: { if (!ViewModel.AppHost.Device.IsAudioMuted()) { _focusLoss = default; return; } ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute); _focusLoss = default; break; } case FocusLostType.BlockInputAndMuteAudio: { if (!ViewModel.AppHost.Device.IsAudioMuted()) goto case FocusLostType.BlockInput; ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute); ViewModel.AppHost.NpadManager.UnblockInputUpdates(); _focusLoss = default; break; } case FocusLostType.PauseEmulation: { if (!ViewModel.AppHost.Device.System.IsPaused) { _focusLoss = default; return; } ViewModel.AppHost.Resume(); _focusLoss = default; break; } } } private (FocusLostType Type, bool Active) _focusLoss; private void InputElement_OnLostFocus(object sender, RoutedEventArgs e) { if (ConfigurationState.Instance.FocusLostActionType.Value is FocusLostType.DoNothing) return; if (ViewModel.AppHost is null) return; switch (ConfigurationState.Instance.FocusLostActionType.Value) { case FocusLostType.BlockInput: { if (ViewModel.AppHost.NpadManager.InputUpdatesBlocked) return; ViewModel.AppHost.NpadManager.BlockInputUpdates(); _focusLoss = (FocusLostType.BlockInput, ViewModel.AppHost.NpadManager.InputUpdatesBlocked); break; } case FocusLostType.MuteAudio: { if (ViewModel.AppHost.Device.GetVolume() is 0) return; ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume(); ViewModel.AppHost.Device.SetVolume(0); _focusLoss = (FocusLostType.MuteAudio, ViewModel.AppHost.Device.GetVolume() is 0f); break; } case FocusLostType.BlockInputAndMuteAudio: { if (ViewModel.AppHost.Device.GetVolume() is 0) goto case FocusLostType.BlockInput; ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume(); ViewModel.AppHost.Device.SetVolume(0); ViewModel.AppHost.NpadManager.BlockInputUpdates(); _focusLoss = (FocusLostType.BlockInputAndMuteAudio, ViewModel.AppHost.Device.GetVolume() is 0f && ViewModel.AppHost.NpadManager.InputUpdatesBlocked); break; } case FocusLostType.PauseEmulation: { if (ViewModel.AppHost.Device.System.IsPaused) return; ViewModel.AppHost.Pause(); _focusLoss = (FocusLostType.PauseEmulation, ViewModel.AppHost.Device.System.IsPaused); break; } } } } }