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:
16
Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs
Normal file
16
Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs
Normal file
16
Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs
Normal file
16
Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
35
Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs
Normal file
35
Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
357
Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
Normal file
357
Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
188
Ryujinx.Ava/Ui/Controls/GameGridView.axaml
Normal file
188
Ryujinx.Ava/Ui/Controls/GameGridView.axaml
Normal 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>
|
82
Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs
Normal file
82
Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
188
Ryujinx.Ava/Ui/Controls/GameListView.axaml
Normal file
188
Ryujinx.Ava/Ui/Controls/GameListView.axaml
Normal 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>
|
82
Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs
Normal file
82
Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
Ryujinx.Ava/Ui/Controls/Glyph.cs
Normal file
9
Ryujinx.Ava/Ui/Controls/Glyph.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
public enum Glyph : int
|
||||
{
|
||||
List,
|
||||
Grid,
|
||||
Chip
|
||||
}
|
||||
}
|
49
Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs
Normal file
49
Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
52
Ryujinx.Ava/Ui/Controls/HotKeyControl.cs
Normal file
52
Ryujinx.Ava/Ui/Controls/HotKeyControl.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs
Normal file
25
Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
18
Ryujinx.Ava/Ui/Controls/InputDialog.axaml
Normal file
18
Ryujinx.Ava/Ui/Controls/InputDialog.axaml
Normal 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>
|
67
Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs
Normal file
67
Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
46
Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs
Normal file
46
Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
71
Ryujinx.Ava/Ui/Controls/MiniCommand.cs
Normal file
71
Ryujinx.Ava/Ui/Controls/MiniCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
40
Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs
Normal file
40
Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
20
Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs
Normal file
20
Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
100
Ryujinx.Ava/Ui/Controls/RenderTimer.cs
Normal file
100
Ryujinx.Ava/Ui/Controls/RenderTimer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
273
Ryujinx.Ava/Ui/Controls/RendererControl.cs
Normal file
273
Ryujinx.Ava/Ui/Controls/RendererControl.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs
Normal file
47
Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
28
Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml
Normal file
28
Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml
Normal 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>
|
35
Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs
Normal file
35
Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
91
Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs
Normal file
91
Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
Ryujinx.Ava/Ui/Controls/UserResult.cs
Normal file
12
Ryujinx.Ava/Ui/Controls/UserResult.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
public enum UserResult
|
||||
{
|
||||
Ok,
|
||||
Yes,
|
||||
No,
|
||||
Abort,
|
||||
Cancel,
|
||||
None,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user