Avalonia UI - Part 1 (#3270)

* avalonia part 1

* remove vulkan ui backend

* move ui common files to ui common project

* get name for oading screen from device

* rebase.

* review 1

* review 1.1

* review

* cleanup

* addressed review

* use cancellation token

* review

* review

* rebased

* cancel library loading when closing window

* remove star  image, use fonticon instead

* delete render control frame buffer when game ends. change position of fav star

* addressed @Thog review

* ensure the right ui is downloaded in updates

* fix crash when showing not supported dialog during controller request

* add prefix to artifact names

* Auto-format Avalonia project

* Fix input

* Fix build, simplify app disposal

* remove nv stutter thread

* addressed review

* add missing change

* maintain window size if new size is zero length

* add game, handheld, docked to local

* reverse scale main window

* Update de_DE.json

* Update de_DE.json

* Update de_DE.json

* Update italian json

* Update it_IT.json

* let render timer poll with no wait

* remove unused code

* more unused code

* enabled tiered compilation and trimming

* check if window event is not closed before signaling

* fix atmospher case

* locale fix

* locale fix

* remove explicit tiered compilation declarations

* Remove ) it_IT.json

* Remove ) de_DE.json

* Update it_IT.json

* Update pt_BR locale with latest strings

* Remove ')'

* add more strings to locale

* update locale

* remove extra slash

* remove extra slash

* set firmware version to 0 if key's not found

* fix

* revert timer changes

* lock  on object instead

* Update it_IT.json

* remove unused method

* add load screen text to locale

* drop swap event

* Update de_DE.json

* Update de_DE.json

* do null check when stopping emulator

* Update de_DE.json

* Create tr_TR.json

* Add tr_TR

* Add tr_TR + Turkish

* Update it_IT.json

* Update Ryujinx.Ava/Input/AvaloniaMappingHelper.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* addressed review

* Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* use avalonia's inbuilt renderer on linux

* removed whitespace

* workaround for queue render crash with vsync off

* drop custom backend

* format files

* fix not closing issue

* remove warnings

* rebase

* update avalonia library

* Reposition the Text and Button on About Page

* Assign build version

* Remove appveyor text

Co-authored-by: gdk <gab.dark.100@gmail.com>
Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com>
Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com>
Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com>
This commit is contained in:
Emmanuel Hansen
2022-05-15 11:30:15 +00:00
committed by GitHub
parent 9ba73ffbe5
commit deb99d2cae
161 changed files with 17179 additions and 855 deletions

View File

@@ -0,0 +1,16 @@
using Avalonia.Interactivity;
using Ryujinx.Ui.App.Common;
namespace Ryujinx.Ava.Ui.Controls
{
public class ApplicationOpenedEventArgs : RoutedEventArgs
{
public ApplicationData Application { get; }
public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent)
{
Application = application;
RoutedEvent = routedEvent;
}
}
}

View File

@@ -0,0 +1,16 @@
using SPB.Graphics;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.Ui.Controls
{
[SupportedOSPlatform("linux")]
public class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext
{
public AvaloniaGlxContext(IntPtr handle)
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
{
ContextHandle = handle;
}
}
}

View File

@@ -0,0 +1,16 @@
using SPB.Graphics;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.Ui.Controls
{
[SupportedOSPlatform("windows")]
public class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext
{
public AvaloniaWglContext(IntPtr handle)
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
{
ContextHandle = handle;
}
}
}

View File

@@ -0,0 +1,35 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using System;
using System.Globalization;
using System.IO;
namespace Ryujinx.Ava.Ui.Controls
{
public class BitmapArrayValueConverter : IValueConverter
{
public static BitmapArrayValueConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return null;
}
if (value is byte[] buffer && targetType == typeof(IImage))
{
MemoryStream mem = new(buffer);
return new Bitmap(mem);
}
throw new NotSupportedException();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,357 @@
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
public static class ContentDialogHelper
{
private static bool _isChoiceDialogOpen;
private async static Task<UserResult> ShowContentDialog(
StyleableWindow window,
string title,
string primaryText,
string secondaryText,
string primaryButton,
string secondaryButton,
string closeButton,
int iconSymbol,
UserResult primaryButtonResult = UserResult.Ok)
{
UserResult result = UserResult.None;
ContentDialog contentDialog = window.ContentDialog;
await ShowDialog();
async Task ShowDialog()
{
if (contentDialog != null)
{
contentDialog.Title = title;
contentDialog.PrimaryButtonText = primaryButton;
contentDialog.SecondaryButtonText = secondaryButton;
contentDialog.CloseButtonText = closeButton;
contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = primaryButtonResult;
});
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.No;
});
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Cancel;
});
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
};
}
return result;
}
public async static Task<UserResult> ShowDeferredContentDialog(
StyleableWindow window,
string title,
string primaryText,
string secondaryText,
string primaryButton,
string secondaryButton,
string closeButton,
int iconSymbol,
ManualResetEvent deferResetEvent,
Func<Window, Task> doWhileDeferred = null)
{
bool startedDeferring = false;
UserResult result = UserResult.None;
ContentDialog contentDialog = window.ContentDialog;
Window overlay = window;
if (contentDialog != null)
{
contentDialog.PrimaryButtonClick += DeferClose;
contentDialog.Title = title;
contentDialog.PrimaryButtonText = primaryButton;
contentDialog.SecondaryButtonText = secondaryButton;
contentDialog.CloseButtonText = closeButton;
contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
});
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.No;
});
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Cancel;
});
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
};
return result;
async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (startedDeferring)
{
return;
}
startedDeferring = true;
var deferral = args.GetDeferral();
result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
contentDialog.PrimaryButtonClick -= DeferClose;
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Run(() =>
{
deferResetEvent.WaitOne();
Dispatcher.UIThread.Post(() =>
{
deferral.Complete();
});
});
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
if (doWhileDeferred != null)
{
await doWhileDeferred(overlay);
deferResetEvent.Set();
}
}
}
private static Grid CreateDialogTextContent(string primaryText, string secondaryText, int symbol)
{
Grid content = new Grid();
content.RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() };
content.ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() };
content.MinHeight = 80;
SymbolIcon icon = new SymbolIcon { Symbol = (Symbol)symbol, Margin = new Avalonia.Thickness(10) };
icon.FontSize = 40;
icon.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
Grid.SetColumn(icon, 0);
Grid.SetRowSpan(icon, 2);
Grid.SetRow(icon, 0);
TextBlock primaryLabel = new TextBlock()
{
Text = primaryText,
Margin = new Avalonia.Thickness(5),
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
MaxWidth = 450
};
TextBlock secondaryLabel = new TextBlock()
{
Text = secondaryText,
Margin = new Avalonia.Thickness(5),
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
MaxWidth = 450
};
Grid.SetColumn(primaryLabel, 1);
Grid.SetColumn(secondaryLabel, 1);
Grid.SetRow(primaryLabel, 0);
Grid.SetRow(secondaryLabel, 1);
content.Children.Add(icon);
content.Children.Add(primaryLabel);
content.Children.Add(secondaryLabel);
return content;
}
public static async Task<UserResult> CreateInfoDialog(
StyleableWindow window,
string primary,
string secondaryText,
string acceptButton,
string closeButton,
string title)
{
return await ShowContentDialog(
window,
title,
primary,
secondaryText,
acceptButton,
"",
closeButton,
(int)Symbol.Important);
}
internal static async Task<UserResult> CreateConfirmationDialog(
StyleableWindow window,
string primaryText,
string secondaryText,
string acceptButtonText,
string cancelButtonText,
string title,
UserResult primaryButtonResult = UserResult.Yes)
{
return await ShowContentDialog(
window,
string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance["DialogConfirmationTitle"] : title,
primaryText,
secondaryText,
acceptButtonText,
"",
cancelButtonText,
(int)Symbol.Help,
primaryButtonResult);
}
internal static UpdateWaitWindow CreateWaitingDialog(string mainText, string secondaryText)
{
return new(mainText, secondaryText);
}
internal static async void CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText)
{
await ShowContentDialog(
window,
LocaleManager.Instance["DialogUpdaterTitle"],
primary,
secondaryText,
"",
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Important);
}
internal static async void ShowNotAvailableMessage(StyleableWindow window)
{
// Temporary placeholder for features to be added
await ShowContentDialog(
window,
"Feature Not Available",
"The selected feature is not available in this version.",
"",
"",
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Important);
}
internal static async void CreateWarningDialog(StyleableWindow window, string primary, string secondaryText)
{
await ShowContentDialog(
window,
LocaleManager.Instance["DialogWarningTitle"],
primary,
secondaryText,
"",
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Important);
}
internal static async void CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "")
{
Logger.Error?.Print(LogClass.Application, errorMessage);
await ShowContentDialog(
owner,
LocaleManager.Instance["DialogErrorTitle"],
LocaleManager.Instance["DialogErrorMessage"],
errorMessage,
secondaryErrorMessage,
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Dismiss);
}
internal static async Task<bool> CreateChoiceDialog(StyleableWindow window, string title, string primary, string secondaryText)
{
if (_isChoiceDialogOpen)
{
return false;
}
_isChoiceDialogOpen = true;
UserResult response =
await ShowContentDialog(
window,
title,
primary,
secondaryText,
LocaleManager.Instance["InputDialogYes"],
"",
LocaleManager.Instance["InputDialogNo"],
(int)Symbol.Help,
UserResult.Yes);
_isChoiceDialogOpen = false;
return response == UserResult.Yes;
}
internal static async Task<bool> CreateExitDialog(StyleableWindow owner)
{
return await CreateChoiceDialog(
owner,
LocaleManager.Instance["DialogExitTitle"],
LocaleManager.Instance["DialogExitMessage"],
LocaleManager.Instance["DialogExitSubMessage"]);
}
internal static async Task<bool> CreateStopEmulationDialog(StyleableWindow owner)
{
return await CreateChoiceDialog(
owner,
LocaleManager.Instance["DialogStopEmulationTitle"],
LocaleManager.Instance["DialogStopEmulationMessage"],
LocaleManager.Instance["DialogExitSubMessage"]);
}
internal static async Task<string> CreateInputDialog(
string title,
string mainText,
string subText,
StyleableWindow owner,
uint maxLength = int.MaxValue,
string input = "")
{
var result = await InputDialog.ShowInputDialog(
owner,
title,
mainText,
input,
subText,
maxLength);
if (result.Result == UserResult.Ok)
{
return result.Input;
}
return string.Empty;
}
}
}

