From 6d078a89652d74484a1a31bc2605faab4807b25b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 22 Sep 2022 11:02:08 -0700 Subject: [PATCH 1/8] avn: update to latest Avalonia UI --- src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj b/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj index a7ade3aaa..961f67649 100644 --- a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj +++ b/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj @@ -11,9 +11,9 @@ - - - + + + From 4fb392fdf55c59d676d997703d00d17cfd2aadd2 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 22 Sep 2022 11:02:22 -0700 Subject: [PATCH 2/8] avn: workaround a macOS window focus bug For some reason on macOS windows that are activated are not appearing on top, and get lost behind other application windows. The platform implementation on macOS is seemingly correctly calling the `makeKeyAndOrderFront` native windowing API, but it's not being made "key and front". Setting the "topmost" flag true (and then false again) brings our windows to the front, which is what we want. --- src/shared/Core.UI.Avalonia/AvaloniaUi.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/shared/Core.UI.Avalonia/AvaloniaUi.cs b/src/shared/Core.UI.Avalonia/AvaloniaUi.cs index a7e698f58..0b0bea8fb 100644 --- a/src/shared/Core.UI.Avalonia/AvaloniaUi.cs +++ b/src/shared/Core.UI.Avalonia/AvaloniaUi.cs @@ -88,6 +88,15 @@ private static Task ShowWindowInternal(Func windowFunc, object dataConte window.Activate(); window.Focus(); + // Workaround an issue where "Activate()" and "Focus()" don't actually + // cause the window to become the top-most window. Avalonia is correctly + // calling 'makeKeyAndOrderFront' but this isn't working for some reason. + if (PlatformUtils.IsMacOS()) + { + window.Topmost = true; + window.Topmost = false; + } + return tcs.Task; } From 2530c2814afe36de3ca3ec6a21fd0b4ee5b63894 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 22 Sep 2022 10:27:26 -0700 Subject: [PATCH 3/8] avnui: include images in app app views in Avaloina UIs Include the images resource across all the Avalonia UI applications. --- src/shared/Core.UI.Avalonia/AvaloniaApp.axaml | 1 + src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml b/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml index 05044ae2d..33e285d77 100644 --- a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml +++ b/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml @@ -10,6 +10,7 @@ + diff --git a/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml b/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml index 1164a06d4..e05c0251f 100644 --- a/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml +++ b/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml @@ -7,13 +7,6 @@ Title="About Git Credential Manager" CanResize="False" Width="300" SizeToContent="Height" Background="#F6F6F6"> - - - - - - - @@ -37,6 +30,6 @@ + Text="Copyright © GitHub"/> From 1be714c43b3380c9602f16d556a04afe2667a19c Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 26 Jul 2022 15:44:58 +0100 Subject: [PATCH 4/8] ui: add generic 'basic' credentials UI VM and command Add a generic credentials prompt view model and command for collecting username/passwords. --- .../Core.UI/Commands/CredentialsCommand.cs | 81 +++++++++++++++++++ .../ViewModels/CredentialsViewModel.cs | 68 ++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/shared/Core.UI/Commands/CredentialsCommand.cs create mode 100644 src/shared/Core.UI/ViewModels/CredentialsViewModel.cs diff --git a/src/shared/Core.UI/Commands/CredentialsCommand.cs b/src/shared/Core.UI/Commands/CredentialsCommand.cs new file mode 100644 index 000000000..02da8472f --- /dev/null +++ b/src/shared/Core.UI/Commands/CredentialsCommand.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager.UI.ViewModels; + +namespace GitCredentialManager.UI.Commands +{ + public abstract class CredentialsCommand : HelperCommand + { + protected CredentialsCommand(ICommandContext context) + : base(context, "basic", "Show basic authentication prompt.") + { + AddOption( + new Option("--title", "Window title (optional).") + ); + + AddOption( + new Option("--resource", "Resource name or URL (optional).") + ); + + AddOption( + new Option("--username", "User name (optional).") + ); + + AddOption( + new Option("--no-logo", "Hide the Git Credential Manager logo and logotype.") + ); + + Handler = CommandHandler.Create(ExecuteAsync); + } + + private class CommandOptions + { + public string Title { get; set; } + public string Resource { get; set; } + public string UserName { get; set; } + public bool NoLogo { get; set; } + } + + private async Task ExecuteAsync(CommandOptions options) + { + var viewModel = new CredentialsViewModel(); + + viewModel.Title = !string.IsNullOrWhiteSpace(options.Title) + ? options.Title + : "Git Credential Manager"; + + viewModel.Description = !string.IsNullOrWhiteSpace(options.Resource) + ? $"Enter your credentials for '{options.Resource}'" + : "Enter your credentials"; + + if (!string.IsNullOrWhiteSpace(options.UserName)) + { + viewModel.UserName = options.UserName; + } + + viewModel.ShowProductHeader = !options.NoLogo; + + await ShowAsync(viewModel, CancellationToken.None); + + if (!viewModel.WindowResult) + { + throw new Exception("User cancelled dialog."); + } + + WriteResult( + new Dictionary + { + ["username"] = viewModel.UserName, + ["password"] = viewModel.Password + } + ); + return 0; + } + + protected abstract Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct); + } +} diff --git a/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs new file mode 100644 index 000000000..c93c8ff29 --- /dev/null +++ b/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; + +namespace GitCredentialManager.UI.ViewModels +{ + public class CredentialsViewModel : WindowViewModel + { + private string _userName; + private string _password; + private string _description; + private bool _showProductHeader; + private RelayCommand _signInCommand; + + public CredentialsViewModel() + { + SignInCommand = new RelayCommand(Accept, CanSignIn); + PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(UserName): + case nameof(Password): + SignInCommand.RaiseCanExecuteChanged(); + break; + } + } + + private bool CanSignIn() + { + // Allow empty username or empty password, or both! + // This is what the older Windows API CredUIPromptForWindowsCredentials + // permitted so we should continue to support any possible scenarios. + return true; + } + + public string UserName + { + get => _userName; + set => SetAndRaisePropertyChanged(ref _userName, value); + } + + public string Password + { + get => _password; + set => SetAndRaisePropertyChanged(ref _password, value); + } + + public string Description + { + get => _description; + set => SetAndRaisePropertyChanged(ref _description, value); + } + + public bool ShowProductHeader + { + get => _showProductHeader; + set => _showProductHeader = value; + } + + public RelayCommand SignInCommand + { + get => _signInCommand; + set => SetAndRaisePropertyChanged(ref _signInCommand, value); + } + } +} From b141c837dd7047aa08c907db2a221402ab80123f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 26 Jul 2022 15:45:51 +0100 Subject: [PATCH 5/8] ui: add Avalonia-based UI for generic cred prompt Add an Avalonia-based UI prompt for the generic/basic credential prompt. --- Git-Credential-Manager.sln | 19 ++++++ .../Controls/DialogWindow.axaml | 2 +- .../Commands/CredentialsCommandImpl.cs | 17 +++++ .../Controls/TesterWindow.axaml | 49 ++++++++++++++ .../Controls/TesterWindow.axaml.cs | 40 ++++++++++++ .../Git-Credential-Manager.UI.Avalonia.csproj | 26 ++++++++ .../Program.cs | 65 +++++++++++++++++++ .../Views/CredentialsView.axaml | 50 ++++++++++++++ .../Views/CredentialsView.axaml.cs | 43 ++++++++++++ 9 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml.cs create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Git-Credential-Manager.UI.Avalonia.csproj create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Program.cs create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Views/CredentialsView.axaml create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Views/CredentialsView.axaml.cs diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln index 9b0e76ba8..7d51e983f 100644 --- a/Git-Credential-Manager.sln +++ b/Git-Credential-Manager.sln @@ -67,6 +67,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI.Avalonia", "src\s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI.Windows", "src\windows\GitLab.UI.Windows\GitLab.UI.Windows.csproj", "{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Git-Credential-Manager.UI.Avalonia", "src\shared\Git-Credential-Manager.UI.Avalonia\Git-Credential-Manager.UI.Avalonia.csproj", "{35659127-8859-4DB9-8DD6-A08C1952632E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -469,6 +471,22 @@ Global {83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU {83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU {83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.MacDebug|Any CPU.Build.0 = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.Release|Any CPU.Build.0 = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.MacRelease|Any CPU.Build.0 = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU + {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -505,6 +523,7 @@ Global {9AFD88E2-7E2C-46DA-9D38-4342086426D3} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} {83EAC1F9-8E1F-41FC-8FC9-2C452452D64E} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} + {35659127-8859-4DB9-8DD6-A08C1952632E} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0EF9FC65-E6BA-45D4-A455-262A9EA4366B} diff --git a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml b/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml index 3ca500e90..f995a8790 100644 --- a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml +++ b/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml @@ -10,7 +10,7 @@ ExtendClientAreaChromeHints="{Binding ShowCustomChrome, Converter={x:Static converters:WindowClientAreaConverters.BoolToChromeHints}}" Title="{Binding Title}" SizeToContent="Height" CanResize="False" - Width="420" MaxHeight="520" MinHeight="320" + Width="420" MaxHeight="520" MinHeight="280" WindowState="Normal" WindowStartupLocation="CenterScreen" ShowInTaskbar="True" ShowActivated="True" PointerPressed="Window_PointerPressed" diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs new file mode 100644 index 000000000..876f95ab0 --- /dev/null +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager.UI.ViewModels; +using GitCredentialManager.UI.Views; + +namespace GitCredentialManager.UI.Commands +{ + public class CredentialsCommandImpl : CredentialsCommand + { + public CredentialsCommandImpl(ICommandContext context) : base(context) { } + + protected override Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct) + { + return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct); + } + } +} diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml b/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml new file mode 100644 index 000000000..e10e5c401 --- /dev/null +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + +