diff options
Diffstat (limited to 'src/Ryujinx/UI/Windows/MainWindow.axaml.cs')
-rw-r--r-- | src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 551 |
1 files changed, 551 insertions, 0 deletions
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs new file mode 100644 index 00000000..33a9af5b --- /dev/null +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -0,0 +1,551 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Applet; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Logging; +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 Ryujinx.Modules; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using System; +using System.IO; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class MainWindow : StyleableWindow + { + internal static MainWindowViewModel MainWindowViewModel { get; private set; } + + private bool _isLoading; + + private UserChannelPersistence _userChannelPersistence; + private static bool _deferLoad; + private static string _launchPath; + private static bool _startFullscreen; + internal readonly AvaHostUIHandler UiHandler; + + 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; } + + internal MainWindowViewModel ViewModel { get; private set; } + public SettingsWindow SettingsWindow { get; set; } + + public static bool ShowKeyErrorOnLoad { get; set; } + public ApplicationLibrary ApplicationLibrary { get; set; } + + public MainWindow() + { + ViewModel = new MainWindowViewModel(); + + MainWindowViewModel = ViewModel; + + DataContext = ViewModel; + + SetWindowSizePosition(); + + InitializeComponent(); + Load(); + + UiHandler = new AvaHostUIHandler(this); + + ViewModel.Title = $"Ryujinx {Program.Version}"; + + // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. + double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight; + Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; + Width /= Program.WindowScaleFactor; + + if (Program.PreviewerDetached) + { + InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); + + this.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged); + this.ScalingChanged += OnScalingChanged; + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + NotificationHelper.SetNotificationManager(this); + } + + private void IsActiveChanged(bool obj) + { + ViewModel.IsActive = obj; + } + + private void OnScalingChanged(object sender, EventArgs e) + { + Program.DesktopScaleFactor = this.RenderScaling; + } + + private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + ViewModel.Applications.Add(e.AppData); + }); + } + + 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; + } + }); + } + + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) + { + if (args.Application != null) + { + ViewModel.SelectedIcon = args.Application.Icon; + + string path = new FileInfo(args.Application.Path).FullName; + + ViewModel.LoadApplication(path).Wait(); + } + + args.Handled = true; + } + + internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg) + { + _deferLoad = true; + _launchPath = launchPathArg; + _startFullscreen = startFullscreenArg; + } + + public void SwitchToGameControl(bool startFullscreen = false) + { + ViewModel.ShowLoadProgress = false; + ViewModel.ShowContent = true; + ViewModel.IsLoadingIndeterminate = false; + + if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + } + + public void ShowLoading(bool startFullscreen = false) + { + ViewModel.ShowContent = false; + ViewModel.ShowLoadProgress = true; + ViewModel.IsLoadingIndeterminate = true; + + if (startFullscreen && ViewModel.WindowState != 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); + + // 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( + $"Ryujinx - {LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTitle]}", + 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; + + ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); + } + } + else + { + ShowKeyErrorOnLoad = false; + + await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) + { + await Updater.BeginParse(this, false).ContinueWith(task => + { + Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + } + + private void Load() + { + StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; + + ApplicationGrid.ApplicationOpened += Application_Opened; + + ApplicationGrid.DataContext = ViewModel; + + ApplicationList.ApplicationOpened += Application_Opened; + + ApplicationList.DataContext = ViewModel; + } + + private void SetWindowSizePosition() + { + 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 (CheckScreenBounds(savedPoint)) + { + Position = savedPoint; + } + else + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + } + + private bool CheckScreenBounds(PixelPoint configPoint) + { + for (int i = 0; i < Screens.ScreenCount; i++) + { + if (Screens.All[i].Bounds.Contains(configPoint)) + { + return true; + } + } + + Logger.Warning?.Print(LogClass.Application, "Failed to find valid start-up coordinates. Defaulting to primary monitor center."); + return false; + } + + private void SaveWindowSizePosition() + { + ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height; + ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)Width; + + ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; + ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; + + ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value = WindowState == WindowState.Maximized; + + MainWindowViewModel.SaveConfig(); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + Initialize(); + + ViewModel.Initialize( + ContentManager, + StorageProvider, + ApplicationLibrary, + VirtualFileSystem, + AccountManager, + InputManager, + _userChannelPersistence, + LibHacHorizonManager, + UiHandler, + ShowLoading, + SwitchToGameControl, + SetMainContent, + this); + + ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; + ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + + ViewModel.RefreshFirmwareStatus(); + + LoadApplications(); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + CheckLaunchState(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + + private void SetMainContent(Control content = null) + { + content ??= GameLibrary; + + if (MainContent.Content != content) + { + 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; +#pragma warning restore IDE0055 + } + + private void VolumeStatus_CheckedChanged(object sender, RoutedEventArgs e) + { + var volumeSplitButton = sender as ToggleSplitButton; + if (ViewModel.IsGameRunning) + { + 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 += (sender, e) => + { + ViewModel.AppHost = null; + + Dispatcher.UIThread.Post(() => + { + MainContent = null; + + Close(); + }); + }; + ViewModel.AppHost?.Stop(); + + e.Cancel = true; + + return; + } + + SaveWindowSizePosition(); + + ApplicationLibrary.CancelLoading(); + InputManager.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() + { + ViewModel.Applications.Clear(); + + StatusBarView.LoadProgressBar.IsVisible = true; + ViewModel.StatusBarProgressMaximum = 0; + ViewModel.StatusBarProgressValue = 0; + + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); + + ReloadGameList(); + } + + public void ToggleFileType(string fileType) + { + _ = fileType switch + { +#pragma warning disable IDE0055 // Disable formatting + "NSP" => ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NSP, + "PFS0" => ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value = !ConfigurationState.Instance.UI.ShownFileTypes.PFS0, + "XCI" => ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value = !ConfigurationState.Instance.UI.ShownFileTypes.XCI, + "NCA" => ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NCA, + "NRO" => ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NRO, + "NSO" => ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NSO, + _ => throw new ArgumentOutOfRangeException(fileType), +#pragma warning restore IDE0055 + }; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + LoadApplications(); + } + + private void ReloadGameList() + { + if (_isLoading) + { + return; + } + + _isLoading = true; + + Thread applicationLibraryThread = new(() => + { + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language); + + _isLoading = false; + }) + { + Name = "GUI.ApplicationLibraryThread", + IsBackground = true, + }; + applicationLibraryThread.Start(); + } + } +} |