View File

@@ -0,0 +1,188 @@
<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:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Ui.Controls.GameGridView">
<UserControl.Resources>
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
<MenuItem
Command="{Binding ToggleFavorite}"
Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenUserSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenDeviceSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenBcatSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenTitleUpdateManager}"
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
<MenuItem
Command="{Binding OpenDlcManager}"
Header="{locale:Locale GameListContextMenuManageDlc}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
<MenuItem
Command="{Binding OpenCheatManager}"
Header="{locale:Locale GameListContextMenuManageCheat}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
<MenuItem
Command="{Binding OpenModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenSdModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
<Separator />
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
<MenuItem
Command="{Binding PurgePtcCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
<MenuItem
Command="{Binding PurgeShaderCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
<MenuItem
Command="{Binding OpenPtcDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenShaderCacheDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
</MenuItem>
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
<MenuItem
Command="{Binding ExtractExeFs}"
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
<MenuItem
Command="{Binding ExtractRomFs}"
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Command="{Binding ExtractLogo}"
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem>
</MenuFlyout>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
Padding="8"
HorizontalAlignment="Stretch"
DoubleTapped="GameList_DoubleTapped"
SelectionChanged="GameList_SelectionChanged"
ContextFlyout="{StaticResource GameContextMenu}"
VerticalAlignment="Stretch"
Items="{Binding AppsObservableList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<flex:FlexPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" JustifyContent="Center"
AlignContent="FlexStart" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark3}" />
<Style.Animations>
<Animation Duration="0:0:0.7">
<KeyFrame Cue="0%">
<Setter Property="MaxWidth" Value="0"/>
<Setter Property="Opacity" Value="0.0"/>
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="MaxWidth" Value="1000"/>
<Setter Property="Opacity" Value="0.3"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="MaxWidth" Value="1000"/>
<Setter Property="Opacity" Value="1.0"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.Styles>
<Style Selector="ui|SymbolIcon.small.icon">
<Setter Property="FontSize" Value="15" />
</Style>
<Style Selector="ui|SymbolIcon.normal.icon">
<Setter Property="FontSize" Value="19" />
</Style>
<Style Selector="ui|SymbolIcon.large.icon">
<Setter Property="FontSize" Value="23" />
</Style>
<Style Selector="ui|SymbolIcon.huge.icon">
<Setter Property="FontSize" Value="26" />
</Style>
</Grid.Styles>
<Border
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
HorizontalAlignment="Stretch"
Padding="{Binding $parent[UserControl].DataContext.GridItemPadding}" CornerRadius="5"
VerticalAlignment="Stretch" Margin="0" ClipToBounds="True">
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="0" Grid.Row="0"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<StackPanel IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}"
Height="50" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="5" Grid.Row="1">
<TextBlock Text="{Binding TitleName}" TextAlignment="Center" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
</StackPanel>
</Grid>
</Border>
<ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Foreground="Yellow" Symbol="StarFilled"
IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top"
HorizontalAlignment="Left" />
<ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Foreground="Black" Symbol="Star"
IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top"
HorizontalAlignment="Left" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@@ -0,0 +1,82 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using LibHac.Common;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.Ui.App.Common;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public partial class GameGridView : UserControl
{
private ApplicationData _selectedApplication;
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
{
add { AddHandler(ApplicationOpenedEvent, value); }
remove { RemoveHandler(ApplicationOpenedEvent, value); }
}
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
{
if (sender is ListBox listBox)
{
if (listBox.SelectedItem is ApplicationData selected)
{
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
}
}
}
public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
if (sender is ListBox listBox)
{
var selected = listBox.SelectedItem as ApplicationData;
_selectedApplication = selected;
}
}
public ApplicationData SelectedApplication => _selectedApplication;
public GameGridView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
{
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
}
private void MenuBase_OnMenuOpened(object sender, EventArgs e)
{
var selection = SelectedApplication;
if (selection != null)
{
if (sender is ContextMenu menu)
{
bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
}
}
}
}
}

