diff --git a/Controls/AppTile.xaml b/Controls/AppTile.xaml index 62b53b2..8f23c0d 100644 --- a/Controls/AppTile.xaml +++ b/Controls/AppTile.xaml @@ -8,7 +8,7 @@ mc:Ignorable="d" Margin="10, 10, 10, 10" > - diff --git a/Controls/AppTile.xaml.cs b/Controls/AppTile.xaml.cs index 292b5e2..968d3d7 100644 --- a/Controls/AppTile.xaml.cs +++ b/Controls/AppTile.xaml.cs @@ -26,6 +26,8 @@ public sealed partial class AppTile private string _Publisher; private string _Version; private Uri _Logo; + + private AppListEntry appListEntry; private async void HandleUnregister(object sender, SplitButtonClickEventArgs e) { @@ -200,19 +202,19 @@ public AppTile(string familyName) catch { Logger.WriteWarning($"Could not get the applist entries of \"{_Name}\""); - } - AppListEntry firstAppListEntry = appListEntries?.FirstOrDefault() ?? null; + } + appListEntry = appListEntries?.FirstOrDefault() ?? null; - if (firstAppListEntry == null) + if (appListEntry == null) Logger.WriteWarning($"Could not get the applist entry of \"{_Name}\""); if (String.IsNullOrEmpty(ss) || !File.Exists(ss)) { try { - if (firstAppListEntry != null) + if (appListEntry != null) { - RandomAccessStreamReference logoStream = firstAppListEntry.DisplayInfo.GetLogo(new Size(320, 180)); + RandomAccessStreamReference logoStream = appListEntry.DisplayInfo.GetLogo(new Size(320, 180)); BitmapImage logoImage = new(); using IRandomAccessStream stream = logoStream.OpenReadAsync().GetAwaiter().GetResult(); logoImage.SetSource(stream); @@ -275,24 +277,28 @@ public AppTile(string familyName) rcFlyout.ShowAt(sender as FrameworkElement, e.GetPosition(sender as UIElement)); }; - startButton.Tapped += async (s, e) => + startButton.Tapped += (_, _) => StartApp(); + } + + + public async void StartApp() + { + if (_package.Status.LicenseIssue) { - if (_package.Status.LicenseIssue) - { - Logger.WriteError($"Could not launch {_Name} due to licensing issue."); - _ = new NoticeDialog($"There is a licensing issue... Do you own this package?", $"Could not launch {_Name}").ShowAsync(); - return; - } + Logger.WriteError($"Could not launch {_Name} due to licensing issue."); + _ = new NoticeDialog($"There is a licensing issue... Do you own this package?", $"Could not launch {_Name}").ShowAsync(); + return; + } - if (firstAppListEntry == null) - { - _ = new NoticeDialog($"Could not get the applist entry of \"{_Name}\"", $"Could not launch {_Name}").ShowAsync(); - return; - } - Logger.WriteInformation($"Launching {_Name}"); - if (await firstAppListEntry.LaunchAsync() == false) - _ = new NoticeDialog($"Failed to launch \"{_Name}\"!", $"Could not launch {_Name}").ShowAsync(); - }; + if (appListEntry == null) + { + _ = new NoticeDialog($"Could not get the applist entry of \"{_Name}\"", $"Could not launch {_Name}").ShowAsync(); + return; + } + Logger.WriteInformation($"Launching {_Name}"); + if (await appListEntry.LaunchAsync() == false) + _ = new NoticeDialog($"Failed to launch \"{_Name}\"!", $"Could not launch {_Name}").ShowAsync(); } } + } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 98463e6..26ce374 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -102,9 +102,18 @@ private async void appTitleBar_Loaded(object sender, RoutedEventArgs e) if (String.IsNullOrEmpty(FSHelper.FindFileOnPath("vcruntime140d.dll"))) missing.Add("Microsoft Visual C++ Redistributable", null); - var devNotice = new NoticeDialog($"This UI is very early in development, and mainly developed by a C# learner... There WILL be bugs, and some things will NOT work...\n\nDevelopers, check Readme.md in the repo for the todolist.", "Important"); - await devNotice.ShowAsync(); + if (App.Settings.Settings.ShowDevNotice) + { + var devNotice = new NoticeDialog("This UI is very early in development, and mainly developed by a C# learner... There WILL be bugs, and some things will NOT work...\n\nDevelopers, check Readme.md in the repo for the todolist.", "Important"); + await devNotice.ShowAsync(); + + // We only show this notification once from now on + Settings.Set("ShowDevNotice", false); + Settings.Save(); + } + + if (missing.Count != 0) { // todo: properly provide download link diff --git a/Pages/AppsListPage.xaml.cs b/Pages/AppsListPage.xaml.cs index 8d27151..0ddbc39 100644 --- a/Pages/AppsListPage.xaml.cs +++ b/Pages/AppsListPage.xaml.cs @@ -8,6 +8,8 @@ using System.Security.Principal; using System.Threading.Tasks; using Windows.ApplicationModel; +using Windows.Foundation; +using Windows.Gaming.Input; using Windows.Management.Deployment; using Windows.Storage; using Windows.Storage.Pickers; @@ -21,6 +23,12 @@ namespace WinDurango.UI.Pages { public sealed partial class AppsListPage : Page { + private Gamepad gamepad; + private int currentIndex; + private Point currentPoint = new(0, 0); + private bool inputProcessed = true; + private long lastInput; + public async Task InitAppListAsync() { appList.Children.Clear(); @@ -138,14 +146,148 @@ public AppsListPage() InitializeComponent(); _ = InitAppListAsync(); - // All this is useless now basically... because InitAppListAsync now runs asynchronously (not blocking the ui thread) - /* - Stopwatch PlatinumWatch = new Stopwatch(); - Logger.WriteDebug("Initializing AppsListPage..."); - PlatinumWatch.Start(); - PlatinumWatch.Stop(); - Logger.WriteDebug("Initialized AppsListPage in {0:D2}:{1:D2}:{2:D2}.{3:D3}", (int)PlatinumWatch.Elapsed.TotalHours, (int)PlatinumWatch.Elapsed.TotalMinutes, (int)PlatinumWatch.Elapsed.TotalSeconds, (int)PlatinumWatch.Elapsed.TotalMilliseconds); - */ + Loaded += OnAppListPage_Loaded; + } + + private void OnAppListPage_Loaded(object sender, RoutedEventArgs e) + { + Gamepad.GamepadAdded += onGamepadAdded; + Gamepad.GamepadRemoved += OnGamepadRemoved; + } + + private void OnGamepadRemoved(object sender, Gamepad e) + { + gamepad = null; + } + + private void onGamepadAdded(object sender, Gamepad e) + { + gamepad = e; + ListenGamepadInput(); + this.DispatcherQueue.TryEnqueue(() => + { + if (appList.Children.Count > 0) + { + appList.Children[currentIndex].Focus(FocusState.Keyboard); + } + }); + } + + + private async void ListenGamepadInput() + { + while (gamepad != null) + { + GamepadReading gamepadInput = gamepad.GetCurrentReading(); + bool moveRight = gamepadInput.LeftThumbstickX > 0.5 || (gamepadInput.Buttons & GamepadButtons.DPadRight) != 0; + bool moveLeft = gamepadInput.LeftThumbstickX < -0.5 || (gamepadInput.Buttons & GamepadButtons.DPadLeft) != 0; + bool moveUp = gamepadInput.LeftThumbstickY > 0.5 || (gamepadInput.Buttons & GamepadButtons.DPadUp) != 0; + bool moveDown = gamepadInput.LeftThumbstickY < -0.5 || (gamepadInput.Buttons & GamepadButtons.DPadDown) != 0; + bool actionClicked = (gamepadInput.Buttons & GamepadButtons.A) != 0; + + + if (actionClicked && inputProcessed) + { + inputProcessed = false; + this.DispatcherQueue.TryEnqueue(() => + { + var appTile = appList.Children[currentIndex] as AppTile; + appTile.StartApp(); + inputProcessed = true; + }); + } + + if ((moveRight || moveLeft || moveUp || moveDown) && inputProcessed) + { + inputProcessed = false; + if (moveRight) this.DispatcherQueue.TryEnqueue(() => MoveFocus(1, 0)); + else if (moveLeft) this.DispatcherQueue.TryEnqueue(() => MoveFocus(-1, 0)); + else if (moveUp) this.DispatcherQueue.TryEnqueue(() => MoveFocus(0, -1)); + else if (moveDown) this.DispatcherQueue.TryEnqueue(() => MoveFocus(0, 1)); + } + + await Task.Delay(100); + } + } + + private void MoveFocus(int xOffset, int yOffset) + { + bool firstInput = lastInput == 0; + if (lastInput > DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - 200) + { + inputProcessed = true; + return; + } + lastInput = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (appList.Children.Count == 0) + { + inputProcessed = true; + return; + } + + // We only set focus first to the index 0 if ShowDevNotice was shown before as it cancels the initial focus done in Load event + if (firstInput && App.Settings.Settings.ShowDevNotice) + { + appList.Children[0].Focus(FocusState.Keyboard); + inputProcessed = true; + return; + } + + int columns = GetColumnCount(); + int rows = appList.Children.Count / columns; + + int newX = (int)(currentPoint.X + xOffset); + int newY = (int)(currentPoint.Y + yOffset); + + newX = Math.Clamp(newX, 0, columns - 1); + newY = Math.Clamp(newY, 0, rows - 1); + + if (newX != currentPoint.X || newY != currentPoint.Y) + { + currentPoint = new Point(newX, newY); + currentIndex = newY * columns + newX; + + if (currentIndex < appList.Children.Count) + { + appList.Children[currentIndex].Focus(FocusState.Keyboard); + } + } + + inputProcessed = true; + } + + // We need do this our self as WrapPanel doesn't have internal field or function to get current column amount + private int GetColumnCount() + { + if (appList.Children.Count == 0) return 1; + + + FrameworkElement firstItem = appList.Children[0] as FrameworkElement; + if (firstItem == null) return 1; + + + double firstItemTop = firstItem.TransformToVisual(appList).TransformPoint(new Point(0, 0)).Y; + int columnCount = 1; + + for (int i = 1; i < appList.Children.Count; i++) + { + var item = appList.Children[i] as FrameworkElement; + if (item == null) + continue; + + double itemTop = item.TransformToVisual(appList).TransformPoint(new Point(0, 0)).Y; + + if (Math.Abs(itemTop - firstItemTop) < 1) + { + columnCount++; + } + else + { + break; + } + } + + return columnCount; } private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) diff --git a/Settings/UiConfig.cs b/Settings/UiConfig.cs index bf2095c..e4294ea 100644 --- a/Settings/UiConfig.cs +++ b/Settings/UiConfig.cs @@ -32,6 +32,8 @@ public enum PatchSource public string DownloadedWdVer { get; set; } = String.Empty; public PatchSource DownloadSource { get; set; } = PatchSource.Release; + + public bool ShowDevNotice { get; set; } = true; } // TODO: fix type init exception