Răsfoiți Sursa

UI: More advanced customization for what happens when Ryujinx loses focus

Evan Husted 1 an în urmă
părinte
comite
17e8ae1d9a

+ 1 - 1
src/Ryujinx/AppHost.cs

@@ -517,7 +517,7 @@ namespace Ryujinx.Ava
             Device?.System.ChangeDockedModeState(e.NewValue);
         }
 
-        private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
+        public void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
         {
             Device?.SetVolume(e.NewValue);
 

+ 151 - 1
src/Ryujinx/Assets/locales.json

@@ -3447,6 +3447,156 @@
         "zh_TW": ""
       }
     },
+    {
+      "ID": "SettingsTabGeneralFocusLossType",
+      "Translations": {
+        "ar_SA": "",
+        "de_DE": "",
+        "el_GR": "",
+        "en_US": "On Emulator Focus Lost:",
+        "es_ES": "",
+        "fr_FR": "",
+        "he_IL": "",
+        "it_IT": "",
+        "ja_JP": "",
+        "ko_KR": "",
+        "no_NO": "",
+        "pl_PL": "",
+        "pt_BR": "",
+        "ru_RU": "",
+        "sv_SE": "",
+        "th_TH": "",
+        "tr_TR": "",
+        "uk_UA": "",
+        "zh_CN": "",
+        "zh_TW": ""
+      }
+    },
+    {
+      "ID": "SettingsTabGeneralFocusLossTypeDoNothing",
+      "Translations": {
+        "ar_SA": "",
+        "de_DE": "",
+        "el_GR": "",
+        "en_US": "Do Nothing",
+        "es_ES": "",
+        "fr_FR": "",
+        "he_IL": "",
+        "it_IT": "",
+        "ja_JP": "",
+        "ko_KR": "",
+        "no_NO": "",
+        "pl_PL": "",
+        "pt_BR": "",
+        "ru_RU": "",
+        "sv_SE": "",
+        "th_TH": "",
+        "tr_TR": "",
+        "uk_UA": "",
+        "zh_CN": "",
+        "zh_TW": ""
+      }
+    },
+    {
+      "ID": "SettingsTabGeneralFocusLossTypeBlockInput",
+      "Translations": {
+        "ar_SA": "",
+        "de_DE": "",
+        "el_GR": "",
+        "en_US": "Block Input",
+        "es_ES": "",
+        "fr_FR": "",
+        "he_IL": "",
+        "it_IT": "",
+        "ja_JP": "",
+        "ko_KR": "",
+        "no_NO": "",
+        "pl_PL": "",
+        "pt_BR": "",
+        "ru_RU": "",
+        "sv_SE": "",
+        "th_TH": "",
+        "tr_TR": "",
+        "uk_UA": "",
+        "zh_CN": "",
+        "zh_TW": ""
+      }
+    },
+    {
+      "ID": "SettingsTabGeneralFocusLossTypeMuteAudio",
+      "Translations": {
+        "ar_SA": "",
+        "de_DE": "",
+        "el_GR": "",
+        "en_US": "Mute Volume",
+        "es_ES": "",
+        "fr_FR": "",
+        "he_IL": "",
+        "it_IT": "",
+        "ja_JP": "",
+        "ko_KR": "",
+        "no_NO": "",
+        "pl_PL": "",
+        "pt_BR": "",
+        "ru_RU": "",
+        "sv_SE": "",
+        "th_TH": "",
+        "tr_TR": "",
+        "uk_UA": "",
+        "zh_CN": "",
+        "zh_TW": ""
+      }
+    },
+    {
+      "ID": "SettingsTabGeneralFocusLossTypeBlockInputAndMuteAudio",
+      "Translations": {
+        "ar_SA": "",
+        "de_DE": "",
+        "el_GR": "",
+        "en_US": "Block Input & Mute Volume",
+        "es_ES": "",
+        "fr_FR": "",
+        "he_IL": "",
+        "it_IT": "",
+        "ja_JP": "",
+        "ko_KR": "",
+        "no_NO": "",
+        "pl_PL": "",
+        "pt_BR": "",
+        "ru_RU": "",
+        "sv_SE": "",
+        "th_TH": "",
+        "tr_TR": "",
+        "uk_UA": "",
+        "zh_CN": "",
+        "zh_TW": ""
+      }
+    },
+    {
+      "ID": "SettingsTabGeneralFocusLossTypePauseEmulation",
+      "Translations": {
+        "ar_SA": "",
+        "de_DE": "",
+        "el_GR": "",
+        "en_US": "Pause Emulation",
+        "es_ES": "",
+        "fr_FR": "",
+        "he_IL": "",
+        "it_IT": "",
+        "ja_JP": "",
+        "ko_KR": "",
+        "no_NO": "",
+        "pl_PL": "",
+        "pt_BR": "",
+        "ru_RU": "",
+        "sv_SE": "",
+        "th_TH": "",
+        "tr_TR": "",
+        "uk_UA": "",
+        "zh_CN": "",
+        "zh_TW": ""
+      }
+    },
     {
       "ID": "SettingsTabGeneralShowConfirmExitDialog",
       "Translations": {
@@ -23923,4 +24073,4 @@
       }
     }
   ]