View File

@@ -0,0 +1,188 @@
<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:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Ui.Controls.GameListView">
<UserControl.Resources>
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
<MenuItem
Command="{Binding ToggleFavorite}"
Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenUserSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenDeviceSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenBcatSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenTitleUpdateManager}"
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
<MenuItem
Command="{Binding OpenDlcManager}"
Header="{locale:Locale GameListContextMenuManageDlc}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
<MenuItem
Command="{Binding OpenCheatManager}"
Header="{locale:Locale GameListContextMenuManageCheat}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
<MenuItem
Command="{Binding OpenModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenSdModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
<Separator />
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
<MenuItem
Command="{Binding PurgePtcCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
<MenuItem
Command="{Binding PurgeShaderCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
<MenuItem
Command="{Binding OpenPtcDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenShaderCacheDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
</MenuItem>
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
<MenuItem
Command="{Binding ExtractExeFs}"
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
<MenuItem
Command="{Binding ExtractRomFs}"
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Command="{Binding ExtractLogo}"
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem>
</MenuFlyout>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
Padding="8"
HorizontalAlignment="Stretch"
DoubleTapped="GameList_DoubleTapped"
SelectionChanged="GameList_SelectionChanged"
ContextFlyout="{StaticResource GameContextMenu}"
VerticalAlignment="Stretch"
Name="GameListBox"
Items="{Binding AppsObservableList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical" Spacing="2" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark3}" />
<Setter Property="BorderThickness" Value="2" />
<Style.Animations>
<Animation Duration="0:0:0.7">
<KeyFrame Cue="0%">
<Setter Property="MaxHeight" Value="0"/>
<Setter Property="Opacity" Value="0.0"/>
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="MaxHeight" Value="1000"/>
<Setter Property="Opacity" Value="0.3"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="MaxHeight" Value="1000"/>
<Setter Property="Opacity" Value="1.0"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border HorizontalAlignment="Stretch"
Padding="10" CornerRadius="5"
VerticalAlignment="Stretch" Margin="0" ClipToBounds="True">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Image
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Grid.RowSpan="3" Grid.Column="0" Margin="0"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Left"
Grid.Column="2">
<TextBlock Text="{Binding TitleName}" TextAlignment="Left" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding Developer}" TextAlignment="Left" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding Version}" TextAlignment="Left" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Right"
Grid.Column="3">
<TextBlock Text="{Binding TimePlayed}" TextAlignment="Right" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding LastPlayed}" TextAlignment="Right" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding FileSize}" TextAlignment="Right" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
</StackPanel>
<ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20"
Foreground="Yellow"
Symbol="StarFilled"
IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top"
HorizontalAlignment="Left" />
<ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20"
Foreground="Black"
Symbol="Star"
IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top"
HorizontalAlignment="Left" />
</Grid>
</Border>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@@ -0,0 +1,82 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using LibHac.Common;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.Ui.App.Common;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public partial class GameListView : UserControl
{
private ApplicationData _selectedApplication;
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
{
add { AddHandler(ApplicationOpenedEvent, value); }
remove { RemoveHandler(ApplicationOpenedEvent, value); }
}
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
{
if (sender is ListBox listBox)
{
if (listBox.SelectedItem is ApplicationData selected)
{
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
}
}
}
public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
if (sender is ListBox listBox)
{
var selected = listBox.SelectedItem as ApplicationData;
_selectedApplication = selected;
}
}
public ApplicationData SelectedApplication => _selectedApplication;
public GameListView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
{
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
}
private void MenuBase_OnMenuOpened(object sender, EventArgs e)
{
var selection = SelectedApplication;
if (selection != null)
{
if (sender is ContextMenu menu)
{
bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Ryujinx.Ava.Ui.Controls
{
public enum Glyph : int
{
List,
Grid,
Chip
}
}

View File

@@ -0,0 +1,49 @@
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using System;
using System.Collections.Generic;
namespace Ryujinx.Ava.Ui.Controls
{
public class GlyphValueConverter : MarkupExtension
{
private string _key;
private static Dictionary<Glyph, string> _glyphs = new Dictionary<Glyph, string>
{
{ Glyph.List, char.ConvertFromUtf32((int)Symbol.List).ToString() },
{ Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll).ToString() },
{ Glyph.Chip, char.ConvertFromUtf32(59748).ToString() }
};
public GlyphValueConverter(string key)
{
_key = key;
}
public string this[string key]
{
get
{
if (_glyphs.TryGetValue(Enum.Parse<Glyph>(key), out var val))
{
return val;
}
return string.Empty;
}
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension binding = new($"[{_key}]")
{
Mode = BindingMode.OneWay,
Source = this
};
return binding.ProvideValue(serviceProvider);
}
}
}

View File

@@ -0,0 +1,52 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using System;
using System.Windows.Input;
namespace Ryujinx.Ava.Ui.Controls
{
public class HotKeyControl : ContentControl, ICommandSource
{
public static readonly StyledProperty<object> CommandParameterProperty =
AvaloniaProperty.Register<HotKeyControl, object>(nameof(CommandParameter));
public static readonly DirectProperty<HotKeyControl, ICommand> CommandProperty =
AvaloniaProperty.RegisterDirect<HotKeyControl, ICommand>(nameof(Command),
control => control.Command, (control, command) => control.Command = command, enableDataValidation: true);
public static readonly StyledProperty<KeyGesture> HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner<Button>();
private ICommand _command;
private bool _commandCanExecute;
public ICommand Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
}
public KeyGesture HotKey
{
get { return GetValue(HotKeyProperty); }
set { SetValue(HotKeyProperty, value); }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public void CanExecuteChanged(object sender, EventArgs e)
{
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
}
}

View File

@@ -0,0 +1,25 @@
using Avalonia.OpenGL;
using SPB.Graphics.OpenGL;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public static class IGlContextExtension
{
public static OpenGLContextBase AsOpenGLContextBase(this IGlContext context)
{
var handle = (IntPtr)context.GetType().GetProperty("Handle").GetValue(context);
if (OperatingSystem.IsWindows())
{
return new AvaloniaWglContext(handle);
}
else if (OperatingSystem.IsLinux())
{
return new AvaloniaGlxContext(handle);
}
return null;
}
}
}

View File

@@ -0,0 +1,18 @@
<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"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.Ui.Controls.InputDialog">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding Message}" />
<TextBox MaxLength="{Binding MaxLength}" Grid.Row="1" Margin="10" Width="300" HorizontalAlignment="Center"
Text="{Binding Input, Mode=TwoWay}" />
<TextBlock Grid.Row="2" Margin="5, 5, 5, 10" HorizontalAlignment="Center" Text="{Binding SubMessage}" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,67 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Windows;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
public class InputDialog : UserControl
{
public string Message { get; set; }
public string Input { get; set; }
public string SubMessage { get; set; }
public uint MaxLength { get; }
public InputDialog(string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue)
{
Message = message;
Input = input;
SubMessage = subMessage;
MaxLength = maxLength;
DataContext = this;
InitializeComponent();
}
public InputDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue)
{
ContentDialog contentDialog = window.ContentDialog;
UserResult result = UserResult.Cancel;
InputDialog content = new InputDialog(message, input = "", subMessage = "", maxLength);
if (contentDialog != null)
{
contentDialog.Title = title;
contentDialog.PrimaryButtonText = LocaleManager.Instance["InputDialogOk"];
contentDialog.SecondaryButtonText = "";
contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
contentDialog.Content = content;
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Ok;
input = content.Input;
});
await contentDialog.ShowAsync();
}
return (result, input);
}
}
}

