diff options
| author | Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> | 2022-12-29 14:24:05 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-12-29 15:24:05 +0100 |
| commit | 76671d63d4f3ea18f8ad99e9ce9f0b2ec9a2599d (patch) | |
| tree | 05013214e4696a9254369d0706173f58877f6a83 /Ryujinx.Ava/UI/Windows | |
| parent | 3d1a0bf3749afa14da5b5ba1e0666fdb78c99beb (diff) | |
Ava GUI: Restructure `Ryujinx.Ava` (#4165)
* Restructure `Ryujinx.Ava`
* Stylistic consistency
* Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Fix redundancies
* Remove redunancies
* Add back elses
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/UI/Windows')
26 files changed, 6366 insertions, 0 deletions
diff --git a/Ryujinx.Ava/UI/Windows/AboutWindow.axaml b/Ryujinx.Ava/UI/Windows/AboutWindow.axaml new file mode 100644 index 00000000..08d28740 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/AboutWindow.axaml @@ -0,0 +1,282 @@ +<window:StyleableWindow + x:Class="Ryujinx.Ava.UI.Windows.AboutWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + Width="850" + Height="550" + MinWidth="500" + MinHeight="550" + d:DesignHeight="350" + d:DesignWidth="400" + CanResize="False" + SizeToContent="Width" + WindowStartupLocation="CenterOwner" + mc:Ignorable="d" + Focusable="True"> + <Grid + Margin="15" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + <Grid + Grid.Row="1" + Grid.Column="0" + Margin="20" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="*" /> + <RowDefinition Height="*" /> + <RowDefinition /> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Grid Grid.Row="0"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image + Grid.Row="0" + Grid.RowSpan="3" + Grid.Column="0" + Height="110" + MinWidth="50" + Margin="5,10,20,10" + Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" /> + <TextBlock + Grid.Row="0" + Grid.Column="1" + Margin="0,20,0,0" + FontSize="35" + Text="Ryujinx" + TextAlignment="Center" /> + <TextBlock + Grid.Row="1" + Grid.Column="1" + Margin="0,0,0,0" + FontSize="16" + Text="(REE-YOU-JINX)" + TextAlignment="Center" /> + <Button + Grid.Row="2" + Grid.Column="1" + Margin="0" + HorizontalAlignment="Center" + Background="Transparent" + Click="Button_OnClick" + Tag="https://www.ryujinx.org/"> + <TextBlock + Text="www.ryujinx.org" + TextAlignment="Center" + TextDecorations="Underline" + ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}" /> + </Button> + </Grid> + <TextBlock + Grid.Row="1" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{Binding Version}" + TextAlignment="Center" /> + <TextBlock + Grid.Row="2" + Margin="20" + HorizontalAlignment="Center" + MaxLines="2" + Text="{locale:Locale AboutDisclaimerMessage}" + TextAlignment="Center" /> + <TextBlock + Name="AmiiboLabel" + Grid.Row="3" + Margin="20" + HorizontalAlignment="Center" + MaxLines="2" + PointerPressed="AmiiboLabel_OnPointerPressed" + Text="{locale:Locale AboutAmiiboDisclaimerMessage}" + TextAlignment="Center" /> + <StackPanel + Grid.Row="4" + HorizontalAlignment="Center" + Orientation="Horizontal" + Spacing="10"> + <StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}"> + <Button + Height="65" + Background="Transparent" + Click="Button_OnClick" + Tag="https://www.patreon.com/ryujinx"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Patreon.png?assembly=Ryujinx.Ui.Common" /> + <TextBlock + Grid.Row="1" + Margin="0,5,0,0" + HorizontalAlignment="Center" + Text="Patreon" /> + </Grid> + </Button> + </StackPanel> + <StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}"> + <Button + Height="65" + Background="Transparent" + Click="Button_OnClick" + Tag="https://github.com/Ryujinx/Ryujinx"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_GitHub.png?assembly=Ryujinx.Ui.Common" /> + <TextBlock + Grid.Row="1" + Margin="0,5,0,0" + HorizontalAlignment="Center" + Text="GitHub" /> + </Grid> + </Button> + </StackPanel> + <StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}"> + <Button + Height="65" + Background="Transparent" + Click="Button_OnClick" + Tag="https://discordapp.com/invite/N2FmfVc"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Discord.png?assembly=Ryujinx.Ui.Common" /> + <TextBlock + Grid.Row="1" + Margin="0,5,0,0" + HorizontalAlignment="Center" + Text="Discord" /> + </Grid> + </Button> + </StackPanel> + <StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}"> + <Button + Height="65" + Background="Transparent" + Click="Button_OnClick" + Tag="https://twitter.com/RyujinxEmu"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Twitter.png?assembly=Ryujinx.Ui.Common" /> + <TextBlock + Grid.Row="1" + Margin="0,5,0,0" + HorizontalAlignment="Center" + Text="Twitter" /> + </Grid> + </Button> + </StackPanel> + </StackPanel> + </Grid> + <Border + Grid.Row="1" + Grid.Column="1" + Width="2" + Margin="5" + VerticalAlignment="Stretch" + BorderBrush="White" + BorderThickness="1,0,0,0"> + <Separator Width="0" /> + </Border> + <Grid + Grid.Row="1" + Grid.Column="2" + Margin="20" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock + FontWeight="Bold" + Text="{locale:Locale AboutRyujinxAboutTitle}" + TextDecorations="Underline" /> + <TextBlock + Grid.Row="1" + Margin="20,5,5,5" + LineHeight="20" + Text="{locale:Locale AboutRyujinxAboutContent}" /> + <TextBlock + Grid.Row="2" + Margin="0,10,0,0" + FontWeight="Bold" + Text="{locale:Locale AboutRyujinxMaintainersTitle}" + TextDecorations="Underline" /> + <TextBlock + Grid.Row="3" + Margin="20,5,5,5" + LineHeight="20" + Text="{Binding Developers}" /> + <Button + Grid.Row="4" + HorizontalAlignment="Right" + Background="Transparent" + Click="Button_OnClick" + Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"> + <TextBlock + Text="{locale:Locale AboutRyujinxContributorsButtonHeader}" + TextAlignment="Right" + TextDecorations="Underline" + ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" /> + </Button> + <TextBlock + Grid.Row="5" + Margin="0,0,0,0" + FontWeight="Bold" + Text="{locale:Locale AboutRyujinxSupprtersTitle}" + TextDecorations="Underline" /> + <Border + Grid.Row="6" + Width="460" + Height="200" + Margin="20,5" + Padding="5" + VerticalAlignment="Stretch" + BorderBrush="White" + BorderThickness="1"> + <TextBlock + Name="SupportersTextBlock" + VerticalAlignment="Top" + Text="{Binding Supporters}" + TextWrapping="Wrap" /> + </Border> + </Grid> + </Grid> +</window:StyleableWindow> diff --git a/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs new file mode 100644 index 00000000..2fb17e3a --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Common.Utilities; +using Ryujinx.Ui.Common.Helper; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class AboutWindow : StyleableWindow + { + public AboutWindow() + { + if (Program.PreviewerDetached) + { + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["MenuBarHelpAbout"]; + } + + Version = Program.Version; + + DataContext = this; + + InitializeComponent(); + + _ = DownloadPatronsJson(); + } + + public string Supporters { get; set; } + public string Version { get; set; } + + public string Developers => string.Format(LocaleManager.Instance["AboutPageDeveloperListMore"], "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD«"); + + private void Button_OnClick(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + OpenHelper.OpenUrl(button.Tag.ToString()); + } + } + + private async Task DownloadPatronsJson() + { + if (!NetworkInterface.GetIsNetworkAvailable()) + { + Supporters = LocaleManager.Instance["ConnectionError"]; + + return; + } + + HttpClient httpClient = new(); + + try + { + string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); + + Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)); + } + catch + { + Supporters = LocaleManager.Instance["ApiError"]; + } + + await Dispatcher.UIThread.InvokeAsync(() => SupportersTextBlock.Text = Supporters); + } + + private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (sender is TextBlock) + { + OpenHelper.OpenUrl("https://amiiboapi.com"); + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml new file mode 100644 index 00000000..90d47b8e --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml @@ -0,0 +1,74 @@ +<window:StyleableWindow + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + mc:Ignorable="d" + d:DesignWidth="400" + d:DesignHeight="350" + x:Class="Ryujinx.Ava.UI.Windows.AmiiboWindow" + CanResize="False" + WindowStartupLocation="CenterOwner" + Width="800" + MinHeight="650" + Height="650" + SizeToContent="Manual" + MinWidth="600" + Focusable="True"> + <Design.DataContext> + <viewModels:AmiiboWindowViewModel /> + </Design.DataContext> + <Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Grid Grid.Row="1" HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboSeriesLabel}" /> + <ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" Items="{Binding AmiiboSeries}" MinWidth="100" /> + </StackPanel> + <StackPanel Spacing="10" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboCharacterLabel}" /> + <ComboBox SelectedIndex="{Binding AmiiboSelectedIndex}" MinWidth="100" Items="{Binding AmiiboList}" /> + </StackPanel> + </Grid> + <StackPanel Margin="20" Grid.Row="2"> + <Image Source="{Binding AmiiboImage}" Height="350" Width="350" HorizontalAlignment="Center" /> + <ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" + Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch"> + <TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center" + TextAlignment="Center" /> + </ScrollViewer> + </StackPanel> + <Grid Grid.Row="3"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding ShowAllAmiibo}" + Content="{locale:Locale AmiiboOptionsShowAllLabel}" /> + <CheckBox Margin="10" VerticalContentAlignment="Center" Grid.Column="1" IsChecked="{Binding UseRandomUuid}" + Content="{locale:Locale AmiiboOptionsUsRandomTagLabel}" /> + + <Button Grid.Column="3" IsEnabled="{Binding EnableScanning}" Width="80" + Content="{locale:Locale AmiiboScanButtonLabel}" Name="ScanButton" + Click="ScanButton_Click" /> + <Button Grid.Column="4" Margin="10,0" Width="80" Content="{locale:Locale InputDialogCancel}" + Name="CancelButton" + Click="CancelButton_Click" /> + </Grid> + </Grid> +</window:StyleableWindow>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs new file mode 100644 index 00000000..68d16f35 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs @@ -0,0 +1,59 @@ +using Avalonia.Interactivity; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class AmiiboWindow : StyleableWindow + { + public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId) + { + ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId); + + ViewModel.ShowAllAmiibo = showAll; + + DataContext = ViewModel; + + InitializeComponent(); + + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"]; + } + + public AmiiboWindow() + { + ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty); + + DataContext = ViewModel; + + InitializeComponent(); + + if (Program.PreviewerDetached) + { + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"]; + } + } + + public bool IsScanned { get; set; } + public Amiibo.AmiiboApi ScannedAmiibo { get; set; } + public AmiiboWindowViewModel ViewModel { get; set; } + + private void ScanButton_Click(object sender, RoutedEventArgs e) + { + if (ViewModel.AmiiboSelectedIndex > -1) + { + Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex]; + ScannedAmiibo = amiibo; + IsScanned = true; + Close(); + } + } + + private void CancelButton_Click(object sender, RoutedEventArgs e) + { + IsScanned = false; + + Close(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml b/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml new file mode 100644 index 00000000..c90ce022 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml @@ -0,0 +1,54 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350" + x:Class="Ryujinx.Ava.UI.Windows.AvatarWindow" + Margin="0" + Padding="0" + x:CompileBindings="True" + x:DataType="viewModels:AvatarProfileViewModel" + Focusable="True"> + <Design.DataContext> + <viewModels:AvatarProfileViewModel /> + </Design.DataContext> + <UserControl.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + </UserControl.Resources> + <Grid Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Height="400" + Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <WrapPanel Orientation="Horizontal" MaxWidth="700" Margin="0" HorizontalAlignment="Center" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.ItemTemplate> + <DataTemplate> + <Image Margin="5" Height="96" Width="96" + Source="{Binding Data, Converter={StaticResource ByteImage}}" /> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + <ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5" + Maximum="{Binding ImageCount}" Minimum="0" /> + <StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center"> + <Button Content="{locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" /> + <ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" /> + <Button HorizontalAlignment="Right" Content="{locale:Locale Discard}" Click="CloseButton_OnClick" + Name="CloseButton" + Width="200" /> + </StackPanel> + </Grid> +</UserControl>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs new file mode 100644 index 00000000..e060d65e --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs @@ -0,0 +1,77 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class AvatarWindow : UserControl + { + private NavigationDialogHost _parent; + private TempProfile _profile; + + public AvatarWindow(ContentManager contentManager) + { + ContentManager = contentManager; + + DataContext = ViewModel; + + InitializeComponent(); + } + + public AvatarWindow() + { + InitializeComponent(); + + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + if (arg.NavigationMode == NavigationMode.New) + { + (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter; + ContentManager = _parent.ContentManager; + if (Program.PreviewerDetached) + { + ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages()); + } + + DataContext = ViewModel; + } + } + } + + public ContentManager ContentManager { get; private set; } + + internal AvatarProfileViewModel ViewModel { get; set; } + + private void CloseButton_OnClick(object sender, RoutedEventArgs e) + { + ViewModel.Dispose(); + + _parent.GoBack(); + } + + private void ChooseButton_OnClick(object sender, RoutedEventArgs e) + { + if (ViewModel.SelectedIndex > -1) + { + _profile.Image = ViewModel.SelectedImage; + + ViewModel.Dispose(); + + _parent.GoBack(); + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/Ryujinx.Ava/UI/Windows/CheatWindow.axaml new file mode 100644 index 00000000..3557ed69 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/CheatWindow.axaml @@ -0,0 +1,106 @@ +<window:StyleableWindow + x:Class="Ryujinx.Ava.UI.Windows.CheatWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:model="clr-namespace:Ryujinx.Ava.UI.Models" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + Width="500" + Height="500" + MinWidth="500" + MinHeight="500" + WindowStartupLocation="CenterOwner" + mc:Ignorable="d" + Focusable="True"> + <Window.Styles> + <Style Selector="TreeViewItem"> + <Setter Property="IsExpanded" Value="True" /> + </Style> + </Window.Styles> + <Grid Name="CheatGrid" Margin="15"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock + Grid.Row="1" + MaxWidth="500" + Margin="20,15,20,20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + LineHeight="18" + Text="{Binding Heading}" + TextAlignment="Center" + TextWrapping="Wrap" /> + <Border + Grid.Row="2" + Margin="5" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + BorderBrush="Gray" + BorderThickness="1"> + <TreeView + Name="CheatsView" + MinHeight="300" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Items="{Binding LoadedCheats}"> + <TreeView.Styles> + <Styles> + <Style Selector="TreeViewItem:empty /template/ ItemsPresenter"> + <Setter Property="IsVisible" Value="False" /> + </Style> + </Styles> + </TreeView.Styles> + <TreeView.DataTemplates> + <TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}"> + <StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> + <CheckBox MinWidth="20" IsChecked="{Binding IsEnabled}" /> + <TextBlock Width="150" Text="{Binding BuildId}" /> + <TextBlock Text="{Binding Path}" /> + </StackPanel> + </TreeDataTemplate> + <DataTemplate x:DataType="model:CheatModel"> + <StackPanel + Margin="0" + HorizontalAlignment="Left" + Orientation="Horizontal"> + <CheckBox + MinWidth="20" + Margin="5,0" + Padding="0" + IsChecked="{Binding IsEnabled}" /> + <TextBlock VerticalAlignment="Center" Text="{Binding CleanName}" /> + </StackPanel> + </DataTemplate> + </TreeView.DataTemplates> + </TreeView> + </Border> + <DockPanel + Grid.Row="3" + Margin="0" + HorizontalAlignment="Stretch"> + <DockPanel Margin="0" HorizontalAlignment="Right"> + <Button + Name="SaveButton" + MinWidth="90" + Margin="5" + Command="{Binding Save}" + IsVisible="{Binding !NoCheatsFound}"> + <TextBlock Text="{locale:Locale SettingsButtonSave}" /> + </Button> + <Button + Name="CancelButton" + MinWidth="90" + Margin="5" + Command="{Binding Close}"> + <TextBlock Text="{locale:Locale InputDialogCancel}" /> + </Button> + </DockPanel> + </DockPanel> + </Grid> +</window:StyleableWindow>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs new file mode 100644 index 00000000..d31212c1 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs @@ -0,0 +1,119 @@ +using Avalonia.Collections; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class CheatWindow : StyleableWindow + { + private readonly string _enabledCheatsPath; + public bool NoCheatsFound { get; } + + private AvaloniaList<CheatsList> LoadedCheats { get; } + + public string Heading { get; } + + public CheatWindow() + { + DataContext = this; + + InitializeComponent(); + + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"]; + } + + public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) + { + LoadedCheats = new AvaloniaList<CheatsList>(); + + Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper()); + + InitializeComponent(); + + string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); + string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId); + ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber); + + _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt"); + + string[] enabled = { }; + + if (File.Exists(_enabledCheatsPath)) + { + enabled = File.ReadAllLines(_enabledCheatsPath); + } + + int cheatAdded = 0; + + var mods = new ModLoader.ModCache(); + + ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue); + + string currentCheatFile = string.Empty; + string buildId = string.Empty; + string parentPath = string.Empty; + + CheatsList currentGroup = null; + + foreach (var cheat in mods.Cheats) + { + if (cheat.Path.FullName != currentCheatFile) + { + currentCheatFile = cheat.Path.FullName; + parentPath = currentCheatFile.Replace(titleModsPath, ""); + + buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); + currentGroup = new CheatsList(buildId, parentPath); + + LoadedCheats.Add(currentGroup); + } + + var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}")); + currentGroup?.Add(model); + + cheatAdded++; + } + + if (cheatAdded == 0) + { + NoCheatsFound = true; + } + + DataContext = this; + + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"]; + } + + public void Save() + { + if (NoCheatsFound) + { + return; + } + + List<string> enabledCheats = new List<string>(); + + foreach (var cheats in LoadedCheats) + { + foreach (var cheat in cheats) + { + if (cheat.IsEnabled) + { + enabledCheats.Add(cheat.BuildIdKey); + } + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath)); + + File.WriteAllLines(_enabledCheatsPath, enabledCheats); + + Close(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml b/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml new file mode 100644 index 00000000..6cdcae2b --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml @@ -0,0 +1,29 @@ +<window:StyleableWindow + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" + x:Class="Ryujinx.Ava.UI.Windows.ContentDialogOverlayWindow" + Title="ContentDialogOverlayWindow" + Focusable="True"> + <window:StyleableWindow.Styles> + <Style Selector="ui|ContentDialog /template/ Panel#LayoutRoot"> + <Setter Property="Background" + Value="Transparent" /> + </Style> + </window:StyleableWindow.Styles> + <ContentControl + Focusable="False" + IsVisible="False" + KeyboardNavigation.IsTabStop="False"> + <ui:ContentDialog Name="ContentDialog" + IsPrimaryButtonEnabled="True" + IsSecondaryButtonEnabled="True" + IsVisible="False" /> + </ContentControl> +</window:StyleableWindow> diff --git a/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs new file mode 100644 index 00000000..3f77124d --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ContentDialogOverlayWindow : StyleableWindow + { + public ContentDialogOverlayWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + ExtendClientAreaToDecorationsHint = true; + TransparencyLevelHint = WindowTransparencyLevel.Transparent; + WindowStartupLocation = WindowStartupLocation.Manual; + SystemDecorations = SystemDecorations.None; + ExtendClientAreaTitleBarHeightHint = 0; + Background = Brushes.Transparent; + CanResize = false; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml b/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml new file mode 100644 index 00000000..f6bb1aa4 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml @@ -0,0 +1,1169 @@ +<UserControl + x:Class="Ryujinx.Ava.UI.Windows.ControllerSettingsWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + d:DesignHeight="800" + d:DesignWidth="800" + x:CompileBindings="False" + mc:Ignorable="d" + Focusable="True"> + <Design.DataContext> + <viewModels:ControllerSettingsViewModel /> + </Design.DataContext> + <UserControl.Resources> + <helpers:KeyValueConverter x:Key="Key" /> + </UserControl.Resources> + <StackPanel + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Orientation="Vertical"> + <Grid Margin="2,2,2,5" HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="0.5*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <Border + Grid.Column="0" + Margin="0,0,2,0" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + Padding="2,0"> + <StackPanel + Margin="2" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Vertical"> + <TextBlock + Margin="0,0,0,4" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsPlayer}" /> + <ComboBox + Name="PlayerIndexBox" + Width="150" + SelectionChanged="PlayerIndexBox_OnSelectionChanged" + Items="{Binding PlayerIndexes}" + SelectedIndex="{Binding PlayerId}"> + <ComboBox.ItemTemplate> + <DataTemplate> + <TextBlock Text="{Binding Name}" /> + </DataTemplate> + </ComboBox.ItemTemplate> + </ComboBox> + </StackPanel> + </Border> + + <!-- Main Controller Settings --> + <Border + Grid.Column="1" + Margin="0,0,2,0" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + Padding="2,0"> + <StackPanel + Margin="2" + HorizontalAlignment="Stretch" + VerticalAlignment="Center" + Orientation="Vertical"> + <TextBlock + Margin="0,0,0,5" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsInputDevice}" /> + <Grid HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <ComboBox + Name="DeviceBox" + HorizontalAlignment="Stretch" + Items="{Binding DeviceList}" + SelectedIndex="{Binding Device}" /> + <Button + Grid.Column="1" + MinWidth="0" + Margin="5,0,0,0" + VerticalAlignment="Center" + Command="{Binding LoadDevices}"> + <ui:SymbolIcon + Symbol="Refresh" + FontSize="15" + Height="20" /> + </Button> + </Grid> + </StackPanel> + </Border> + <Border + Grid.Column="2" + Margin="0,0,2,0" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + Padding="2,0"> + <Grid + Margin="2" + HorizontalAlignment="Stretch" + VerticalAlignment="Center"> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <TextBlock + Margin="0,0,0,4" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsControllerType}" /> + <ComboBox + Grid.Row="1" + HorizontalAlignment="Stretch" + Items="{Binding Controllers}" + SelectedIndex="{Binding Controller}"> + <ComboBox.ItemTemplate> + <DataTemplate> + <TextBlock Text="{Binding Name}" /> + </DataTemplate> + </ComboBox.ItemTemplate> + </ComboBox> + </Grid> + </Border> + <Border + Grid.Column="3" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + Padding="2,0" > + <StackPanel + Margin="2" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Vertical"> + <TextBlock + Margin="0,0,0,4" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsProfile}" /> + <StackPanel Orientation="Horizontal"> + <ui:ComboBox + IsEditable="True" + Name="ProfileBox" + Width="100" + SelectedIndex="0" + Items="{Binding ProfilesList}" + Text="{Binding ProfileName}" /> + <Button + MinWidth="0" + Margin="5,0,0,0" + VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale ControllerSettingsLoadProfileToolTip}" + Command="{Binding LoadProfile}"> + <ui:SymbolIcon + Symbol="Upload" + FontSize="15" + Height="20" /> + </Button> + <Button + MinWidth="0" + Margin="5,0,0,0" + VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale ControllerSettingsSaveProfileToolTip}" + Command="{Binding SaveProfile}"> + <ui:SymbolIcon + Symbol="Save" + FontSize="15" + Height="20" /> + </Button> + <Button + MinWidth="0" + Margin="5,0,0,0" + VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale ControllerSettingsRemoveProfileToolTip}" + Command="{Binding RemoveProfile}"> + <ui:SymbolIcon + Symbol="Delete" + FontSize="15" + Height="20" /> + </Button> + </StackPanel> + </StackPanel> + </Border> + </Grid> + + <!-- Button / JoyStick Settings --> + <Grid + Name="SettingButtons" + MinHeight="450" + IsVisible="{Binding ShowSettings}"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + + <!-- Left --> + <Grid + Margin="0,0,10,0" + Grid.Column="0" + VerticalAlignment="Stretch" + DockPanel.Dock="Left"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + + <!-- Left Triggers --> + <Border + Grid.Row="0" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + IsVisible="{Binding IsLeft}"> + <Grid Margin="10" HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <StackPanel + Margin="0,0,0,4" + Grid.Column="0" + Grid.Row="0" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsTriggerZL}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonZl, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Grid.Column="0" + Grid.Row="1" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsTriggerL}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonL, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Margin="0,0,0,4" + Grid.Column="1" + Grid.Row="0" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtonMinus}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonMinus, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </Grid> + </Border> + + <!-- Left Joystick --> + <Border + Grid.Row="1" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + IsVisible="{Binding IsLeft}"> + <StackPanel Margin="10" Orientation="Vertical"> + <TextBlock + Margin="0,0,0,10" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStick}" /> + + <!-- Left Joystick Keyboard --> + <StackPanel IsVisible="{Binding !IsController}" Orientation="Vertical"> + + <!-- Left Joystick Button --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickButton}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftKeyboardStickButton, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left Joystick Up --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickUp}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftStickUp, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left Joystick Down --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickDown}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftStickDown, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left Joystick Left --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickLeft}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftStickLeft, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left Joystick Right --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickRight}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftStickRight, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </StackPanel> + + <!-- Left Joystick Controller --> + <StackPanel IsVisible="{Binding IsController}" Orientation="Vertical"> + + <!-- Left Joystick Button --> + <StackPanel Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickButton}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftControllerStickButton, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left Joystick Stick --> + <StackPanel Margin="0,4,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLStickStick}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch" + Tag="stick"> + <TextBlock + Text="{Binding Configuration.LeftJoystick, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <CheckBox IsChecked="{Binding Configuration.LeftInvertStickX}"> + <TextBlock Text="{locale:Locale ControllerSettingsLStickInvertXAxis}" /> + </CheckBox> + <CheckBox IsChecked="{Binding Configuration.LeftInvertStickY}"> + <TextBlock Text="{locale:Locale ControllerSettingsLStickInvertYAxis}" /> + </CheckBox> + <CheckBox IsChecked="{Binding Configuration.LeftRotate90}"> + <TextBlock Text="{locale:Locale ControllerSettingsRotate90}" /> + </CheckBox> + <Separator Margin="0,4,0,4" Height="1" /> + <StackPanel Orientation="Vertical"> + <TextBlock Text="{locale:Locale ControllerSettingsLStickDeadzone}" /> + <StackPanel + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Horizontal"> + <Slider + Width="130" + Maximum="1" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Minimum="0" + Value="{Binding Configuration.DeadzoneLeft, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Text="{Binding Configuration.DeadzoneLeft, StringFormat=\{0:0.00\}}" /> + </StackPanel> + <TextBlock Text="{locale:Locale ControllerSettingsStickRange}" /> + <StackPanel + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Horizontal"> + <Slider + Width="130" + Maximum="2" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Minimum="0" + Value="{Binding Configuration.RangeLeft, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Text="{Binding Configuration.RangeLeft, StringFormat=\{0:0.00\}}" /> + </StackPanel> + </StackPanel> + </StackPanel> + </StackPanel> + </Border> + + <!-- Left DPad --> + <Border + Grid.Row="2" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + VerticalAlignment="Top" + IsVisible="{Binding IsLeft}"> + <StackPanel Margin="10" Orientation="Vertical"> + <TextBlock + Margin="0,0,0,10" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsDPad}" /> + <StackPanel Orientation="Vertical"> + <!-- Left DPad Up --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsDPadUp}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.DpadUp, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left DPad Down --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsDPadDown}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.DpadDown, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left DPad Left --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsDPadLeft}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.DpadLeft, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Left DPad Right --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsDPadRight}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.DpadRight, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </StackPanel> + </StackPanel> + </Border> + </Grid> + + <!-- Triggers And Side Buttons--> + <StackPanel Grid.Column="1" HorizontalAlignment="Stretch"> + <Border + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1"> + <StackPanel Margin="10" Orientation="Vertical"> + <TextBlock HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerThreshold}" /> + <StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> + <Slider + Width="130" + Maximum="1" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Minimum="0" + Value="{Binding Configuration.TriggerThreshold, Mode=TwoWay}" /> + <TextBlock Text="{Binding Configuration.TriggerThreshold, StringFormat=\{0:0.00\}}" /> + </StackPanel> + <StackPanel + Margin="0,4,0,0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Background="{DynamicResource ThemeDarkColor}" + IsVisible="{Binding !IsRight}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsLeftSR}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.LeftButtonSr, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Margin="0,4,0,0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + IsVisible="{Binding !IsRight}" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRightSL}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightButtonSl, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Margin="0,4,0,0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + IsVisible="{Binding !IsLeft}" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRightSR}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightButtonSr, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Margin="0,4,0,0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + IsVisible="{Binding !IsLeft}" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRightSL}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightButtonSl, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </StackPanel> + </Border> + + <!-- Controller Picture --> + <Image + Margin="0,10,0,0" + MaxHeight="250" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Source="{Binding Image}" /> + + <!-- Motion+Rumble --> + <StackPanel Margin="0,10,0,0" Orientation="Vertical" > + <Border + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + HorizontalAlignment="Stretch" + IsVisible="{Binding IsController}"> + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <CheckBox + Margin="10" + MinWidth="0" + Grid.Column="0" + IsChecked="{Binding Configuration.EnableMotion, Mode=TwoWay}"> + <TextBlock Text="{locale:Locale ControllerSettingsMotion}" /> + </CheckBox> + <Button Margin="10" Grid.Column="1" Command="{Binding ShowMotionConfig}"> + <TextBlock Text="{locale:Locale ControllerSettingsConfigureGeneral}" /> + </Button> + </Grid> + </Border> + <Border + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + HorizontalAlignment="Stretch" + IsVisible="{Binding IsController}"> + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <CheckBox + Margin="10" + MinWidth="0" + Grid.Column="0" + IsChecked="{Binding Configuration.EnableRumble, Mode=TwoWay}"> + <TextBlock Text="{locale:Locale ControllerSettingsRumble}" /> + </CheckBox> + <Button Margin="10" Grid.Column="1" Command="{Binding ShowRumbleConfig}"> + <TextBlock Text="{locale:Locale ControllerSettingsConfigureGeneral}" /> + </Button> + </Grid> + </Border> + </StackPanel> + </StackPanel> + + <!--Right Controls--> + <Grid + Margin="10,0,0,0" + Grid.Column="2" + VerticalAlignment="Top" + HorizontalAlignment="Stretch" > + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <Border + Grid.Row="0" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + IsVisible="{Binding IsRight}"> + <StackPanel Margin="10" Orientation="Vertical"> + <Grid HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <StackPanel + Margin="0,0,0,4" + Grid.Column="1" + Grid.Row="0" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsTriggerZR}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonZr, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Grid.Column="1" + Grid.Row="1" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsTriggerR}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonR, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel + Margin="0,0,8,4" + Grid.Column="0" + Grid.Row="0" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtonPlus}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonPlus, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </Grid> + </StackPanel> + </Border> + <Border + Grid.Row="1" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + IsVisible="{Binding IsRight}"> + <StackPanel Margin="10" Orientation="Vertical"> + <TextBlock + Margin="0,0,0,10" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtons}" /> + <Grid HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <!-- Right Buttons X --> + <StackPanel + Margin="0,0,0,4" + Grid.Column="0" + Grid.Row="0" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtonX}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonX, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <!-- Right Buttons Y --> + <StackPanel + Grid.Column="0" + Grid.Row="1" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtonY}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonY, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <!-- Right Buttons A --> + <StackPanel + Margin="0,0,0,4" + Grid.Column="1" + Grid.Row="0" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtonA}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonA, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <!-- Right Buttons B --> + <StackPanel + Grid.Column="1" + Grid.Row="1" + Background="{DynamicResource ThemeDarkColor}" + Orientation="Horizontal"> + <TextBlock + Width="20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsButtonB}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.ButtonB, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </Grid> + </StackPanel> + </Border> + <Border + Grid.Row="2" + Padding="10" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + IsVisible="{Binding IsRight}"> + <StackPanel Orientation="Vertical"> + <TextBlock + Margin="0,0,0,10" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStick}" /> + + <!-- Right Joystick Keyboard --> + <StackPanel IsVisible="{Binding !IsController}" Orientation="Vertical"> + + <!-- Right Joystick Button --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickButton}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightKeyboardStickButton, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Right Joystick Up --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickUp}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightStickUp, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Right Joystick Down --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickDown}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightStickDown, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Right Joystick Left --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickLeft}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightStickLeft, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + <!-- Right Joystick Right --> + <StackPanel Margin="0,0,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickRight}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightStickRight, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </StackPanel> + + <!-- Right Joystick Controller --> + <StackPanel IsVisible="{Binding IsController}" Orientation="Vertical"> + + <!-- Right Joystick Button --> + <StackPanel Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickButton}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch"> + <TextBlock + Text="{Binding Configuration.RightControllerStickButton, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + + + <!-- Right Joystick Stick --> + <StackPanel Margin="0,4,0,4" Background="{DynamicResource ThemeDarkColor}" Orientation="Horizontal"> + <TextBlock + Margin="0,0,10,0" + Width="120" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsRStickStick}" + TextAlignment="Center" /> + <ToggleButton + Width="90" + Height="27" + HorizontalAlignment="Stretch" + Tag="stick"> + <TextBlock + Text="{Binding Configuration.RightJoystick, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <CheckBox IsChecked="{Binding Configuration.RightInvertStickX}"> + <TextBlock Text="{locale:Locale ControllerSettingsRStickInvertXAxis}" /> + </CheckBox> + <CheckBox IsChecked="{Binding Configuration.RightInvertStickY}"> + <TextBlock Text="{locale:Locale ControllerSettingsRStickInvertYAxis}" /> + </CheckBox> + <CheckBox IsChecked="{Binding Configuration.RightRotate90}"> + <TextBlock Text="{locale:Locale ControllerSettingsRotate90}" /> + </CheckBox> + <Separator Margin="0,4,0,4" Height="1" /> + <StackPanel Orientation="Vertical"> + <TextBlock Text="{locale:Locale ControllerSettingsRStickDeadzone}" /> + <StackPanel + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Horizontal"> + <Slider + Width="130" + Maximum="1" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Padding="0" + VerticalAlignment="Center" + Minimum="0" + Value="{Binding Configuration.DeadzoneRight, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Text="{Binding Configuration.DeadzoneRight, StringFormat=\{0:0.00\}}" /> + </StackPanel> + <TextBlock Text="{locale:Locale ControllerSettingsStickRange}" /> + <StackPanel + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Horizontal"> + <Slider + Width="130" + Maximum="2" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Minimum="0" + Value="{Binding Configuration.RangeRight, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Text="{Binding Configuration.RangeRight, StringFormat=\{0:0.00\}}" /> + </StackPanel> + </StackPanel> + </StackPanel> + </StackPanel> + </Border> + </Grid> + + </Grid> + + </StackPanel> + +</UserControl>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs new file mode 100644 index 00000000..df083085 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs @@ -0,0 +1,181 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ControllerSettingsWindow : UserControl + { + private bool _dialogOpen; + + private ButtonKeyAssigner _currentAssigner; + internal ControllerSettingsViewModel ViewModel { get; set; } + + public ControllerSettingsWindow() + { + DataContext = ViewModel = new ControllerSettingsViewModel(this); + + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button && !(visual is CheckBox)) + { + button.Checked += Button_Checked; + button.Unchecked += Button_Unchecked; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + { + _currentAssigner.Cancel(); + } + } + + private void Button_Checked(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; + + if (_currentAssigner == null && (bool)button.IsChecked) + { + _currentAssigner = new ButtonKeyAssigner(button); + + FocusManager.Instance.Focus(this, NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(isStick); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.IsAssigned) + { + ViewModel.IsModified = true; + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + ToggleButton oldButton = _currentAssigner.ToggledButton; + + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + } + + public void SaveCurrentProfile() + { + ViewModel.Save(); + } + + private IButtonAssigner CreateButtonAssigner(bool forStick) + { + IButtonAssigner assigner; + + var device = ViewModel.Devices[ViewModel.Device]; + + if (device.Type == DeviceType.Keyboard) + { + assigner = new KeyboardKeyAssigner((IKeyboard)ViewModel.SelectedGamepad); + } + else if (device.Type == DeviceType.Controller) + { + assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (ViewModel.Config as StandardControllerInputConfig).TriggerThreshold, forStick); + } + else + { + throw new Exception("Controller not supported"); + } + + return assigner; + } + + private void Button_Unchecked(object sender, RoutedEventArgs e) + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = false; + + if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed) + { + shouldUnbind = true; + } + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ViewModel.IsModified && !_dialogOpen) + { + _dialogOpen = true; + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance["DialogControllerSettingsModifiedConfirmMessage"], + LocaleManager.Instance["DialogControllerSettingsModifiedConfirmSubMessage"], + LocaleManager.Instance["InputDialogYes"], + LocaleManager.Instance["InputDialogNo"], + LocaleManager.Instance["RyujinxConfirm"]); + + if (result == UserResult.Yes) + { + ViewModel.Save(); + } + + _dialogOpen = false; + + ViewModel.IsModified = false; + + if (e.AddedItems.Count > 0) + { + var player = (PlayerModel)e.AddedItems[0]; + ViewModel.PlayerId = player.Id; + } + } + } + + public void Dispose() + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + ViewModel.Dispose(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml new file mode 100644 index 00000000..e524d6e4 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -0,0 +1,172 @@ +<window:StyleableWindow + x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + Width="800" + Height="500" + MinWidth="800" + MinHeight="500" + MaxWidth="800" + MaxHeight="500" + SizeToContent="Height" + WindowStartupLocation="CenterOwner" + mc:Ignorable="d" + Focusable="True"> + <Grid Name="DownloadableContentGrid" Margin="15"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock + Name="Heading" + Grid.Row="1" + MaxWidth="500" + Margin="20,15,20,20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + LineHeight="18" + TextAlignment="Center" + TextWrapping="Wrap" /> + <DockPanel + Grid.Row="2" + Margin="0" + HorizontalAlignment="Left"> + <Button + Name="EnableAllButton" + MinWidth="90" + Margin="5" + Command="{Binding EnableAll}"> + <TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" /> + </Button> + <Button + Name="DisableAllButton" + MinWidth="90" + Margin="5" + Command="{Binding DisableAll}"> + <TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" /> + </Button> + </DockPanel> + <Border + Grid.Row="3" + Margin="5" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + BorderBrush="Gray" + BorderThickness="1"> + <ScrollViewer + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Auto" + VerticalScrollBarVisibility="Auto"> + <DataGrid + Name="DlcDataGrid" + MinHeight="200" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + CanUserReorderColumns="False" + CanUserResizeColumns="True" + CanUserSortColumns="True" + HorizontalScrollBarVisibility="Auto" + Items="{Binding _downloadableContents}" + SelectionMode="Extended" + VerticalScrollBarVisibility="Auto"> + <DataGrid.Styles> + <Styles> + <Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)"> + <Setter Property="HorizontalAlignment" Value="Left" /> + <Setter Property="HorizontalContentAlignment" Value="Left" /> + </Style> + </Styles> + <Styles> + <Style Selector="DataGridCell:nth-child(1)"> + <Setter Property="HorizontalAlignment" Value="Right" /> + <Setter Property="HorizontalContentAlignment" Value="Right" /> + </Style> + </Styles> + </DataGrid.Styles> + <DataGrid.Columns> + <DataGridTemplateColumn Width="90"> + <DataGridTemplateColumn.CellTemplate> + <DataTemplate> + <CheckBox + Width="50" + MinWidth="40" + HorizontalAlignment="Center" + IsChecked="{Binding Enabled}" /> + </DataTemplate> + </DataGridTemplateColumn.CellTemplate> + <DataGridTemplateColumn.Header> + <TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" /> + </DataGridTemplateColumn.Header> + </DataGridTemplateColumn> + <DataGridTextColumn Width="140" Binding="{Binding TitleId}"> + <DataGridTextColumn.Header> + <TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" /> + </DataGridTextColumn.Header> + </DataGridTextColumn> + <DataGridTextColumn Width="280" Binding="{Binding FullPath}"> + <DataGridTextColumn.Header> + <TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" /> + </DataGridTextColumn.Header> + </DataGridTextColumn> + <DataGridTextColumn Binding="{Binding ContainerPath}"> + <DataGridTextColumn.Header> + <TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" /> + </DataGridTextColumn.Header> + </DataGridTextColumn> + </DataGrid.Columns> + </DataGrid> + </ScrollViewer> + </Border> + <DockPanel + Grid.Row="4" + Margin="0" + HorizontalAlignment="Stretch"> + <DockPanel Margin="0" HorizontalAlignment="Left"> + <Button + Name="AddButton" + MinWidth="90" + Margin="5" + Command="{Binding Add}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" /> + </Button> + <Button + Name="RemoveButton" + MinWidth="90" + Margin="5" + Command="{Binding RemoveSelected}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" /> + </Button> + <Button + Name="RemoveAllButton" + MinWidth="90" + Margin="5" + Command="{Binding RemoveAll}"> + <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" /> + </Button> + </DockPanel> + <DockPanel Margin="0" HorizontalAlignment="Right"> + <Button + Name="SaveButton" + MinWidth="90" + Margin="5" + Command="{Binding SaveAndClose}"> + <TextBlock Text="{locale:Locale SettingsButtonSave}" /> + </Button> + <Button + Name="CancelButton" + MinWidth="90" + Margin="5" + Command="{Binding Close}"> + <TextBlock Text="{locale:Locale InputDialogCancel}" /> + </Button> + </DockPanel> + </DockPanel> + </Grid> +</window:StyleableWindow>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs new file mode 100644 index 00000000..11be9383 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs @@ -0,0 +1,314 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class DownloadableContentManagerWindow : StyleableWindow + { + private readonly List<DownloadableContentContainer> _downloadableContentContainerList; + private readonly string _downloadableContentJsonPath; + + private VirtualFileSystem _virtualFileSystem { get; } + private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; } + + private ulong _titleId { get; } + private string _titleName { get; } + + public DownloadableContentManagerWindow() + { + DataContext = this; + + InitializeComponent(); + + Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["DlcWindowTitle"]} - {_titleName} ({_titleId:X16})"; + } + + public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + _virtualFileSystem = virtualFileSystem; + _downloadableContents = new AvaloniaList<DownloadableContentModel>(); + + _titleId = titleId; + _titleName = titleName; + + _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); + + try + { + _downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath); + } + catch + { + _downloadableContentContainerList = new List<DownloadableContentContainer>(); + } + + DataContext = this; + + InitializeComponent(); + + RemoveButton.IsEnabled = false; + + DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged; + + Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["DlcWindowTitle"]} - {_titleName} ({_titleId:X16})"; + + LoadDownloadableContents(); + PrintHeading(); + } + + private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0); + } + + private void PrintHeading() + { + Heading.Text = string.Format(LocaleManager.Instance["DlcWindowHeading"], _downloadableContents.Count, _titleName, _titleId.ToString("X16")); + } + + private void LoadDownloadableContents() + { + foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) + { + if (File.Exists(downloadableContentContainer.ContainerPath)) + { + using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); + + PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + + _virtualFileSystem.ImportTickets(partitionFileSystem); + + foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList) + { + using UniqueRef<IFile> ncaFile = new(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref(), downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath); + if (nca != null) + { + _downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), + downloadableContentContainer.ContainerPath, + downloadableContentNca.FullPath, + downloadableContentNca.Enabled)); + } + } + } + } + + // NOTE: Save the list again to remove leftovers. + Save(); + } + + private Nca TryOpenNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(_virtualFileSystem.KeySet, ncaStorage); + } + catch (Exception ex) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath)); + }); + } + + return null; + } + + private async Task AddDownloadableContent(string path) + { + if (!File.Exists(path) || _downloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) + { + return; + } + + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + bool containsDownloadableContent = false; + + _virtualFileSystem.ImportTickets(partitionFileSystem); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef<IFile>(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path); + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) + { + break; + } + + _downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true)); + + containsDownloadableContent = true; + } + } + + if (!containsDownloadableContent) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]); + } + } + + private void RemoveDownloadableContents(bool removeSelectedOnly = false) + { + if (removeSelectedOnly) + { + AvaloniaList<DownloadableContentModel> removedItems = new(); + + foreach (var item in DlcDataGrid.SelectedItems) + { + removedItems.Add(item as DownloadableContentModel); + } + + DlcDataGrid.SelectedItems.Clear(); + + foreach (var item in removedItems) + { + _downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList()); + } + } + else + { + _downloadableContents.Clear(); + } + + PrintHeading(); + } + + public void RemoveSelected() + { + RemoveDownloadableContents(true); + } + + public void RemoveAll() + { + RemoveDownloadableContents(); + } + + public void EnableAll() + { + foreach(var item in _downloadableContents) + { + item.Enabled = true; + } + } + + public void DisableAll() + { + foreach (var item in _downloadableContents) + { + item.Enabled = false; + } + } + + public async void Add() + { + OpenFileDialog dialog = new OpenFileDialog() + { + Title = LocaleManager.Instance["SelectDlcDialogTitle"], + AllowMultiple = true + }; + + dialog.Filters.Add(new FileDialogFilter + { + Name = "NSP", + Extensions = { "nsp" } + }); + + string[] files = await dialog.ShowAsync(this); + + if (files != null) + { + foreach (string file in files) + { + await AddDownloadableContent(file); + } + } + + PrintHeading(); + } + + public void Save() + { + _downloadableContentContainerList.Clear(); + + DownloadableContentContainer container = default; + + foreach (DownloadableContentModel downloadableContent in _downloadableContents) + { + if (container.ContainerPath != downloadableContent.ContainerPath) + { + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + _downloadableContentContainerList.Add(container); + } + + container = new DownloadableContentContainer + { + ContainerPath = downloadableContent.ContainerPath, + DownloadableContentNcaList = new List<DownloadableContentNca>() + }; + } + + container.DownloadableContentNcaList.Add(new DownloadableContentNca + { + Enabled = downloadableContent.Enabled, + TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16), + FullPath = downloadableContent.FullPath + }); + } + + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + _downloadableContentContainerList.Add(container); + } + + using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough)) + { + downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true))); + } + } + + public void SaveAndClose() + { + Save(); + Close(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/IconColorPicker.cs b/Ryujinx.Ava/UI/Windows/IconColorPicker.cs new file mode 100644 index 00000000..9dca83eb --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/IconColorPicker.cs @@ -0,0 +1,192 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Windows +{ + static class IconColorPicker + { + private const int ColorsPerLine = 64; + private const int TotalColors = ColorsPerLine * ColorsPerLine; + + private const int UvQuantBits = 3; + private const int UvQuantShift = BitsPerComponent - UvQuantBits; + + private const int SatQuantBits = 5; + private const int SatQuantShift = BitsPerComponent - SatQuantBits; + + private const int BitsPerComponent = 8; + + private const int CutOffLuminosity = 64; + + private readonly struct PaletteColor + { + public int Qck { get; } + public byte R { get; } + public byte G { get; } + public byte B { get; } + + public PaletteColor(int qck, byte r, byte g, byte b) + { + Qck = qck; + R = r; + G = g; + B = b; + } + } + + public static Color GetFilteredColor(Image<Bgra32> image) + { + var color = GetColor(image).ToPixel<Bgra32>(); + + // We don't want colors that are too dark. + // If the color is too dark, make it brighter by reducing the range + // and adding a constant color. + int luminosity = GetColorApproximateLuminosity(color.R, color.G, color.B); + if (luminosity < CutOffLuminosity) + { + color = Color.FromRgb( + (byte)Math.Min(CutOffLuminosity + color.R, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.G, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.B, byte.MaxValue)); + } + + return color; + } + + public static Color GetColor(Image<Bgra32> image) + { + var colors = new PaletteColor[TotalColors]; + + var dominantColorBin = new Dictionary<int, int>(); + + var buffer = GetBuffer(image); + + int w = image.Width; + + int w8 = w << 8; + int h8 = image.Height << 8; + + int xStep = w8 / ColorsPerLine; + int yStep = h8 / ColorsPerLine; + + int i = 0; + int maxHitCount = 0; + + for (int y = 0; y < image.Height; y++) + { + int yOffset = y * image.Width; + + for (int x = 0; x < image.Width && i < TotalColors; x++) + { + int offset = x + yOffset; + + byte cb = buffer[offset].B; + byte cg = buffer[offset].G; + byte cr = buffer[offset].R; + + var qck = GetQuantizedColorKey(cr, cg, cb); + + if (dominantColorBin.TryGetValue(qck, out int hitCount)) + { + dominantColorBin[qck] = hitCount + 1; + + if (maxHitCount < hitCount) + { + maxHitCount = hitCount; + } + } + else + { + dominantColorBin.Add(qck, 1); + } + + colors[i++] = new PaletteColor(qck, cr, cg, cb); + } + } + + int highScore = -1; + PaletteColor bestCandidate = default; + + for (i = 0; i < TotalColors; i++) + { + var score = GetColorScore(dominantColorBin, maxHitCount, colors[i]); + + if (highScore < score) + { + highScore = score; + bestCandidate = colors[i]; + } + } + + return Color.FromRgb(bestCandidate.R, bestCandidate.G, bestCandidate.B); + } + + public static Bgra32[] GetBuffer(Image<Bgra32> image) + { + return image.TryGetSinglePixelSpan(out var data) ? data.ToArray() : new Bgra32[0]; + } + + private static int GetColorScore(Dictionary<int, int> dominantColorBin, int maxHitCount, PaletteColor color) + { + var hitCount = dominantColorBin[color.Qck]; + var balancedHitCount = BalanceHitCount(hitCount, maxHitCount); + var quantSat = (GetColorSaturation(color) >> SatQuantShift) << SatQuantShift; + var value = GetColorValue(color); + + // If the color is rarely used on the image, + // then chances are that theres a better candidate, even if the saturation value + // is high. By multiplying the saturation value with a weight, we can lower + // it if the color is almost never used (hit count is low). + var satWeighted = quantSat; + var satWeight = balancedHitCount << 5; + if (satWeight < 0x100) + { + satWeighted = (satWeighted * satWeight) >> 8; + } + + // Compute score from saturation and dominance of the color. + // We prefer more vivid colors over dominant ones, so give more weight to the saturation. + var score = ((satWeighted << 1) + balancedHitCount) * value; + + return score; + } + + private static int BalanceHitCount(int hitCount, int maxHitCount) + { + return (hitCount << 8) / maxHitCount; + } + + private static int GetColorApproximateLuminosity(byte r, byte g, byte b) + { + return (r + g + b) / 3; + } + + private static int GetColorSaturation(PaletteColor color) + { + int cMax = Math.Max(Math.Max(color.R, color.G), color.B); + + if (cMax == 0) + { + return 0; + } + + int cMin = Math.Min(Math.Min(color.R, color.G), color.B); + int delta = cMax - cMin; + return (delta << 8) / cMax; + } + + private static int GetColorValue(PaletteColor color) + { + return Math.Max(Math.Max(color.R, color.G), color.B); + } + + private static int GetQuantizedColorKey(byte r, byte g, byte b) + { + int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + return (v >> UvQuantShift) | ((u >> UvQuantShift) << UvQuantBits); + } + } +} diff --git a/Ryujinx.Ava/UI/Windows/MainWindow.axaml b/Ryujinx.Ava/UI/Windows/MainWindow.axaml new file mode 100644 index 00000000..1eb42279 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/MainWindow.axaml @@ -0,0 +1,770 @@ +<window:StyleableWindow + x:Class="Ryujinx.Ava.UI.Windows.MainWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + Title="Ryujinx" + Width="1280" + Height="777" + MinWidth="1092" + MinHeight="672" + d:DesignHeight="720" + d:DesignWidth="1280" + x:CompileBindings="True" + x:DataType="viewModels:MainWindowViewModel" + WindowStartupLocation="CenterScreen" + mc:Ignorable="d" + Focusable="True"> + <Window.Styles> + <Style Selector="TitleBar:fullscreen"> + <Setter Property="Background" Value="#000000" /> + </Style> + </Window.Styles> + <Design.DataContext> + <viewModels:MainWindowViewModel /> + </Design.DataContext> + <Window.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + </Window.Resources> + <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <helpers:OffscreenTextBox Name="HiddenTextBox" Grid.Row="0" /> + <StackPanel Grid.Row="0" IsVisible="False"> + <helpers:HotKeyControl Name="FullscreenHotKey" Command="{ReflectionBinding ToggleFullscreen}" /> + <helpers:HotKeyControl Name="FullscreenHotKey2" Command="{ReflectionBinding ToggleFullscreen}" /> + <helpers:HotKeyControl Name="DockToggleHotKey" Command="{ReflectionBinding ToggleDockMode}" /> + <helpers:HotKeyControl Name="ExitHotKey" Command="{ReflectionBinding ExitCurrentState}" /> + </StackPanel> + <Grid + Grid.Row="1" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <StackPanel + Name="MenuBar" + MinHeight="35" + Grid.Row="0" + Margin="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + IsVisible="{Binding ShowMenuAndStatusBar}" + Orientation="Vertical"> + <DockPanel HorizontalAlignment="Stretch"> + <Menu + Name="Menu" + Height="35" + Margin="0" + HorizontalAlignment="Left"> + <Menu.ItemsPanel> + <ItemsPanelTemplate> + <DockPanel Margin="0" HorizontalAlignment="Stretch" /> + </ItemsPanelTemplate> + </Menu.ItemsPanel> + <MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarFile}"> + <MenuItem + Command="{ReflectionBinding OpenFile}" + Header="{locale:Locale MenuBarFileOpenFromFile}" + IsEnabled="{Binding EnableNonGameRunningControls}" + ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" /> + <MenuItem + Command="{ReflectionBinding OpenFolder}" + Header="{locale:Locale MenuBarFileOpenUnpacked}" + IsEnabled="{Binding EnableNonGameRunningControls}" + ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" /> + <MenuItem Header="{locale:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}"> + <MenuItem + Command="{ReflectionBinding OpenMiiApplet}" + Header="Mii Edit Applet" + ToolTip.Tip="{locale:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" /> + </MenuItem> + <Separator /> + <MenuItem + Command="{ReflectionBinding OpenRyujinxFolder}" + Header="{locale:Locale MenuBarFileOpenEmuFolder}" + ToolTip.Tip="{locale:Locale OpenRyujinxFolderTooltip}" /> + <MenuItem + Command="{ReflectionBinding OpenLogsFolder}" + Header="{locale:Locale MenuBarFileOpenLogsFolder}" + ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" /> + <Separator /> + <MenuItem + Command="{ReflectionBinding CloseWindow}" + Header="{locale:Locale MenuBarFileExit}" + ToolTip.Tip="{locale:Locale ExitTooltip}" /> + </MenuItem> + <MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarOptions}"> + <MenuItem + Command="{ReflectionBinding ToggleFullscreen}" + Header="{locale:Locale MenuBarOptionsToggleFullscreen}" + InputGesture="F11" /> + <MenuItem> + <MenuItem.Icon> + <CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}" + MinWidth="250"> + <TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/> + </CheckBox> + </MenuItem.Icon> + </MenuItem> + <MenuItem IsVisible="{Binding ShowConsoleVisible}"> + <MenuItem.Icon> + <CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}" + MinWidth="250"> + <TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/> + </CheckBox> + </MenuItem.Icon> + </MenuItem> + <Separator /> + <MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}"> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="de_DE" + Header="Deutsch" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="en_US" + Header="English (US)" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="es_ES" + Header="Español (ES)" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="fr_FR" + Header="Français" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="it_IT" + Header="Italiano" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="pt_BR" + Header="Português (BR)" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="tr_TR" + Header="Türkçe" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="el_GR" + Header="Ελληνικά" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="pl_PL" + Header="Polski" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="ru_RU" + Header="Русский" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="zh_CN" + Header="简体中文" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="zh_TW" + Header="繁體中文" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="ja_JP" + Header="日本語" /> + <MenuItem + Command="{ReflectionBinding ChangeLanguage}" + CommandParameter="ko_KR" + Header="한국어" /> + </MenuItem> + <Separator /> + <MenuItem + Command="{ReflectionBinding OpenSettings}" + Header="{locale:Locale MenuBarOptionsSettings}" + ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" /> + <MenuItem + Command="{ReflectionBinding ManageProfiles}" + Header="{locale:Locale MenuBarOptionsManageUserProfiles}" + IsEnabled="{Binding EnableNonGameRunningControls}" + ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" /> + </MenuItem> + <MenuItem + Name="ActionsMenuItem" + VerticalAlignment="Center" + Header="{locale:Locale MenuBarActions}" + IsEnabled="{Binding IsGameRunning}"> + <MenuItem + Click="PauseEmulation_Click" + Header="{locale:Locale MenuBarOptionsPauseEmulation}" + InputGesture="{Binding PauseKey}" + IsEnabled="{Binding !IsPaused}" + IsVisible="{Binding !IsPaused}" /> + <MenuItem + Click="ResumeEmulation_Click" + Header="{locale:Locale MenuBarOptionsResumeEmulation}" + InputGesture="{Binding PauseKey}" + IsEnabled="{Binding IsPaused}" + IsVisible="{Binding IsPaused}" /> + <MenuItem + Click="StopEmulation_Click" + Header="{locale:Locale MenuBarOptionsStopEmulation}" + InputGesture="Escape" + IsEnabled="{Binding IsGameRunning}" + ToolTip.Tip="{locale:Locale StopEmulationTooltip}" /> + <MenuItem Command="{ReflectionBinding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" /> + <Separator /> + <MenuItem + Name="ScanAmiiboMenuItem" + AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree" + Command="{ReflectionBinding OpenAmiiboWindow}" + Header="{locale:Locale MenuBarActionsScanAmiibo}" + IsEnabled="{Binding IsAmiiboRequested}" /> + <MenuItem + Command="{ReflectionBinding TakeScreenshot}" + Header="{locale:Locale MenuBarFileToolsTakeScreenshot}" + InputGesture="{Binding ScreenshotKey}" + IsEnabled="{Binding IsGameRunning}" /> + <MenuItem + Command="{ReflectionBinding HideUi}" + Header="{locale:Locale MenuBarFileToolsHideUi}" + InputGesture="{Binding ShowUiKey}" + IsEnabled="{Binding IsGameRunning}" /> + <MenuItem + Command="{ReflectionBinding OpenCheatManagerForCurrentApp}" + Header="{locale:Locale GameListContextMenuManageCheat}" + IsEnabled="{Binding IsGameRunning}" /> + </MenuItem> + <MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}"> + <MenuItem Header="{locale:Locale MenuBarToolsInstallFirmware}" IsEnabled="{Binding EnableNonGameRunningControls}"> + <MenuItem Command="{ReflectionBinding InstallFirmwareFromFile}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromFile}" /> + <MenuItem Command="{ReflectionBinding InstallFirmwareFromFolder}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" /> + </MenuItem> + </MenuItem> + <MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarHelp}"> + <MenuItem + Name="UpdateMenuItem" + Command="{ReflectionBinding CheckForUpdates}" + Header="{locale:Locale MenuBarHelpCheckForUpdates}" + ToolTip.Tip="{locale:Locale CheckUpdatesTooltip}" /> + <Separator /> + <MenuItem + Command="{ReflectionBinding OpenAboutWindow}" + Header="{locale:Locale MenuBarHelpAbout}" + ToolTip.Tip="{locale:Locale OpenAboutTooltip}" /> + </MenuItem> + </Menu> + </DockPanel> + </StackPanel> + <ContentControl + Name="MainContent" + Grid.Row="1" + Padding="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="0,0,0,0" + DockPanel.Dock="Top" + IsVisible="{Binding ShowContent}"> + <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <DockPanel + Grid.Row="0" + Margin="0,0,0,5" + HorizontalAlignment="Stretch"> + <Button + Width="40" + MinWidth="40" + Margin="5,2,0,2" + VerticalAlignment="Stretch" + Command="{ReflectionBinding SetListMode}" + IsEnabled="{Binding IsGrid}"> + <ui:FontIcon + Margin="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Center" + FontFamily="avares://FluentAvalonia/Fonts#Symbols" + Glyph="{helpers:GlyphValueConverter List}" /> + </Button> + <Button + Width="40" + MinWidth="40" + Margin="5,2,5,2" + VerticalAlignment="Stretch" + Command="{ReflectionBinding SetGridMode}" + IsEnabled="{Binding IsList}"> + <ui:FontIcon + Margin="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Center" + FontFamily="avares://FluentAvalonia/Fonts#Symbols" + Glyph="{helpers:GlyphValueConverter Grid}" /> + </Button> + <TextBlock + Margin="10,0" + VerticalAlignment="Center" + Text="{locale:Locale IconSize}" + ToolTip.Tip="{locale:Locale IconSizeTooltip}" /> + <Slider + Width="150" + Height="35" + Margin="5,-10,5,0" + VerticalAlignment="Center" + IsSnapToTickEnabled="True" + Maximum="4" + Minimum="1" + TickFrequency="1" + ToolTip.Tip="{locale:Locale IconSizeTooltip}" + Value="{Binding GridSizeScale}" /> + <CheckBox + Margin="0" + VerticalAlignment="Center" + IsChecked="{Binding ShowNames, Mode=TwoWay}" + IsVisible="{Binding IsGrid}"> + <TextBlock Margin="5,3,0,0" Text="{locale:Locale CommonShowNames}" /> + </CheckBox> + <TextBox + Name="SearchBox" + MinWidth="200" + Margin="5,0,5,0" + HorizontalAlignment="Right" + VerticalAlignment="Center" + DockPanel.Dock="Right" + KeyUp="SearchBox_OnKeyUp" + Text="{Binding SearchText}" + Watermark="{locale:Locale MenuSearch}" /> + <ui:DropDownButton + Width="150" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Content="{Binding SortName}" + DockPanel.Dock="Right"> + <ui:DropDownButton.Flyout> + <Flyout Placement="Bottom"> + <StackPanel + Margin="0" + HorizontalAlignment="Stretch" + Orientation="Vertical"> + <StackPanel> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale CommonFavorite}" + GroupName="Sort" + IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}" + Tag="Favorite" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderApplication}" + GroupName="Sort" + IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" + Tag="Title" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderDeveloper}" + GroupName="Sort" + IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}" + Tag="Developer" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderTimePlayed}" + GroupName="Sort" + IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}" + Tag="TotalTimePlayed" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderLastPlayed}" + GroupName="Sort" + IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}" + Tag="LastPlayed" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderFileExtension}" + GroupName="Sort" + IsChecked="{Binding IsSortedByType, Mode=OneTime}" + Tag="FileType" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderFileSize}" + GroupName="Sort" + IsChecked="{Binding IsSortedBySize, Mode=OneTime}" + Tag="FileSize" /> + <RadioButton + Checked="Sort_Checked" + Content="{locale:Locale GameListHeaderPath}" + GroupName="Sort" + IsChecked="{Binding IsSortedByPath, Mode=OneTime}" + Tag="Path" /> + </StackPanel> + <Border + Width="60" + Height="2" + Margin="5" + HorizontalAlignment="Stretch" + BorderBrush="White" + BorderThickness="0,1,0,0"> + <Separator Height="0" HorizontalAlignment="Stretch" /> + </Border> + <RadioButton + Checked="Order_Checked" + Content="{locale:Locale OrderAscending}" + GroupName="Order" + IsChecked="{Binding IsAscending, Mode=OneTime}" + Tag="Ascending" /> + <RadioButton + Checked="Order_Checked" + Content="{locale:Locale OrderDescending}" + GroupName="Order" + IsChecked="{Binding !IsAscending, Mode=OneTime}" + Tag="Descending" /> + </StackPanel> + </Flyout> + </ui:DropDownButton.Flyout> + </ui:DropDownButton> + <TextBlock + Margin="10,0" + HorizontalAlignment="Right" + VerticalAlignment="Center" + DockPanel.Dock="Right" + Text="{locale:Locale CommonSort}" /> + </DockPanel> + <controls:GameListView + x:Name="GameList" + Grid.Row="1" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalContentAlignment="Stretch" + VerticalContentAlignment="Stretch" + IsVisible="{Binding IsList}" /> + <controls:GameGridView + x:Name="GameGrid" + Grid.Row="1" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalContentAlignment="Stretch" + VerticalContentAlignment="Stretch" + IsVisible="{Binding IsGrid}" /> + </Grid> + </ContentControl> + <Grid + Grid.Row="1" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Background="{DynamicResource ThemeContentBackgroundColor}" + IsVisible="{Binding ShowLoadProgress}" + Name="LoadingView" + ZIndex="1000"> + <Grid + Margin="40" + HorizontalAlignment="Center" + VerticalAlignment="Center" + IsVisible="{Binding ShowLoadProgress}"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + <Border + Grid.RowSpan="2" + Grid.Column="0" + Width="256" + Height="256" + Margin="10" + Padding="4" + BorderBrush="Black" + BorderThickness="2" + BoxShadow="4 4 32 8 #40000000" + CornerRadius="3" + IsVisible="{Binding ShowLoadProgress}"> + <Image + Width="256" + Height="256" + IsVisible="{Binding ShowLoadProgress}" + Source="{Binding SelectedIcon, Converter={StaticResource ByteImage}}" /> + </Border> + <Grid + Grid.Column="1" + HorizontalAlignment="Stretch" + VerticalAlignment="Center" + IsVisible="{Binding ShowLoadProgress}"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock + Grid.Row="0" + Margin="10" + FontSize="30" + FontWeight="Bold" + IsVisible="{Binding ShowLoadProgress}" + Text="{Binding LoadHeading}" + TextAlignment="Left" + TextWrapping="Wrap" /> + <Border + Grid.Row="1" + Margin="10" + Padding="0" + HorizontalAlignment="Stretch" + BorderBrush="{Binding ProgressBarBackgroundColor}" + BorderThickness="1" + ClipToBounds="True" + CornerRadius="5" + IsVisible="{Binding ShowLoadProgress}"> + <ProgressBar + Height="10" + MinWidth="500" + Margin="0" + Padding="0" + HorizontalAlignment="Stretch" + Background="{Binding ProgressBarBackgroundColor}" + ClipToBounds="True" + CornerRadius="5" + Foreground="{Binding ProgressBarForegroundColor}" + IsIndeterminate="{Binding IsLoadingIndeterminate}" + IsVisible="{Binding ShowLoadProgress}" + Maximum="{Binding ProgressMaximum}" + Minimum="0" + Value="{Binding ProgressValue}" /> + </Border> + <TextBlock + Grid.Row="2" + Margin="10" + FontSize="18" + IsVisible="{Binding ShowLoadProgress}" + Text="{Binding CacheLoadStatus}" + TextAlignment="Left" /> + </Grid> + </Grid> + </Grid> + <Grid + Name="StatusBar" + Grid.Row="2" + Margin="0" + MinHeight="22" + HorizontalAlignment="Stretch" + VerticalAlignment="Bottom" + Background="{DynamicResource ThemeContentBackgroundColor}" + DockPanel.Dock="Bottom" + IsVisible="{Binding ShowMenuAndStatusBar}"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <StackPanel + Grid.Column="0" + Margin="5" + VerticalAlignment="Center" + IsVisible="{Binding EnableNonGameRunningControls}"> + <Grid Margin="0"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Button + Width="25" + Height="25" + MinWidth="0" + Margin="0,0,5,0" + VerticalAlignment="Center" + Background="Transparent" + Command="{ReflectionBinding LoadApplications}"> + <ui:SymbolIcon + Width="50" + Height="100" + Symbol="Refresh" /> + </Button> + <TextBlock + Name="LoadStatus" + Grid.Column="1" + Margin="0,0,5,0" + VerticalAlignment="Center" + IsVisible="{Binding EnableNonGameRunningControls}" + Text="{locale:Locale StatusBarGamesLoaded}" /> + <ProgressBar + Name="LoadProgressBar" + Grid.Column="2" + Height="6" + VerticalAlignment="Center" + Foreground="{DynamicResource HighlightColor}" + IsVisible="{Binding EnableNonGameRunningControls}" + Maximum="{Binding StatusBarProgressMaximum}" + Value="{Binding StatusBarProgressValue}" /> + </Grid> + </StackPanel> + <StackPanel + Grid.Column="1" + Margin="0,2" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding IsGameRunning}" + Orientation="Horizontal"> + <TextBlock + Name="VsyncStatus" + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + Foreground="{Binding VsyncColor}" + IsVisible="{Binding !ShowLoadProgress}" + PointerReleased="VsyncStatus_PointerReleased" + Text="VSync" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Name="DockedStatus" + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + PointerReleased="DockedStatus_PointerReleased" + Text="{Binding DockedStatusText}" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Name="AspectRatioStatus" + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + PointerReleased="AspectRatioStatus_PointerReleased" + Text="{Binding AspectRatioStatusText}" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <ui:ToggleSplitButton + Name="VolumeStatus" + Padding="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + VerticalContentAlignment="Center" + Background="{DynamicResource ThemeContentBackgroundColor}" + BorderThickness="0" + Content="{Binding VolumeStatusText}" + IsChecked="{Binding VolumeMuted}" + IsVisible="{Binding !ShowLoadProgress}"> + <ui:ToggleSplitButton.Flyout> + <Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway"> + <Grid Margin="0"> + <Slider + MaxHeight="40" + Width="150" + Margin="0" + Padding="0" + IsSnapToTickEnabled="True" + LargeChange="0.05" + Maximum="1" + Minimum="0" + SmallChange="0.01" + TickFrequency="0.05" + ToolTip.Tip="{locale:Locale AudioVolumeTooltip}" + Value="{Binding Volume}" /> + </Grid> + </Flyout> + </ui:ToggleSplitButton.Flyout> + </ui:ToggleSplitButton> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + Text="{Binding GameStatusText}" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + Text="{Binding FifoStatusText}" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + Text="{Binding BackendText}" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + Text="{Binding GpuNameText}" + TextAlignment="Left" /> + </StackPanel> + <StackPanel + Grid.Column="3" + Margin="0,0,5,0" + VerticalAlignment="Center" + IsVisible="{Binding ShowFirmwareStatus}" + Orientation="Horizontal"> + <TextBlock + Name="FirmwareStatus" + Margin="0" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Text="{locale:Locale StatusBarSystemVersion}" /> + </StackPanel> + </Grid> + </Grid> + </Grid> +</window:StyleableWindow> diff --git a/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs new file mode 100644 index 00000000..08332da8 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -0,0 +1,721 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +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.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration; +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.SDL2; +using Ryujinx.Modules; +using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common; +using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.Common.Helper; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.ComponentModel; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using InputManager = Ryujinx.Input.HLE.InputManager; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class MainWindow : StyleableWindow + { + internal static MainWindowViewModel MainWindowViewModel { get; private set; } + private bool _canUpdate; + private bool _isClosing; + private bool _isLoading; + + private Control _mainViewContent; + + private UserChannelPersistence _userChannelPersistence; + private static bool _deferLoad; + private static string _launchPath; + private static bool _startFullscreen; + private string _currentEmulatedGamePath; + internal readonly AvaHostUiHandler UiHandler; + private AutoResetEvent _rendererWaitEvent; + + public VirtualFileSystem VirtualFileSystem { get; private set; } + public ContentManager ContentManager { get; private set; } + public AccountManager AccountManager { get; private set; } + + public LibHacHorizonManager LibHacHorizonManager { get; private set; } + + internal AppHost AppHost { get; private set; } + public InputManager InputManager { get; private set; } + + internal RendererHost RendererControl { get; private set; } + internal MainWindowViewModel ViewModel { get; private set; } + public SettingsWindow SettingsWindow { get; set; } + + public bool CanUpdate + { + get => _canUpdate; + set + { + _canUpdate = value; + + Dispatcher.UIThread.InvokeAsync(() => UpdateMenuItem.IsEnabled = _canUpdate); + } + } + + public static bool ShowKeyErrorOnLoad { get; set; } + public ApplicationLibrary ApplicationLibrary { get; set; } + + public MainWindow() + { + ViewModel = new MainWindowViewModel(this); + + MainWindowViewModel = ViewModel; + + DataContext = ViewModel; + + InitializeComponent(); + Load(); + + UiHandler = new AvaHostUiHandler(this); + + 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 + StatusBar.MinHeight; + Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; + Width /= Program.WindowScaleFactor; + + if (Program.PreviewerDetached) + { + Initialize(); + + ViewModel.Initialize(); + + InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); + + LoadGameList(); + } + + _rendererWaitEvent = new AutoResetEvent(false); + } + + public void LoadGameList() + { + if (_isLoading) + { + return; + } + + _isLoading = true; + + ViewModel.LoadApplications(); + + _isLoading = false; + } + + private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) + { + if (ViewModel.ShowMenuAndStatusBar && !ViewModel.ShowLoadProgress) + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (args.VSyncEnabled) + { + ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ff2eeac9")); + } + else + { + ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ffff4554")); + } + + ViewModel.DockedStatusText = args.DockedMode; + ViewModel.AspectRatioStatusText = args.AspectRatio; + ViewModel.GameStatusText = args.GameStatus; + ViewModel.VolumeStatusText = args.VolumeStatus; + ViewModel.FifoStatusText = args.FifoStatus; + ViewModel.GpuNameText = args.GpuName; + ViewModel.BackendText = args.GpuBackend; + + ViewModel.ShowStatusSeparator = true; + }); + } + } + + protected override void HandleScalingChanged(double scale) + { + Program.DesktopScaleFactor = scale; + base.HandleScalingChanged(scale); + } + + 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; + + LoadApplication(path); + } + + args.Handled = true; + } + + public async Task PerformanceCheck() + { + if (ConfigurationState.Instance.Logger.EnableTrace.Value) + { + string mainMessage = LocaleManager.Instance["DialogPerformanceCheckLoggingEnabledMessage"]; + string secondaryMessage = LocaleManager.Instance["DialogPerformanceCheckLoggingEnabledConfirmMessage"]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage, + LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], + LocaleManager.Instance["RyujinxConfirm"]); + + if (result != UserResult.Yes) + { + ConfigurationState.Instance.Logger.EnableTrace.Value = false; + + SaveConfig(); + } + } + + if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) + { + string mainMessage = LocaleManager.Instance["DialogPerformanceCheckShaderDumpEnabledMessage"]; + string secondaryMessage = + LocaleManager.Instance["DialogPerformanceCheckShaderDumpEnabledConfirmMessage"]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage, + LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], + LocaleManager.Instance["RyujinxConfirm"]); + + if (result != UserResult.Yes) + { + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; + + SaveConfig(); + } + } + } + + internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg) + { + _deferLoad = true; + _launchPath = launchPathArg; + _startFullscreen = startFullscreenArg; + } + +#pragma warning disable CS1998 + public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "") +#pragma warning restore CS1998 + { + if (AppHost != null) + { + await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance["DialogLoadAppGameAlreadyLoadedMessage"], + LocaleManager.Instance["DialogLoadAppGameAlreadyLoadedSubMessage"], + LocaleManager.Instance["InputDialogOk"], + "", + LocaleManager.Instance["RyujinxInfo"]); + + return; + } + +#if RELEASE + await PerformanceCheck(); +#endif + + Logger.RestartTime(); + + if (ViewModel.SelectedIcon == null) + { + ViewModel.SelectedIcon = ApplicationLibrary.GetApplicationIcon(path); + } + + PrepareLoadScreen(); + + _mainViewContent = MainContent.Content as Control; + + RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel); + if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl) + { + RendererControl.CreateOpenGL(); + } + else + { + RendererControl.CreateVulkan(); + } + + AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); + + Dispatcher.UIThread.Post(async () => + { + if (!await AppHost.LoadGuestApplication()) + { + AppHost.DisposeContext(); + AppHost = null; + + return; + } + + CanUpdate = false; + ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance["LoadingHeading"], AppHost.Device.Application.TitleName) : titleName; + ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName; + + SwitchToGameControl(startFullscreen); + + _currentEmulatedGamePath = path; + + Thread gameThread = new(InitializeGame) + { + Name = "GUI.WindowThread" + }; + gameThread.Start(); + }); + } + + private void InitializeGame() + { + RendererControl.RendererInitialized += GlRenderer_Created; + + AppHost.StatusUpdatedEvent += Update_StatusBar; + AppHost.AppExit += AppHost_AppExit; + + _rendererWaitEvent.WaitOne(); + + AppHost?.Start(); + + AppHost.DisposeContext(); + } + + + private void HandleRelaunch() + { + if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart) + { + _userChannelPersistence.ShouldRestart = false; + + Dispatcher.UIThread.Post(() => + { + LoadApplication(_currentEmulatedGamePath); + }); + } + else + { + // otherwise, clear state. + _userChannelPersistence = new UserChannelPersistence(); + _currentEmulatedGamePath = null; + } + } + + public void SwitchToGameControl(bool startFullscreen = false) + { + ViewModel.ShowLoadProgress = false; + ViewModel.ShowContent = true; + ViewModel.IsLoadingIndeterminate = false; + + Dispatcher.UIThread.InvokeAsync(() => + { + MainContent.Content = RendererControl; + + if (startFullscreen && WindowState != WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + + RendererControl.Focus(); + }); + } + + public void ShowLoading(bool startFullscreen = false) + { + ViewModel.ShowContent = false; + ViewModel.ShowLoadProgress = true; + ViewModel.IsLoadingIndeterminate = true; + + Dispatcher.UIThread.InvokeAsync(() => + { + if (startFullscreen && WindowState != WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + }); + } + + private void GlRenderer_Created(object sender, EventArgs e) + { + ShowLoading(); + + _rendererWaitEvent.Set(); + } + + private void AppHost_AppExit(object sender, EventArgs e) + { + if (_isClosing) + { + return; + } + + ViewModel.IsGameRunning = false; + + Dispatcher.UIThread.InvokeAsync(() => + { + ViewModel.ShowMenuAndStatusBar = true; + ViewModel.ShowContent = true; + ViewModel.ShowLoadProgress = false; + ViewModel.IsLoadingIndeterminate = false; + CanUpdate = true; + Cursor = Cursor.Default; + + if (MainContent.Content != _mainViewContent) + { + MainContent.Content = _mainViewContent; + } + + AppHost = null; + + HandleRelaunch(); + }); + + RendererControl.RendererInitialized -= GlRenderer_Created; + RendererControl = null; + + ViewModel.SelectedIcon = null; + + Dispatcher.UIThread.InvokeAsync(() => + { + Title = $"Ryujinx {Program.Version}"; + }); + } + + public void Sort_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton button) + { + var sort = Enum.Parse<ApplicationSort>(button.Tag.ToString()); + ViewModel.Sort(sort); + } + } + + protected override void HandleWindowStateChanged(WindowState state) + { + WindowState = state; + + if (state != WindowState.Minimized) + { + Renderer.Start(); + } + } + + public void Order_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton button) + { + var tag = button.Tag.ToString(); + ViewModel.Sort(tag != "Descending"); + } + } + + 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, this); + + RefreshFirmwareStatus(); + } + + protected void CheckLaunchState() + { + if (ShowKeyErrorOnLoad) + { + ShowKeyErrorOnLoad = false; + + Dispatcher.UIThread.Post(async () => await + UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, this)); + } + + if (_deferLoad) + { + _deferLoad = false; + + LoadApplication(_launchPath, _startFullscreen); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false, this)) + { + Updater.BeginParse(this, false).ContinueWith(task => + { + Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + } + + public void RefreshFirmwareStatus() + { + SystemVersion version = null; + try + { + version = ContentManager.GetCurrentFirmwareVersion(); + } + catch (Exception) { } + + bool hasApplet = false; + + if (version != null) + { + LocaleManager.Instance.UpdateDynamicValue("StatusBarSystemVersion", + version.VersionString); + + hasApplet = version.Major > 3; + } + else + { + LocaleManager.Instance.UpdateDynamicValue("StatusBarSystemVersion", "0.0"); + } + + ViewModel.IsAppletMenuActive = hasApplet; + } + + private void Load() + { + VolumeStatus.Click += VolumeStatus_CheckedChanged; + + GameGrid.ApplicationOpened += Application_Opened; + + GameGrid.DataContext = ViewModel; + + GameList.ApplicationOpened += Application_Opened; + + GameList.DataContext = ViewModel; + + LoadHotKeys(); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + CheckLaunchState(); + } + + public static void UpdateGraphicsConfig() + { + 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; + } + + public void LoadHotKeys() + { + HotKeyManager.SetHotKey(FullscreenHotKey, new KeyGesture(Key.Enter, KeyModifiers.Alt)); + HotKeyManager.SetHotKey(FullscreenHotKey2, new KeyGesture(Key.F11)); + HotKeyManager.SetHotKey(DockToggleHotKey, new KeyGesture(Key.F9)); + HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape)); + } + + public static void SaveConfig() + { + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + public void UpdateGameMetadata(string titleId) + { + ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => + { + if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime)) + { + double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; + + appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); + } + }); + } + + private void PrepareLoadScreen() + { + using MemoryStream stream = new MemoryStream(ViewModel.SelectedIcon); + using var gameIconBmp = SixLabors.ImageSharp.Image.Load<Bgra32>(stream); + + var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel<Bgra32>(); + + const int ColorDivisor = 4; + + Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B); + Color progressBgColor = Color.FromRgb( + (byte)(dominantColor.R / ColorDivisor), + (byte)(dominantColor.G / ColorDivisor), + (byte)(dominantColor.B / ColorDivisor)); + + ViewModel.ProgressBarForegroundColor = new SolidColorBrush(progressFgColor); + ViewModel.ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor); + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + ViewModel.SearchText = SearchBox.Text; + } + + private async void StopEmulation_Click(object sender, RoutedEventArgs e) + { + if (AppHost != null) + { + await AppHost.ShowExitPrompt(); + } + } + + private async void PauseEmulation_Click(object sender, RoutedEventArgs e) + { + await Task.Run(() => + { + AppHost?.Pause(); + }); + } + + private async void ResumeEmulation_Click(object sender, RoutedEventArgs e) + { + await Task.Run(() => + { + AppHost?.Resume(); + }); + } + + private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + if (sender is MenuItem) + { + ViewModel.IsAmiiboRequested = AppHost.Device.System.SearchingForAmiibo(out _); + } + } + + private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + AppHost.Device.EnableDeviceVsync = !AppHost.Device.EnableDeviceVsync; + + Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {AppHost.Device.EnableDeviceVsync}"); + } + + private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value; + } + + private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; + + ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; + } + + private void VolumeStatus_CheckedChanged(object sender, SplitButtonClickEventArgs e) + { + var volumeSplitButton = sender as ToggleSplitButton; + if (ViewModel.IsGameRunning) + { + if (!volumeSplitButton.IsChecked) + { + AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume); + } + else + { + AppHost.Device.SetVolume(0); + } + + ViewModel.Volume = AppHost.Device.GetVolume(); + } + } + + protected override void OnClosing(CancelEventArgs e) + { + if (!_isClosing && AppHost != null && ConfigurationState.Instance.ShowConfirmExit) + { + e.Cancel = true; + + ConfirmExit(); + + return; + } + + _isClosing = true; + + if (AppHost != null) + { + AppHost.AppExit -= AppHost_AppExit; + AppHost.AppExit += (sender, e) => + { + AppHost = null; + + Dispatcher.UIThread.Post(() => + { + MainContent = null; + + Close(); + }); + }; + AppHost?.Stop(); + + e.Cancel = true; + + return; + } + + ApplicationLibrary.CancelLoading(); + InputManager.Dispose(); + Program.Exit(); + + base.OnClosing(e); + } + + private void ConfirmExit() + { + Dispatcher.UIThread.InvokeAsync(async () => + { + _isClosing = await ContentDialogHelper.CreateExitDialog(); + + if (_isClosing) + { + Close(); + } + }); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml b/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml new file mode 100644 index 00000000..862998ac --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml @@ -0,0 +1,141 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + mc:Ignorable="d" + x:Class="Ryujinx.Ava.UI.Windows.MotionSettingsWindow" + Focusable="True"> + <Grid Margin="10"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition /> + </Grid.RowDefinitions> + <StackPanel Orientation="Vertical"> + <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> + <TextBlock + Margin="0" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionGyroSensitivity}" /> + <Slider + Margin="0,-5,0,-5" + Width="150" + MaxWidth="150" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Maximum="100" + Minimum="0" + Value="{Binding Sensitivity, Mode=TwoWay}" /> + <TextBlock HorizontalAlignment="Center" + Margin="5, 0" + Text="{Binding Sensitivity, StringFormat=\{0:0\}%}" /> + </StackPanel> + <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> + <TextBlock + Margin="0" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionGyroDeadzone}" /> + <Slider + Margin="0,-5,0,-5" + Width="150" + MaxWidth="150" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Maximum="100" + Minimum="0" + Value="{Binding GyroDeadzone, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Margin="5, 0" + Text="{Binding GyroDeadzone, StringFormat=\{0:0.00\}}" /> + </StackPanel> + <Separator Height="1" Margin="0,5" /> + <CheckBox Margin="5" IsChecked="{Binding EnableCemuHookMotion}"> + <TextBlock Margin="0,3,0,0" VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionUseCemuhookCompatibleMotion}" /> + </CheckBox> + </StackPanel> + <Border Grid.Row="1" + Padding="20,5" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="1" + HorizontalAlignment="Stretch"> + <Grid VerticalAlignment="Top"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <StackPanel + Grid.Row="1" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Vertical"> + <StackPanel + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Horizontal"> + <TextBlock + Margin="5" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionServerHost}" /> + <TextBox + Height="30" + MinWidth="100" + MaxWidth="100" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{Binding DsuServerHost, Mode=TwoWay}" /> + <TextBlock + Margin="5" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text=":" /> + <TextBox + Height="30" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{Binding DsuServerPort, Mode=TwoWay}" /> + </StackPanel> + <StackPanel Orientation="Vertical"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <TextBlock Margin="0,10,0,0" VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionControllerSlot}" /> + <ui:NumberBox Grid.Row="0" Grid.Column="1" + Name="CemuHookSlotUpDown" + SmallChange="1" + LargeChange="1" + Maximum="4" + Minimum="0" + Value="{Binding Slot}" /> + <TextBlock Margin="0,10,0,0" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionRightJoyConSlot}" /> + <ui:NumberBox Grid.Row="1" Grid.Column="1" + Name="CemuHookRightJoyConSlotUpDown" + SmallChange="1" + LargeChange="1" + Maximum="4" + Minimum="0" + Value="{Binding AltSlot}" /> + </Grid> + </StackPanel> + <CheckBox HorizontalAlignment="Center" + IsChecked="{Binding MirrorInput, Mode=TwoWay}"> + <TextBlock HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsMotionMirrorInput}" /> + </CheckBox> + </StackPanel> + </Grid> + </Border> + </Grid> +</UserControl>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs new file mode 100644 index 00000000..215525fc --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs @@ -0,0 +1,71 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class MotionSettingsWindow : UserControl + { + private readonly InputConfiguration<GamepadInputId, StickInputId> _viewmodel; + + public MotionSettingsWindow() + { + InitializeComponent(); + DataContext = _viewmodel; + } + + public MotionSettingsWindow(ControllerSettingsViewModel viewmodel) + { + var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>; + + _viewmodel = new InputConfiguration<GamepadInputId, StickInputId>() + { + Slot = config.Slot, + AltSlot = config.AltSlot, + DsuServerHost = config.DsuServerHost, + DsuServerPort = config.DsuServerPort, + MirrorInput = config.MirrorInput, + EnableMotion = config.EnableMotion, + Sensitivity = config.Sensitivity, + GyroDeadzone = config.GyroDeadzone, + EnableCemuHookMotion = config.EnableCemuHookMotion + }; + + InitializeComponent(); + DataContext = _viewmodel; + } + + public static async Task Show(ControllerSettingsViewModel viewmodel) + { + MotionSettingsWindow content = new MotionSettingsWindow(viewmodel); + + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance["ControllerMotionTitle"], + PrimaryButtonText = LocaleManager.Instance["ControllerSettingsSave"], + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance["ControllerSettingsClose"], + Content = content + }; + contentDialog.PrimaryButtonClick += (sender, args) => + { + var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>; + config.Slot = content._viewmodel.Slot; + config.EnableMotion = content._viewmodel.EnableMotion; + config.Sensitivity = content._viewmodel.Sensitivity; + config.GyroDeadzone = content._viewmodel.GyroDeadzone; + config.AltSlot = content._viewmodel.AltSlot; + config.DsuServerHost = content._viewmodel.DsuServerHost; + config.DsuServerPort = content._viewmodel.DsuServerPort; + config.EnableCemuHookMotion = content._viewmodel.EnableCemuHookMotion; + config.MirrorInput = content._viewmodel.MirrorInput; + }; + + await contentDialog.ShowAsync(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml b/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml new file mode 100644 index 00000000..e47cc5bd --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml @@ -0,0 +1,57 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + mc:Ignorable="d" + x:Class="Ryujinx.Ava.UI.Windows.RumbleSettingsWindow" + Focusable="True"> + <Grid Margin="10"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition /> + </Grid.RowDefinitions> + <StackPanel Orientation="Vertical"> + <StackPanel Orientation="Horizontal"> + <TextBlock + Width="100" + TextWrapping="WrapWithOverflow" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsRumbleStrongMultiplier}" /> + <Slider + Margin="0,-5,0,-5" + Width="200" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Maximum="10" + Minimum="0" + Value="{Binding StrongRumble, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Margin="5,0" + Text="{Binding StrongRumble, StringFormat=\{0:0.00\}}" /> + </StackPanel> + <StackPanel Orientation="Horizontal"> + <TextBlock + Width="100" + TextWrapping="WrapWithOverflow" + HorizontalAlignment="Center" + Text="{locale:Locale ControllerSettingsRumbleWeakMultiplier}" /> + <Slider + Margin="0,-5,0,-5" + Width="200" + MaxWidth="200" + Maximum="10" + TickFrequency="0.01" + IsSnapToTickEnabled="True" + Minimum="0" + Value="{Binding WeakRumble, Mode=TwoWay}" /> + <TextBlock + VerticalAlignment="Center" + Margin="5,0" + Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> + </StackPanel> + </StackPanel> + </Grid> +</UserControl>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs new file mode 100644 index 00000000..f645ae35 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class RumbleSettingsWindow : UserControl + { + private readonly InputConfiguration<GamepadInputId, StickInputId> _viewmodel; + + public RumbleSettingsWindow() + { + InitializeComponent(); + DataContext = _viewmodel; + } + + public RumbleSettingsWindow(ControllerSettingsViewModel viewmodel) + { + var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>; + + _viewmodel = new InputConfiguration<GamepadInputId, StickInputId>() + { + StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble + }; + + InitializeComponent(); + DataContext = _viewmodel; + } + + public static async Task Show(ControllerSettingsViewModel viewmodel) + { + RumbleSettingsWindow content = new RumbleSettingsWindow(viewmodel); + + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance["ControllerRumbleTitle"], + PrimaryButtonText = LocaleManager.Instance["ControllerSettingsSave"], + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance["ControllerSettingsClose"], + Content = content, + }; + + contentDialog.PrimaryButtonClick += (sender, args) => + { + var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>; + config.StrongRumble = content._viewmodel.StrongRumble; + config.WeakRumble = content._viewmodel.WeakRumble; + }; + + await contentDialog.ShowAsync(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml b/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml new file mode 100644 index 00000000..e2550082 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml @@ -0,0 +1,980 @@ +<window:StyleableWindow + x:Class="Ryujinx.Ava.UI.Windows.SettingsWindow" + xmlns="https://github.com/avaloniaui" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + Width="1100" + Height="768" + d:DesignWidth="800" + d:DesignHeight="950" + MinWidth="800" + MinHeight="480" + WindowStartupLocation="CenterOwner" + x:CompileBindings="True" + x:DataType="viewModels:SettingsViewModel" + mc:Ignorable="d" + Focusable="True"> + <Design.DataContext> + <viewModels:SettingsViewModel /> + </Design.DataContext> + <Window.Resources> + <helpers:KeyValueConverter x:Key="Key" /> + </Window.Resources> + <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="600"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <ContentPresenter + x:Name="ContentPresenter" + Grid.Row="1" + IsVisible="False" + KeyboardNavigation.IsTabStop="False"/> + <Grid Name="Pages" IsVisible="False" Grid.Row="2"> + <ScrollViewer Name="UiPage" + Margin="0,0,2,0" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGeneral}" /> + <StackPanel Margin="10,0,0,0" Orientation="Vertical"> + <CheckBox IsChecked="{Binding EnableDiscordIntegration}"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale ToggleDiscordTooltip}" + Text="{locale:Locale SettingsTabGeneralEnableDiscordRichPresence}" /> + </CheckBox> + <CheckBox IsChecked="{Binding CheckUpdatesOnStart}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" /> + </CheckBox> + <CheckBox IsChecked="{Binding ShowConfirmExit}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" /> + </CheckBox> + <CheckBox IsChecked="{Binding HideCursorOnIdle}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" /> + </CheckBox> + </StackPanel> + <Separator Height="1" /> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" /> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <ListBox + Name="GameList" + MinHeight="250" + Items="{Binding GameDirectories}" /> + <Grid HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <TextBox + Name="PathBox" + Margin="0" + ToolTip.Tip="{locale:Locale AddGameDirBoxTooltip}" + VerticalAlignment="Stretch" /> + <Button + Name="AddButton" + Grid.Column="1" + MinWidth="90" + Margin="10,0,0,0" + ToolTip.Tip="{locale:Locale AddGameDirTooltip}" + Click="AddButton_OnClick"> + <TextBlock HorizontalAlignment="Center" + Text="{locale:Locale SettingsTabGeneralAdd}" /> + </Button> + <Button + Name="RemoveButton" + Grid.Column="2" + MinWidth="90" + Margin="10,0,0,0" + ToolTip.Tip="{locale:Locale RemoveGameDirTooltip}" + Click="RemoveButton_OnClick"> + <TextBlock HorizontalAlignment="Center" + Text="{locale:Locale SettingsTabGeneralRemove}" /> + </Button> + </Grid> + </StackPanel> + <Separator Height="1" /> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralTheme}" /> + <Grid Margin="10,0,0,0"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition /> + <RowDefinition /> + </Grid.RowDefinitions> + <CheckBox IsChecked="{Binding EnableCustomTheme}" + ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" /> + </CheckBox> + <TextBlock VerticalAlignment="Center" + Margin="0,10,0,0" + Grid.Row="1" + Text="{locale:Locale SettingsTabGeneralThemeCustomTheme}" + ToolTip.Tip="{locale:Locale CustomThemePathTooltip}" /> + <TextBox Margin="0,10,0,0" + Grid.Row="1" + Grid.Column="1" + Text="{Binding CustomThemePath}" /> + <Button Grid.Row="1" + Grid.Column="2" + Margin="10,10,0,0" + Command="{ReflectionBinding BrowseTheme}" + ToolTip.Tip="{locale:Locale CustomThemeBrowseTooltip}" + Content="{locale:Locale ButtonBrowse}" /> + <TextBlock VerticalAlignment="Center" + Margin="0,10,0,0" + Grid.Row="2" + Text="{locale:Locale SettingsTabGeneralThemeBaseStyle}" /> + <ComboBox VerticalAlignment="Center" + Margin="0,10,0,0" + Grid.Column="1" + Grid.Row="2" + MinWidth="100" + SelectedIndex="{Binding BaseStyleIndex}"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleLight}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleDark}" /> + </ComboBoxItem> + </ComboBox> + </Grid> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer Name="InputPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Padding="0,0,2,0" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel Margin="4" Orientation="Vertical"> + <StackPanel Orientation="Horizontal"> + <CheckBox Margin="5,0" + ToolTip.Tip="{locale:Locale DockModeToggleTooltip}" + IsChecked="{Binding EnableDockedMode}"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabInputEnableDockedMode}" /> + </CheckBox> + <CheckBox Margin="5,0" + ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}" + IsChecked="{Binding EnableKeyboard}"> + <TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" /> + </CheckBox> + <CheckBox Margin="5,0" + ToolTip.Tip="{locale:Locale DirectMouseTooltip}" + IsChecked="{Binding EnableMouse}"> + <TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" /> + </CheckBox> + </StackPanel> + <window:ControllerSettingsWindow Name="ControllerSettings" Margin="0,0,0,0" MinHeight="600" /> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer Name="HotkeysPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel Margin="10,5" Orientation="Vertical" Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" /> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" /> + <ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked"> + <TextBlock + Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}" + TextAlignment="Center" /> + </ToggleButton> + </StackPanel> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer Name="SystemPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemCore}" /> + <StackPanel Margin="10,0,0,0" Orientation="Vertical"> + <StackPanel Margin="0,0,0,10" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemSystemRegion}" + Width="250" /> + <ComboBox SelectedIndex="{Binding Region}" + ToolTip.Tip="{locale:Locale RegionTooltip}" + HorizontalContentAlignment="Left" + Width="350"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionJapan}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionUSA}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionEurope}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionAustralia}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionChina}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionKorea}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionTaiwan}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Margin="0,0,0,10" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemSystemLanguage}" + ToolTip.Tip="{locale:Locale LanguageTooltip}" + Width="250" /> + <ComboBox SelectedIndex="{Binding Language}" + ToolTip.Tip="{locale:Locale LanguageTooltip}" + HorizontalContentAlignment="Left" + Width="350"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageJapanese}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageFrench}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageGerman}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageItalian}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageSpanish}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageChinese}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageKorean}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageDutch}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguagePortuguese}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageRussian}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageTaiwanese}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageBritishEnglish}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageCanadianFrench}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Margin="0,0,0,10" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemSystemTimeZone}" + ToolTip.Tip="{locale:Locale TimezoneTooltip}" + Width="250" /> + <AutoCompleteBox + Name="TimeZoneBox" + Width="350" + MaxDropDownHeight="500" + FilterMode="Contains" + Items="{Binding TimeZones}" + SelectionChanged="TimeZoneBox_OnSelectionChanged" + Text="{Binding Path=TimeZone, Mode=OneWay}" + TextChanged="TimeZoneBox_OnTextChanged" + ValueMemberBinding="{ReflectionBinding TzMultiBinding}" + ToolTip.Tip="{locale:Locale TimezoneTooltip}" /> + </StackPanel> + <StackPanel Margin="0,0,0,10" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemSystemTime}" + ToolTip.Tip="{locale:Locale TimeTooltip}" + Width="250"/> + <DatePicker VerticalAlignment="Center" SelectedDate="{Binding DateOffset}" + ToolTip.Tip="{locale:Locale TimeTooltip}" + Width="350" /> + </StackPanel> + <StackPanel Margin="250,0,0,10" Orientation="Horizontal"> + <TimePicker + VerticalAlignment="Center" + ClockIdentifier="24HourClock" + SelectedTime="{Binding TimeOffset}" + Width="350" + ToolTip.Tip="{locale:Locale TimeTooltip}" /> + </StackPanel> + <CheckBox IsChecked="{Binding EnableVsync}"> + <TextBlock Text="{locale:Locale SettingsTabSystemEnableVsync}" + ToolTip.Tip="{locale:Locale VSyncToggleTooltip}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableFsIntegrityChecks}"> + <TextBlock Text="{locale:Locale SettingsTabSystemEnableFsIntegrityChecks}" + ToolTip.Tip="{locale:Locale FsIntegrityToggleTooltip}" /> + </CheckBox> + </StackPanel> + <Separator Height="1" /> + <StackPanel Orientation="Horizontal"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" /> + <TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" /> + </StackPanel> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical"> + <CheckBox IsChecked="{Binding ExpandDramSize}" + ToolTip.Tip="{locale:Locale DRamTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabSystemExpandDramSize}" /> + </CheckBox> + <CheckBox IsChecked="{Binding IgnoreMissingServices}" + ToolTip.Tip="{locale:Locale IgnoreMissingServicesTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabSystemIgnoreMissingServices}" /> + </CheckBox> + </StackPanel> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer + Name="CpuPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuCache}" /> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical"> + <CheckBox IsChecked="{Binding EnablePptc}"> + <TextBlock Text="{locale:Locale SettingsTabSystemEnablePptc}" + ToolTip.Tip="{locale:Locale PptcToggleTooltip}" /> + </CheckBox> + </StackPanel> + <Separator Height="1" /> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuMemory}" /> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical"> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemMemoryManagerMode}" + ToolTip.Tip="{locale:Locale MemoryManagerTooltip}" + Width="250" /> + <ComboBox SelectedIndex="{Binding MemoryMode}" + ToolTip.Tip="{locale:Locale MemoryManagerTooltip}" + HorizontalContentAlignment="Left" + Width="350"> + <ComboBoxItem + ToolTip.Tip="{locale:Locale MemoryManagerSoftwareTooltip}"> + <TextBlock + Text="{locale:Locale SettingsTabSystemMemoryManagerModeSoftware}" /> + </ComboBoxItem> + <ComboBoxItem + ToolTip.Tip="{locale:Locale MemoryManagerHostTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabSystemMemoryManagerModeHost}" /> + </ComboBoxItem> + <ComboBoxItem + ToolTip.Tip="{locale:Locale MemoryManagerUnsafeTooltip}"> + <TextBlock + Text="{locale:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + </StackPanel> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer + Name="GraphicsPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" /> + <StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10"> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}" + Text="{locale:Locale SettingsTabGraphicsBackend}" + Width="250" /> + <ComboBox Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}" + SelectedIndex="{Binding GraphicsBackendIndex}"> + <ComboBoxItem IsVisible="{Binding IsVulkanAvailable}"> + <TextBlock Text="Vulkan" /> + </ComboBoxItem> + <ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}"> + <TextBlock Text="OpenGL" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}" + Text="{locale:Locale SettingsTabGraphicsPreferredGpu}" + Width="250" /> + <ComboBox Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}" + SelectedIndex="{Binding PreferredGpuIndex}" + Items="{Binding AvailableGpus}"/> + </StackPanel> + </StackPanel> + <Separator Height="1" /> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" /> + <StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10"> + <StackPanel Orientation="Vertical"> + <CheckBox IsChecked="{Binding EnableShaderCache}" + ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableTextureRecompression}" + ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}"> + <TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableMacroHLE}" + ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}"> + <TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" /> + </CheckBox> + </StackPanel> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}" + Text="{locale:Locale SettingsTabGraphicsResolutionScale}" + Width="250" /> + <ComboBox SelectedIndex="{Binding ResolutionScale}" + Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale2x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale3x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" /> + </ComboBoxItem> + </ComboBox> + <ui:NumberBox + Margin="10,0,0,0" + ToolTip.Tip="{locale:Locale ResolutionScaleEntryTooltip}" + MinWidth="150" + SmallChange="0.1" + LargeChange="1" + SimpleNumberFormat="F2" + SpinButtonPlacementMode="Inline" + IsVisible="{Binding IsCustomResolutionScaleActive}" + Maximum="100" + Minimum="0.1" + Value="{Binding CustomResolutionScale}" /> + </StackPanel> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale AnisotropyTooltip}" + Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}" + Width="250" /> + <ComboBox SelectedIndex="{Binding MaxAnisotropy}" + Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale AnisotropyTooltip}"> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale AspectRatioTooltip}" + Text="{locale:Locale SettingsTabGraphicsAspectRatio}" + Width="250" /> + <ComboBox SelectedIndex="{Binding AspectRatio}" + Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale AspectRatioTooltip}"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio4x3}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x9}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x10}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio21x9}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio32x9}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatioStretch}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + </StackPanel> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}" + Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}" + Width="250" /> + <ComboBox Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale GalThreadingTooltip}" + SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale CommonAuto}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale CommonOff}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale CommonOn}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + </StackPanel> + <Separator Height="1" /> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsDeveloperOptions}" /> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" + Text="{locale:Locale SettingsTabGraphicsShaderDumpPath}" + Width="250" /> + <TextBox Text="{Binding ShaderDumpPath}" + Width="350" + ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" /> + </StackPanel> + </StackPanel> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer + Name="AudioPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabAudio}" /> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemAudioBackend}" + ToolTip.Tip="{locale:Locale AudioBackendTooltip}" + Width="250" /> + <ComboBox SelectedIndex="{Binding AudioBackend}" + Width="350" + HorizontalContentAlignment="Left"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendDummy}" /> + </ComboBoxItem> + <ComboBoxItem IsEnabled="{Binding IsOpenAlEnabled}"> + <TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendOpenAL}" /> + </ComboBoxItem> + <ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}"> + <TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSoundIO}" /> + </ComboBoxItem> + <ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}"> + <TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSDL2}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemAudioVolume}" + ToolTip.Tip="{locale:Locale AudioVolumeTooltip}" + Width="250" /> + <ui:NumberBox Value="{Binding Volume}" + ToolTip.Tip="{locale:Locale AudioVolumeTooltip}" + Width="350" + SmallChange="1" + LargeChange="10" + SimpleNumberFormat="F0" + SpinButtonPlacementMode="Inline" + Minimum="0" + Maximum="100" /> + </StackPanel> + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <Slider Value="{Binding Volume}" + Margin="250,0,0,0" + ToolTip.Tip="{locale:Locale AudioVolumeTooltip}" + Minimum="0" + Maximum="100" + SmallChange="5" + TickFrequency="5" + IsSnapToTickEnabled="True" + LargeChange="10" + Width="350" /> + </StackPanel> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer + Name="NetworkPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" /> + <CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}"> + <TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}" + ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" /> + </CheckBox> + </StackPanel> + </Border> + </ScrollViewer> + <ScrollViewer + Name="LoggingPage" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto"> + <Border Classes="settings"> + <StackPanel + Margin="10,5" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingLogging}" /> + <StackPanel Margin="10,0,0,0" Orientation="Vertical"> + <CheckBox IsChecked="{Binding EnableFileLog}" + ToolTip.Tip="{locale:Locale FileLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableLoggingToFile}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableStub}" + ToolTip.Tip="{locale:Locale StubLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableStubLogs}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableInfo}" + ToolTip.Tip="{locale:Locale InfoLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableInfoLogs}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableWarn}" + ToolTip.Tip="{locale:Locale WarnLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableWarningLogs}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableError}" + ToolTip.Tip="{locale:Locale ErrorLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableTrace}" + ToolTip.Tip="{locale:Locale TraceLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableGuest}" + ToolTip.Tip="{locale:Locale GuestLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" /> + </CheckBox> + </StackPanel> + <Separator Height="1" /> + <TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" /> + <StackPanel + Margin="10,0,0,0" + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="10"> + <StackPanel Orientation="Vertical"> + <CheckBox IsChecked="{Binding EnableDebug}" + ToolTip.Tip="{locale:Locale DebugLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableFsAccessLog}" + ToolTip.Tip="{locale:Locale FileAccessLogTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" /> + </CheckBox> + <StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}" + Text="{locale:Locale SettingsTabLoggingFsGlobalAccessLogMode}" + Width="285" /> + <ui:NumberBox + Maximum="3" + Minimum="0" + Width="150" + SpinButtonPlacementMode="Inline" + SmallChange="1" + LargeChange="1" + Value="{Binding FsGlobalAccessLogMode}" /> + </StackPanel> + <StackPanel Margin="0,10,0,0" Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" + Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevel}" + ToolTip.Tip="{locale:Locale OpenGlLogLevel}" + Width="285" /> + <ComboBox SelectedIndex="{Binding OpenglDebugLevel}" + Width="150" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale OpenGlLogLevel}"> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelNone}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelError}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelPerformance}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelAll}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + </StackPanel> + </StackPanel> + </StackPanel> + </Border> + </ScrollViewer> + </Grid> + <ui:NavigationView Grid.Row="1" + IsSettingsVisible="False" + Name="NavPanel" + IsBackEnabled="False" + PaneDisplayMode="Left" + Margin="2,10,10,0" + VerticalAlignment="Stretch" + HorizontalAlignment="Stretch" + OpenPaneLength="200"> + <ui:NavigationView.MenuItems> + <ui:NavigationViewItem IsSelected="True" + Content="{locale:Locale SettingsTabGeneral}" + Tag="UiPage" + Icon="New" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabInput}" + Tag="InputPage" + Icon="Games" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabHotkeys}" + Tag="HotkeysPage" + Icon="Keyboard" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabSystem}" + Tag="SystemPage" + Icon="Settings" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabCpu}" + Tag="CpuPage"> + <ui:NavigationViewItem.Icon> + <ui:FontIcon FontFamily="avares://Ryujinx.Ava/Assets/Fonts#Segoe Fluent Icons" + Glyph="{helpers:GlyphValueConverter Chip}" /> + </ui:NavigationViewItem.Icon> + </ui:NavigationViewItem> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabGraphics}" + Tag="GraphicsPage" + Icon="Image" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabAudio}" + Icon="Audio" + Tag="AudioPage" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabNetwork}" + Tag="NetworkPage" + Icon="Globe" /> + <ui:NavigationViewItem + Content="{locale:Locale SettingsTabLogging}" + Tag="LoggingPage" + Icon="Document" /> + </ui:NavigationView.MenuItems> + </ui:NavigationView> + <ReversibleStackPanel + Grid.Row="2" + Margin="10" + Spacing="10" + Orientation="Horizontal" + HorizontalAlignment="Right" + ReverseOrder="{ReflectionBinding IsMacOS}"> + <Button + HotKey="Enter" + Classes="accent" + Content="{locale:Locale SettingsButtonOk}" + Command="{ReflectionBinding OkButton}" /> + <Button + HotKey="Escape" + Content="{locale:Locale SettingsButtonCancel}" + Command="{ReflectionBinding CancelButton}" /> + <Button + Content="{locale:Locale SettingsButtonApply}" + Command="{ReflectionBinding ApplyButton}" /> + </ReversibleStackPanel> + </Grid> +</window:StyleableWindow>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs new file mode 100644 index 00000000..f3aa1d5e --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs @@ -0,0 +1,213 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Input; +using Avalonia.Interactivity; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class SettingsWindow : StyleableWindow + { + private ButtonKeyAssigner _currentAssigner; + + internal SettingsViewModel ViewModel { get; set; } + + public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager) + { + Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["Settings"]}"; + + ViewModel = new SettingsViewModel(virtualFileSystem, contentManager, this); + DataContext = ViewModel; + + InitializeComponent(); + Load(); + + FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim()); + MultiBinding tzMultiBinding = new() { Converter = converter }; + tzMultiBinding.Bindings.Add(new Binding("UtcDifference")); + tzMultiBinding.Bindings.Add(new Binding("Location")); + tzMultiBinding.Bindings.Add(new Binding("Abbreviation")); + + TimeZoneBox.ValueMemberBinding = tzMultiBinding; + } + + public SettingsWindow() + { + ViewModel = new SettingsViewModel(); + DataContext = ViewModel; + + InitializeComponent(); + Load(); + } + + private void Load() + { + Pages.Children.Clear(); + NavPanel.SelectionChanged += NavPanelOnSelectionChanged; + NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0); + } + + private void Button_Checked(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + if (_currentAssigner == null && (bool)button.IsChecked) + { + _currentAssigner = new ButtonKeyAssigner(button); + + FocusManager.Instance.Focus(this, NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad(ViewModel.AvaloniaKeyboardDriver.GamepadsIds[0]); + IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard); + + _currentAssigner.GetInputAndAssign(assigner); + } + else + { + if (_currentAssigner != null) + { + ToggleButton oldButton = _currentAssigner.ToggledButton; + + _currentAssigner.Cancel(); + _currentAssigner = null; + + button.IsChecked = false; + } + } + } + } + + private void Button_Unchecked(object sender, RoutedEventArgs e) + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = false; + + if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed) + { + shouldUnbind = true; + } + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e) + { + if (e.SelectedItem is NavigationViewItem navitem) + { + NavPanel.Content = navitem.Tag.ToString() switch + { + "UiPage" => UiPage, + "InputPage" => InputPage, + "HotkeysPage" => HotkeysPage, + "SystemPage" => SystemPage, + "CpuPage" => CpuPage, + "GraphicsPage" => GraphicsPage, + "AudioPage" => AudioPage, + "NetworkPage" => NetworkPage, + "LoggingPage" => LoggingPage, + _ => throw new NotImplementedException() + }; + } + } + + private async void AddButton_OnClick(object sender, RoutedEventArgs e) + { + string path = PathBox.Text; + + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path)) + { + ViewModel.GameDirectories.Add(path); + ViewModel.DirectoryChanged = true; + } + else + { + path = await new OpenFolderDialog().ShowAsync(this); + + if (!string.IsNullOrWhiteSpace(path)) + { + ViewModel.GameDirectories.Add(path); + ViewModel.DirectoryChanged = true; + } + } + } + + private void RemoveButton_OnClick(object sender, RoutedEventArgs e) + { + int oldIndex = GameList.SelectedIndex; + + foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>())) + { + ViewModel.GameDirectories.Remove(path); + ViewModel.DirectoryChanged = true; + } + + if (GameList.ItemCount > 0) + { + GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0; + } + } + + private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.AddedItems != null && e.AddedItems.Count > 0) + { + if (e.AddedItems[0] is TimeZone timeZone) + { + e.Handled = true; + + ViewModel.ValidateAndSetTimeZone(timeZone.Location); + } + } + } + + private void TimeZoneBox_OnTextChanged(object sender, EventArgs e) + { + if (sender is AutoCompleteBox box) + { + if (box.SelectedItem != null && box.SelectedItem is TimeZone timeZone) + { + ViewModel.ValidateAndSetTimeZone(timeZone.Location); + } + } + } + + protected override void OnClosed(EventArgs e) + { + ControllerSettings.Dispose(); + + _currentAssigner?.Cancel(); + _currentAssigner = null; + + base.OnClosed(e); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/StyleableWindow.cs b/Ryujinx.Ava/UI/Windows/StyleableWindow.cs new file mode 100644 index 00000000..a157f154 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/StyleableWindow.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using System; +using System.IO; +using System.Reflection; + +namespace Ryujinx.Ava.UI.Windows +{ + public class StyleableWindow : Window + { + public IBitmap IconImage { get; set; } + + public StyleableWindow() + { + WindowStartupLocation = WindowStartupLocation.CenterOwner; + TransparencyLevelHint = WindowTransparencyLevel.None; + + using Stream stream = Assembly.GetAssembly(typeof(Ryujinx.Ui.Common.Configuration.ConfigurationState)).GetManifestResourceStream("Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); + + Icon = new WindowIcon(stream); + stream.Position = 0; + IconImage = new Bitmap(stream); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml new file mode 100644 index 00000000..5a69be9b --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml @@ -0,0 +1,115 @@ +<window:StyleableWindow + x:Class="Ryujinx.Ava.UI.Windows.TitleUpdateWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + Width="600" + Height="400" + MinWidth="600" + MinHeight="400" + MaxWidth="600" + MaxHeight="400" + SizeToContent="Height" + WindowStartupLocation="CenterOwner" + mc:Ignorable="d" + Focusable="True"> + <Grid Margin="15"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock + Name="Heading" + Grid.Row="1" + MaxWidth="500" + Margin="20,15,20,20" + HorizontalAlignment="Center" + VerticalAlignment="Center" + LineHeight="18" + TextAlignment="Center" + TextWrapping="Wrap" /> + <Border + Grid.Row="2" + Margin="5" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + BorderBrush="Gray" + BorderThickness="1"> + <ScrollViewer + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Auto" + VerticalScrollBarVisibility="Auto"> + <ItemsControl + Margin="10" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Items="{Binding _titleUpdates}"> + <ItemsControl.ItemTemplate> + <DataTemplate> + <RadioButton + Padding="8,0" + VerticalContentAlignment="Center" + GroupName="Update" + IsChecked="{Binding IsEnabled, Mode=TwoWay}"> + <Label + Margin="0" + VerticalAlignment="Center" + Content="{Binding Label}" + FontSize="12" /> + </RadioButton> + </DataTemplate> + </ItemsControl.ItemTemplate> + </ItemsControl> + </ScrollViewer> + </Border> + <DockPanel + Grid.Row="3" + Margin="0" + HorizontalAlignment="Stretch"> + <DockPanel Margin="0" HorizontalAlignment="Left"> + <Button + Name="AddButton" + MinWidth="90" + Margin="5" + Command="{Binding Add}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" /> + </Button> + <Button + Name="RemoveButton" + MinWidth="90" + Margin="5" + Command="{Binding RemoveSelected}"> + <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" /> + </Button> + <Button + Name="RemoveAllButton" + MinWidth="90" + Margin="5" + Command="{Binding RemoveAll}"> + <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" /> + </Button> + </DockPanel> + <DockPanel Margin="0" HorizontalAlignment="Right"> + <Button + Name="SaveButton" + MinWidth="90" + Margin="5" + Command="{Binding Save}"> + <TextBlock Text="{locale:Locale SettingsButtonSave}" /> + </Button> + <Button + Name="CancelButton" + MinWidth="90" + Margin="5" + Command="{Binding Close}"> + <TextBlock Text="{locale:Locale InputDialogCancel}" /> + </Button> + </DockPanel> + </DockPanel> + </Grid> +</window:StyleableWindow>
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs new file mode 100644 index 00000000..03c2b098 --- /dev/null +++ b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -0,0 +1,271 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ns; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Path = System.IO.Path; +using SpanHelpers = LibHac.Common.SpanHelpers; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class TitleUpdateWindow : StyleableWindow + { + private readonly string _titleUpdateJsonPath; + private TitleUpdateMetadata _titleUpdateWindowData; + + private VirtualFileSystem _virtualFileSystem { get; } + private AvaloniaList<TitleUpdateModel> _titleUpdates { get; set; } + + private ulong _titleId { get; } + private string _titleName { get; } + + public TitleUpdateWindow() + { + DataContext = this; + + InitializeComponent(); + + Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["UpdateWindowTitle"]} - {_titleName} ({_titleId:X16})"; + } + + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + _virtualFileSystem = virtualFileSystem; + _titleUpdates = new AvaloniaList<TitleUpdateModel>(); + + _titleId = titleId; + _titleName = titleName; + + _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); + + try + { + _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath); + } + catch + { + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List<string>() + }; + } + + DataContext = this; + + InitializeComponent(); + + Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["UpdateWindowTitle"]} - {_titleName} ({_titleId:X16})"; + + LoadUpdates(); + PrintHeading(); + } + + private void PrintHeading() + { + Heading.Text = string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], _titleUpdates.Count, _titleName, _titleId.ToString("X16")); + } + + private void LoadUpdates() + { + _titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true)); + + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + if (_titleUpdateWindowData.Selected == "") + { + _titleUpdates[0].IsEnabled = true; + } + else + { + TitleUpdateModel selected = _titleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected); + List<TitleUpdateModel> enabled = _titleUpdates.Where(x => x.IsEnabled).ToList(); + + foreach (TitleUpdateModel update in enabled) + { + update.IsEnabled = false; + } + + if (selected != null) + { + selected.IsEnabled = true; + } + } + + SortUpdates(); + } + + private void AddUpdate(string path) + { + if (File.Exists(path) && !_titleUpdates.Any(x => x.Path == path)) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new(); + + using UniqueRef<IFile> nacpFile = new(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + _titleUpdates.Add(new TitleUpdateModel(controlData, path)); + + foreach (var update in _titleUpdates) + { + update.IsEnabled = false; + } + + _titleUpdates.Last().IsEnabled = true; + } + else + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]); + }); + } + } + catch (Exception ex) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path)); + }); + } + } + } + + private void RemoveUpdates(bool removeSelectedOnly = false) + { + if (removeSelectedOnly) + { + _titleUpdates.RemoveAll(_titleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList()); + } + else + { + _titleUpdates.RemoveAll(_titleUpdates.Where(x => !x.IsNoUpdate).ToList()); + } + + _titleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true; + + SortUpdates(); + PrintHeading(); + } + + public void RemoveSelected() + { + RemoveUpdates(true); + } + + public void RemoveAll() + { + RemoveUpdates(); + } + + public async void Add() + { + OpenFileDialog dialog = new() + { + Title = LocaleManager.Instance["SelectUpdateDialogTitle"], + AllowMultiple = true + }; + + dialog.Filters.Add(new FileDialogFilter + { + Name = "NSP", + Extensions = { "nsp" } + }); + + string[] files = await dialog.ShowAsync(this); + + if (files != null) + { + foreach (string file in files) + { + AddUpdate(file); + } + } + + SortUpdates(); + PrintHeading(); + } + + private void SortUpdates() + { + var list = _titleUpdates.ToList(); + + list.Sort((first, second) => + { + if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString())) + { + return -1; + } + else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString())) + { + return 1; + } + + return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1; + }); + + _titleUpdates.Clear(); + _titleUpdates.AddRange(list); + } + + public void Save() + { + _titleUpdateWindowData.Paths.Clear(); + + _titleUpdateWindowData.Selected = ""; + + foreach (TitleUpdateModel update in _titleUpdates) + { + _titleUpdateWindowData.Paths.Add(update.Path); + + if (update.IsEnabled) + { + _titleUpdateWindowData.Selected = update.Path; + } + } + + using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough)) + { + titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); + } + + if (Owner is MainWindow window) + { + window.ViewModel.LoadApplications(); + } + + Close(); + } + } +}
\ No newline at end of file |