-}
+}

+ 0 - 14
src/Ryujinx/Common/ThemeManager.cs

@@ -1,14 +0,0 @@
-using System;
-
-namespace Ryujinx.Ava.Common
-{
-    public static class ThemeManager
-    {
-        public static event Action ThemeChanged;
-
-        public static void OnThemeChanged()
-        {
-            ThemeChanged?.Invoke();
-        }
-    }
-}

+ 3 - 1
src/Ryujinx/RyujinxApp.axaml.cs

@@ -22,6 +22,8 @@ namespace Ryujinx.Ava
 {
     public class RyujinxApp : Application
     {
+        public static event Action ThemeChanged;
+        
         internal static string FormatTitle(LocaleKeys? windowTitleKey = null, bool includeVersion = true)
             => windowTitleKey is null
                 ? $"{FullAppName}{(includeVersion ? $" {Program.Version}" : string.Empty)}"
@@ -112,7 +114,7 @@ namespace Ryujinx.Ava
                     baseStyle = ConfigurationState.Instance.UI.BaseStyle;
                 }
 
-                ThemeManager.OnThemeChanged();
+                ThemeChanged?.Invoke();
 
                 RequestedThemeVariant = baseStyle switch
                 {

+ 3 - 3
src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs

@@ -25,10 +25,10 @@ namespace Ryujinx.Ava.UI.ViewModels
             Version = RyujinxApp.FullAppName + "\n" + Program.Version;
             UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value);
 
-            ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
+            RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
         }
 