View File

@@ -0,0 +1,46 @@
using Avalonia.Data.Converters;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Globalization;
namespace Ryujinx.Ava.Ui.Controls
{
public class KeyValueConverter : IValueConverter
{
public static KeyValueConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return null;
}
return value.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
object key = null;
if (value != null)
{
if (targetType == typeof(Key))
{
key = Enum.Parse<Key>(value.ToString());
}
else if (targetType == typeof(GamepadInputId))
{
key = Enum.Parse<GamepadInputId>(value.ToString());
}
else if (targetType == typeof(StickInputId))
{
key = Enum.Parse<StickInputId>(value.ToString());
}
}
return key;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Ryujinx.Ava.Ui.Models
{
public sealed class MiniCommand<T> : MiniCommand, ICommand
{
private readonly Action<T> _callback;
private bool _busy;
private Func<T, Task> _asyncCallback;
public MiniCommand(Action<T> callback)
{
_callback = callback;
}
public MiniCommand(Func<T, Task> callback)
{
_asyncCallback = callback;
}
private bool Busy
{
get => _busy;
set
{
_busy = value;
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
public override event EventHandler CanExecuteChanged;
public override bool CanExecute(object parameter) => !_busy;
public override async void Execute(object parameter)
{
if (Busy)
{
return;
}
try
{
Busy = true;
if (_callback != null)
{
_callback((T)parameter);
}
else
{
await _asyncCallback((T)parameter);
}
}
finally
{
Busy = false;
}
}
}
public abstract class MiniCommand : ICommand
{
public static MiniCommand Create(Action callback) => new MiniCommand<object>(_ => callback());
public static MiniCommand Create<TArg>(Action<TArg> callback) => new MiniCommand<TArg>(callback);
public static MiniCommand CreateFromTask(Func<Task> callback) => new MiniCommand<object>(_ => callback());
public abstract bool CanExecute(object parameter);
public abstract void Execute(object parameter);
public abstract event EventHandler CanExecuteChanged;
}
}

View File

@@ -0,0 +1,40 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Ryujinx.Ava.Ui.Controls
{
public class OffscreenTextBox : TextBox
{
public RoutedEvent<KeyEventArgs> GetKeyDownRoutedEvent()
{
return KeyDownEvent;
}
public RoutedEvent<KeyEventArgs> GetKeyUpRoutedEvent()
{
return KeyUpEvent;
}
public void SendKeyDownEvent(KeyEventArgs keyEvent)
{
OnKeyDown(keyEvent);
}
public void SendKeyUpEvent(KeyEventArgs keyEvent)
{
OnKeyUp(keyEvent);
}
public void SendText(string text)
{
OnTextInput(new TextInputEventArgs()
{
Text = text,
Device = KeyboardDevice.Instance,
Source = this,
RoutedEvent = TextInputEvent
});
}
}
}

View File

@@ -0,0 +1,20 @@
using OpenTK;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public class OpenToolkitBindingsContext : IBindingsContext
{
private readonly Func<string, IntPtr> _getProcAddress;
public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress)
{
_getProcAddress = getProcAddress;
}
public IntPtr GetProcAddress(string procName)
{
return _getProcAddress(procName);
}
}
}

View File

@@ -0,0 +1,100 @@
using Avalonia.Rendering;
using System;
using System.Threading;
using System.Timers;
namespace Ryujinx.Ava.Ui.Controls
{
internal class RenderTimer : IRenderTimer, IDisposable
{
public event Action<TimeSpan> Tick
{
add
{
_tick += value;
if (_subscriberCount++ == 0)
{
Start();
}
}
remove
{
if (--_subscriberCount == 0)
{
Stop();
}
_tick -= value;
}
}
private Thread _tickThread;
private readonly System.Timers.Timer _timer;
private Action<TimeSpan> _tick;
private int _subscriberCount;
private bool _isRunning;
private AutoResetEvent _resetEvent;
public RenderTimer()
{
_timer = new System.Timers.Timer(15);
_resetEvent = new AutoResetEvent(true);
_timer.Elapsed += Timer_Elapsed;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
TickNow();
}
public void Start()
{
_timer.Start();
if (_tickThread == null)
{
_tickThread = new Thread(RunTick);
_tickThread.Name = "RenderTimerTickThread";
_tickThread.IsBackground = true;
_isRunning = true;
_tickThread.Start();
}
}
public void RunTick()
{
while (_isRunning)
{
_resetEvent.WaitOne();
_tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
}
}
public void TickNow()
{
lock (_timer)
{
_resetEvent.Set();
}
}
public void Stop()
{
_timer.Stop();
}
public void Dispose()
{
_timer.Elapsed -= Timer_Elapsed;
_timer.Stop();
_isRunning = false;
_resetEvent.Set();
_tickThread.Join();
_resetEvent.Dispose();
}
}
}

View File

@@ -0,0 +1,273 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using SkiaSharp;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public class RendererControl : Control
{
private int _image;
static RendererControl()
{
AffectsRender<RendererControl>(ImageProperty);
}
public readonly static StyledProperty<int> ImageProperty =
AvaloniaProperty.Register<RendererControl, int>(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay);
protected int Image
{
get => _image;
set => SetAndRaise(ImageProperty, ref _image, value);
}
public event EventHandler<EventArgs> GlInitialized;
public event EventHandler<Size> SizeChanged;
protected Size RenderSize { get; private set; }
public bool IsStarted { get; private set; }
public int Major { get; }
public int Minor { get; }
public GraphicsDebugLevel DebugLevel { get; }
public OpenGLContextBase GameContext { get; set; }
public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>().PrimaryContext.AsOpenGLContextBase();
private SwappableNativeWindowBase _gameBackgroundWindow;
private bool _isInitialized;
private IntPtr _fence;
private GlDrawOperation _glDrawOperation;
public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
{
Major = major;
Minor = minor;
DebugLevel = graphicsDebugLevel;
IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty);
resizeObservable.Subscribe(Resized);
Focusable = true;
}
private void Resized(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
RenderSize = rect.Size * Program.WindowScaleFactor;
_glDrawOperation?.Dispose();
_glDrawOperation = new GlDrawOperation(this);
}
public override void Render(DrawingContext context)
{
if (!_isInitialized)
{
CreateWindow();
OnGlInitialized();
_isInitialized = true;
}
if (GameContext == null || !IsStarted || Image == 0)
{
return;
}
if (_glDrawOperation != null)
{
context.Custom(_glDrawOperation);
}
base.Render(context);
}
protected void OnGlInitialized()
{
GlInitialized?.Invoke(this, EventArgs.Empty);
}
public void QueueRender()
{
Program.RenderTimer.TickNow();
}
internal void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = (int)image;
}).Wait();
if (_fence != IntPtr.Zero)
{
GL.DeleteSync(_fence);
}
_fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
QueueRender();
_gameBackgroundWindow.SwapBuffers();
}
internal void Start()
{
IsStarted = true;
QueueRender();
}
internal void Stop()
{
IsStarted = false;
}
public void DestroyBackgroundContext()
{
_image = 0;
if (_fence != IntPtr.Zero)
{
_glDrawOperation.Dispose();
GL.DeleteSync(_fence);
}
GlDrawOperation.DeleteFramebuffer();
GameContext?.Dispose();
_gameBackgroundWindow?.Dispose();
}
internal void MakeCurrent()
{
GameContext.MakeCurrent(_gameBackgroundWindow);
}
internal void MakeCurrent(SwappableNativeWindowBase window)
{
GameContext.MakeCurrent(window);
}
protected void CreateWindow()
{
var flags = OpenGLContextFlags.Compat;
if (DebugLevel != GraphicsDebugLevel.None)
{
flags |= OpenGLContextFlags.Debug;
}
_gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
_gameBackgroundWindow.Hide();
GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext);
GameContext.Initialize(_gameBackgroundWindow);
}
private class GlDrawOperation : ICustomDrawOperation
{
private static int _framebuffer;
public Rect Bounds { get; }
private readonly RendererControl _control;
public GlDrawOperation(RendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose() { }
public static void DeleteFramebuffer()
{
if (_framebuffer == 0)
{
GL.DeleteFramebuffer(_framebuffer);
}
_framebuffer = 0;
}
public bool Equals(ICustomDrawOperation other)
{
return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
private void CreateRenderTarget()
{
_framebuffer = GL.GenFramebuffer();
}
public void Render(IDrawingContextImpl context)
{
if (_control.Image == 0)
{
return;
}
if (_framebuffer == 0)
{
CreateRenderTarget();
}
int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding);
var image = _control.Image;
var fence = _control._fence;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer);
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888);
var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat());
GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue);
using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo);
using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
}
}
}
}

View File

@@ -0,0 +1,47 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Graphics.OpenGL;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
namespace Ryujinx.Ava.Ui.Controls
{
class SPBOpenGLContext : IOpenGLContext
{
private OpenGLContextBase _context;
private NativeWindowBase _window;
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
{
_context = context;
_window = window;
}
public void Dispose()
{
_context.Dispose();
_window.Dispose();
}
public void MakeCurrent()
{
_context.MakeCurrent(_window);
}
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
{
OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
context.Initialize(window);
context.MakeCurrent(window);
GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress));
context.MakeCurrent(null);
return new SPBOpenGLContext(context, window);
}
}
}

View File

@@ -0,0 +1,28 @@
<Window 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"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.Ui.Controls.UpdateWaitWindow"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
Title="Ryujinx - Waiting">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Image Grid.Row="1" Margin="5, 10, 20 , 10" Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common"
Height="70"
MinWidth="50" />
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Vertical">
<TextBlock Margin="5" Name="PrimaryText" />
<TextBlock VerticalAlignment="Center" Name="SecondaryText" Margin="5" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,35 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Ui.Windows;
namespace Ryujinx.Ava.Ui.Controls
{
public class UpdateWaitWindow : StyleableWindow
{
public UpdateWaitWindow(string primaryText, string secondaryText) : this()
{
PrimaryText.Text = primaryText;
SecondaryText.Text = secondaryText;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
public UpdateWaitWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
public TextBlock PrimaryText { get; private set; }
public TextBlock SecondaryText { get; private set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
PrimaryText = this.FindControl<TextBlock>("PrimaryText");
SecondaryText = this.FindControl<TextBlock>("SecondaryText");
}
}
}

View File

@@ -0,0 +1,91 @@
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Helper;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
internal class UserErrorDialog
{
private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide";
private static string GetErrorCode(UserError error)
{
return $"RYU-{(uint)error:X4}";
}
private static string GetErrorTitle(UserError error)
{
return error switch
{
UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeys"],
UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmware"],
UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailed"],
UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFound"],
UserError.Unknown => LocaleManager.Instance["UserErrorUnknown"],
_ => LocaleManager.Instance["UserErrorUndefined"]
};
}
private static string GetErrorDescription(UserError error)
{
return error switch
{
UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeysDescription"],
UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmwareDescription"],
UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailedDescription"],
UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFoundDescription"],
UserError.Unknown => LocaleManager.Instance["UserErrorUnknownDescription"],
_ => LocaleManager.Instance["UserErrorUndefinedDescription"]
};
}
private static bool IsCoveredBySetupGuide(UserError error)
{
return error switch
{
UserError.NoKeys or
UserError.NoFirmware or
UserError.FirmwareParsingFailed => true,
_ => false
};
}
private static string GetSetupGuideUrl(UserError error)
{
if (!IsCoveredBySetupGuide(error))
{
return null;
}
return error switch
{
UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys",
UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware",
_ => SetupGuideUrl
};
}
public static async Task ShowUserErrorDialog(UserError error, StyleableWindow owner)
{
string errorCode = GetErrorCode(error);
bool isInSetupGuide = IsCoveredBySetupGuide(error);
string setupButtonLabel = isInSetupGuide ? LocaleManager.Instance["OpenSetupGuideMessage"] : "";
var result = await ContentDialogHelper.CreateInfoDialog(owner,
string.Format(LocaleManager.Instance["DialogUserErrorDialogMessage"], errorCode, GetErrorTitle(error)),
GetErrorDescription(error) + (isInSetupGuide
? LocaleManager.Instance["DialogUserErrorDialogInfoMessage"]
: ""), setupButtonLabel, LocaleManager.Instance["InputDialogOk"],
string.Format(LocaleManager.Instance["DialogUserErrorDialogTitle"], errorCode));
if (result == UserResult.Ok)
{
OpenHelper.OpenUrl(GetSetupGuideUrl(error));
}
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Ryujinx.Ava.Ui.Controls
{
public enum UserResult
{
Ok,
Yes,
No,
Abort,
Cancel,
None,
}
}