-        private void ThemeManager_ThemeChanged()
+        private void Ryujinx_ThemeChanged()
         {
             Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
         }
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
         public void Dispose()
         {
-            ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
+            RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
             
             GithubLogo.Dispose();
             DiscordLogo.Dispose();

+ 4 - 0
src/Ryujinx/UI/ViewModels/SettingsViewModel.cs

@@ -128,6 +128,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         public bool EnableMouse { get; set; }
         public bool DisableInputWhenOutOfFocus { get; set; }
         
+        public int FocusLostActionType { get; set; }
+        
         public VSyncMode VSyncMode
         {
             get => _vSyncMode;
@@ -481,6 +483,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             ShowTitleBar = config.ShowTitleBar;
             HideCursor = (int)config.HideCursor.Value;
             UpdateCheckerType = (int)config.UpdateCheckerType.Value;
+            FocusLostActionType = (int)config.FocusLostActionType.Value;
 
             GameDirectories.Clear();
             GameDirectories.AddRange(config.UI.GameDirs.Value);
@@ -589,6 +592,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             config.ShowTitleBar.Value = ShowTitleBar;
             config.HideCursor.Value = (HideCursorMode)HideCursor;
             config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
+            config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
 
             if (GameDirectoryChanged)
             {

+ 24 - 3
src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml

@@ -37,12 +37,33 @@
                     <CheckBox IsChecked="{Binding RememberWindowState}">
                         <TextBlock Text="{ext:Locale SettingsTabGeneralRememberWindowState}" />
                     </CheckBox>
-                    <CheckBox IsChecked="{Binding DisableInputWhenOutOfFocus}">
-                        <TextBlock Text="{ext:Locale SettingsTabGeneralDisableInputWhenOutOfFocus}" />
-                    </CheckBox>
                     <CheckBox IsChecked="{Binding ShowTitleBar}" IsVisible="{x:Static helper:RunningPlatform.IsWindows}">
                         <TextBlock Text="{ext:Locale SettingsTabGeneralShowTitleBar}" />
                     </CheckBox>
+                    <StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
+                        <TextBlock VerticalAlignment="Center"
+                                   Text="{ext:Locale SettingsTabGeneralFocusLossType}"
+                                   Width="150" />
+                        <ComboBox SelectedIndex="{Binding FocusLostActionType}"
+                                  HorizontalContentAlignment="Left"
+                                  MinWidth="100">
+                            <ComboBoxItem>
+                                <TextBlock Text="{ext:Locale SettingsTabGeneralFocusLossTypeDoNothing}" />
+                            </ComboBoxItem>
+                            <ComboBoxItem>
+                                <TextBlock Text="{ext:Locale SettingsTabGeneralFocusLossTypeBlockInput}" />
+                            </ComboBoxItem>
+                            <ComboBoxItem>
+                                <TextBlock Text="{ext:Locale SettingsTabGeneralFocusLossTypeMuteAudio}" />
+                            </ComboBoxItem>
+                            <ComboBoxItem>
+                                <TextBlock Text="{ext:Locale SettingsTabGeneralFocusLossTypeBlockInputAndMuteAudio}" />
+                            </ComboBoxItem>
+                            <ComboBoxItem>
+                                <TextBlock Text="{ext:Locale SettingsTabGeneralFocusLossTypePauseEmulation}" />
+                            </ComboBoxItem>
+                        </ComboBox>
+                    </StackPanel>
                     <StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
                         <TextBlock VerticalAlignment="Center"
                                    Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}"

+ 100 - 15
src/Ryujinx/UI/Windows/MainWindow.axaml.cs

@@ -765,31 +765,116 @@ namespace Ryujinx.Ava.UI.Windows
         
         private void InputElement_OnGotFocus(object sender, GotFocusEventArgs e)
         {
-            if (!_didDisableInputUpdates) 
-                return;
-
-            if (!ConfigurationState.Instance.Hid.DisableInputWhenOutOfFocus)
+            if (ViewModel.AppHost is null) return;
+            
+            if (!_focusLoss.Active) 
                 return;
 
-            if (ViewModel.AppHost is not { NpadManager.InputUpdatesBlocked: true } appHost)
-                return;
+            switch (_focusLoss.Type)
+            {
+                case FocusLostType.BlockInput:
+                    {
+                        if (!ViewModel.AppHost.NpadManager.InputUpdatesBlocked)
+                        {
+                            _focusLoss = default;
+                            return;
+                        }
 
-            appHost.NpadManager.UnblockInputUpdates();
-            _didDisableInputUpdates = appHost.NpadManager.InputUpdatesBlocked;
+                        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 bool _didDisableInputUpdates;
+        
+        private (FocusLostType Type, bool Active) _focusLoss;
 
         private void InputElement_OnLostFocus(object sender, RoutedEventArgs e)
         {
-            if (!ConfigurationState.Instance.Hid.DisableInputWhenOutOfFocus)
+            if (ConfigurationState.Instance.FocusLostActionType.Value is FocusLostType.DoNothing)
                 return;
 
-            if (ViewModel.AppHost is not { NpadManager.InputUpdatesBlocked: false } appHost)
-                return;
+            if (ViewModel.AppHost is null) return;
+
+            switch (ConfigurationState.Instance.FocusLostActionType.Value)
+            {
+                case FocusLostType.BlockInput:
+                    {
+                        if (ViewModel.AppHost.NpadManager.InputUpdatesBlocked)
+                            return;
             
-            appHost.NpadManager.BlockInputUpdates();
-            _didDisableInputUpdates = appHost.NpadManager.InputUpdatesBlocked;
+                        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;
+                    }
+            }
         }
     }
 }

+ 6 - 1
src/Ryujinx/Utilities/Configuration/ConfigurationFileFormat.cs

@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
         /// <summary>
         /// The current version of the file format
         /// </summary>
-        public const int CurrentVersion = 66;
+        public const int CurrentVersion = 67;
 
         /// <summary>
         /// Version of the configuration file format
@@ -171,6 +171,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
         /// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
         /// </summary>
         public UpdaterType UpdateCheckerType { get; set; }
+        
+        /// <summary>
+        /// How the emulator should behave when you click off/on the window.
+        /// </summary>
+        public FocusLostType FocusLostActionType { get; set; }
 
         /// <summary>
         /// Show "Confirm Exit" Dialog

+ 3 - 1
src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs

@@ -46,6 +46,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
             EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
             CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart;
             UpdateCheckerType.Value = cff.UpdateCheckerType;
+            FocusLostActionType.Value = cff.FocusLostActionType;
             ShowConfirmExit.Value = cff.ShowConfirmExit;
             RememberWindowState.Value = cff.RememberWindowState;
             ShowTitleBar.Value = cff.ShowTitleBar;
@@ -435,7 +436,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
                 (63, static cff => cff.MatchSystemTime = false),
                 (64, static cff => cff.LoggingEnableAvalonia = false),
                 (65, static cff => cff.UpdateCheckerType = cff.CheckUpdatesOnStart ? UpdaterType.PromptAtStartup : UpdaterType.Off),
-                (66, static cff => cff.DisableInputWhenOutOfFocus = false)
+                (66, static cff => cff.DisableInputWhenOutOfFocus = false),
+                (67, static cff => cff.FocusLostActionType = cff.DisableInputWhenOutOfFocus ? FocusLostType.BlockInput : FocusLostType.DoNothing)
             );
     }
 }

+ 6 - 0
src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs

@@ -779,6 +779,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
         /// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
         /// </summary>
         public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
+        
+        /// <summary>
+        /// How the emulator should behave when you click off/on the window.
+        /// </summary>
+        public ReactiveObject<FocusLostType> FocusLostActionType { get; private set; }
 
         /// <summary>
         /// Show "Confirm Exit" Dialog
@@ -817,6 +822,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
             EnableDiscordIntegration = new ReactiveObject<bool>();
             CheckUpdatesOnStart = new ReactiveObject<bool>();
             UpdateCheckerType = new ReactiveObject<UpdaterType>();
+            FocusLostActionType = new ReactiveObject<FocusLostType>();
             ShowConfirmExit = new ReactiveObject<bool>();
             RememberWindowState = new ReactiveObject<bool>();
             ShowTitleBar = new ReactiveObject<bool>();

+ 2 - 0
src/Ryujinx/Utilities/Configuration/ConfigurationState.cs

@@ -57,6 +57,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
                 EnableDiscordIntegration = EnableDiscordIntegration,
                 CheckUpdatesOnStart = CheckUpdatesOnStart,
                 UpdateCheckerType = UpdateCheckerType,
+                FocusLostActionType = FocusLostActionType,
                 ShowConfirmExit = ShowConfirmExit,
                 RememberWindowState = RememberWindowState,
                 ShowTitleBar = ShowTitleBar,
@@ -178,6 +179,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
             System.EnableDockedMode.Value = true;
             EnableDiscordIntegration.Value = true;
             UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
+            FocusLostActionType.Value = FocusLostType.DoNothing;
             ShowConfirmExit.Value = true;
             RememberWindowState.Value = true;
             ShowTitleBar.Value = !OperatingSystem.IsWindows();

+ 15 - 0
src/Ryujinx/Utilities/Configuration/UI/FocusLostType.cs

@@ -0,0 +1,15 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ava.Utilities.Configuration.UI
+{
+    [JsonConverter(typeof(TypedStringEnumConverter<FocusLostType>))]
+    public enum FocusLostType
+    {
+        DoNothing,
+        BlockInput,
+        MuteAudio,
+        BlockInputAndMuteAudio,
+        PauseEmulation
+    }
+}