Merge branch 'v2_develop' into v2-IOrientation

This commit is contained in:
Tig
2024-08-02 13:22:59 -04:00
committed by GitHub
14 changed files with 250 additions and 263 deletions

View File

@@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -8,20 +8,137 @@ namespace ReactiveExample;
public class LoginView : Window, IViewFor<LoginViewModel>
{
private readonly CompositeDisposable _disposable = new ();
private const string SuccessMessage = "The input is valid!";
private const string ErrorMessage = "Please enter a valid user name and password.";
private const string ProgressMessage = "Logging in...";
private const string IdleMessage = "Press 'Login' to log in.";
private readonly CompositeDisposable _disposable = [];
public LoginView (LoginViewModel viewModel)
{
Title = $"Reactive Extensions Example - {Application.QuitKey} to Exit";
ViewModel = viewModel;
Label usernameLengthLabel = UsernameLengthLabel (TitleLabel ());
TextField usernameInput = UsernameInput (usernameLengthLabel);
Label passwordLengthLabel = PasswordLengthLabel (usernameLengthLabel);
TextField passwordInput = PasswordInput (passwordLengthLabel);
Label validationLabel = ValidationLabel (passwordInput);
Button loginButton = LoginButton (validationLabel);
Button clearButton = ClearButton (loginButton);
LoginProgressLabel (clearButton);
var title = this.AddControl<Label> (x => x.Text = "Login Form");
var unLengthLabel = title.AddControlAfter<Label> ((previous, unLength) =>
{
unLength.X = Pos.Left (previous);
unLength.Y = Pos.Top (previous) + 1;
ViewModel
.WhenAnyValue (x => x.UsernameLength)
.Select (length => $"_Username ({length} characters):")
.BindTo (unLength, x => x.Text)
.DisposeWith (_disposable);
});
unLengthLabel.AddControlAfter<TextField> ((previous, unInput) =>
{
unInput.X = Pos.Right (previous) + 1;
unInput.Y = Pos.Top (previous);
unInput.Width = 40;
unInput.Text = ViewModel.Username;
ViewModel
.WhenAnyValue (x => x.Username)
.BindTo (unInput, x => x.Text)
.DisposeWith (_disposable);
unInput
.Events ()
.TextChanged
.Select (_ => unInput.Text)
.DistinctUntilChanged ()
.BindTo (ViewModel, x => x.Username)
.DisposeWith (_disposable);
});
unLengthLabel.AddControlAfter<Label> ((previous, pwLength) =>
{
pwLength.X = Pos.Left (previous);
pwLength.Y = Pos.Top (previous) + 1;
ViewModel
.WhenAnyValue (x => x.PasswordLength)
.Select (length => $"_Password ({length} characters):")
.BindTo (pwLength, x => x.Text)
.DisposeWith (_disposable);
})
.AddControlAfter<TextField> ((previous, pwInput) =>
{
pwInput.X = Pos.Right (previous) + 1;
pwInput.Y = Pos.Top (previous);
pwInput.Width = 40;
pwInput.Text = ViewModel.Password;
ViewModel
.WhenAnyValue (x => x.Password)
.BindTo (pwInput, x => x.Text)
.DisposeWith (_disposable);
pwInput
.Events ()
.TextChanged
.Select (_ => pwInput.Text)
.DistinctUntilChanged ()
.BindTo (ViewModel, x => x.Password)
.DisposeWith (_disposable);
})
.AddControlAfter<Label> ((previous, validation) =>
{
validation.X = Pos.Left (previous);
validation.Y = Pos.Top (previous) + 1;
validation.Text = ErrorMessage;
ViewModel
.WhenAnyValue (x => x.IsValid)
.Select (valid => valid ? SuccessMessage : ErrorMessage)
.BindTo (validation, x => x.Text)
.DisposeWith (_disposable);
ViewModel
.WhenAnyValue (x => x.IsValid)
.Select (valid => valid ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"])
.BindTo (validation, x => x.ColorScheme)
.DisposeWith (_disposable);
})
.AddControlAfter<Button> ((previous, login) =>
{
login.X = Pos.Left (previous);
login.Y = Pos.Top (previous) + 1;
login.Text = "_Login";
login
.Events ()
.Accept
.InvokeCommand (ViewModel, x => x.Login)
.DisposeWith (_disposable);
})
.AddControlAfter<Button> ((previous, clear) =>
{
clear.X = Pos.Left (previous);
clear.Y = Pos.Top (previous) + 1;
clear.Text = "_Clear";
clear
.Events ()
.Accept
.InvokeCommand (ViewModel, x => x.ClearCommand)
.DisposeWith (_disposable);
})
.AddControlAfter<Label> ((previous, progress) =>
{
progress.X = Pos.Left (previous);
progress.Y = Pos.Top (previous) + 1;
progress.Width = 40;
progress.Height = 1;
progress.Text = IdleMessage;
ViewModel
.WhenAnyObservable (x => x.Login.IsExecuting)
.Select (executing => executing ? ProgressMessage : IdleMessage)
.ObserveOn (RxApp.MainThreadScheduler)
.BindTo (progress, x => x.Text)
.DisposeWith (_disposable);
});
}
public LoginViewModel ViewModel { get; set; }
@@ -37,169 +154,4 @@ public class LoginView : Window, IViewFor<LoginViewModel>
_disposable.Dispose ();
base.Dispose (disposing);
}
private Button ClearButton (View previous)
{
var clearButton = new Button
{
X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Text = "_Clear"
};
clearButton
.Events ()
.Accept
.InvokeCommand (ViewModel, x => x.Clear)
.DisposeWith (_disposable);
Add (clearButton);
return clearButton;
}
private Button LoginButton (View previous)
{
var loginButton = new Button
{
X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Text = "_Login"
};
loginButton
.Events ()
.Accept
.InvokeCommand (ViewModel, x => x.Login)
.DisposeWith (_disposable);
Add (loginButton);
return loginButton;
}
private Label LoginProgressLabel (View previous)
{
var progress = "Logging in...";
var idle = "Press 'Login' to log in.";
var loginProgressLabel = new Label
{
X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Width = 40, Height = 1, Text = idle
};
ViewModel
.WhenAnyObservable (x => x.Login.IsExecuting)
.Select (executing => executing ? progress : idle)
.ObserveOn (RxApp.MainThreadScheduler)
.BindTo (loginProgressLabel, x => x.Text)
.DisposeWith (_disposable);
Add (loginProgressLabel);
return loginProgressLabel;
}
private TextField PasswordInput (View previous)
{
var passwordInput = new TextField
{
X = Pos.Right (previous) + 1, Y = Pos.Top (previous), Width = 40, Text = ViewModel.Password
};
ViewModel
.WhenAnyValue (x => x.Password)
.BindTo (passwordInput, x => x.Text)
.DisposeWith (_disposable);
passwordInput
.Events ()
.TextChanged
.Select (old => passwordInput.Text)
.DistinctUntilChanged ()
.BindTo (ViewModel, x => x.Password)
.DisposeWith (_disposable);
Add (passwordInput);
return passwordInput;
}
private Label PasswordLengthLabel (View previous)
{
var passwordLengthLabel = new Label { X = Pos.Left (previous), Y = Pos.Top (previous) + 1, };
ViewModel
.WhenAnyValue (x => x.PasswordLength)
.Select (length => $"_Password ({length} characters):")
.BindTo (passwordLengthLabel, x => x.Text)
.DisposeWith (_disposable);
Add (passwordLengthLabel);
return passwordLengthLabel;
}
private Label TitleLabel ()
{
var label = new Label { Text = "Login Form" };
Add (label);
return label;
}
private TextField UsernameInput (View previous)
{
var usernameInput = new TextField
{
X = Pos.Right (previous) + 1, Y = Pos.Top (previous), Width = 40, Text = ViewModel.Username
};
ViewModel
.WhenAnyValue (x => x.Username)
.BindTo (usernameInput, x => x.Text)
.DisposeWith (_disposable);
usernameInput
.Events ()
.TextChanged
.Select (old => usernameInput.Text)
.DistinctUntilChanged ()
.BindTo (ViewModel, x => x.Username)
.DisposeWith (_disposable);
Add (usernameInput);
return usernameInput;
}
private Label UsernameLengthLabel (View previous)
{
var usernameLengthLabel = new Label { X = Pos.Left (previous), Y = Pos.Top (previous) + 1 };
ViewModel
.WhenAnyValue (x => x.UsernameLength)
.Select (length => $"_Username ({length} characters):")
.BindTo (usernameLengthLabel, x => x.Text)
.DisposeWith (_disposable);
Add (usernameLengthLabel);
return usernameLengthLabel;
}
private Label ValidationLabel (View previous)
{
var error = "Please enter a valid user name and password.";
var success = "The input is valid!";
var validationLabel = new Label
{
X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Text = error
};
ViewModel
.WhenAnyValue (x => x.IsValid)
.Select (valid => valid ? success : error)
.BindTo (validationLabel, x => x.Text)
.DisposeWith (_disposable);
ViewModel
.WhenAnyValue (x => x.IsValid)
.Select (valid => valid ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"])
.BindTo (validationLabel, x => x.ColorScheme)
.DisposeWith (_disposable);
Add (validationLabel);
return validationLabel;
}
}

View File

@@ -5,7 +5,7 @@ using System.Reactive.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using ReactiveUI.SourceGenerators;
namespace ReactiveExample;
@@ -21,41 +21,53 @@ namespace ReactiveExample;
// See also: https://www.reactiveui.net../docs/handbook/data-persistence/
//
[DataContract]
public class LoginViewModel : ReactiveObject
public partial class LoginViewModel : ReactiveObject
{
private readonly ObservableAsPropertyHelper<bool> _isValid;
private readonly ObservableAsPropertyHelper<int> _passwordLength;
private readonly ObservableAsPropertyHelper<int> _usernameLength;
[IgnoreDataMember]
[ObservableAsProperty] private bool _isValid;
[IgnoreDataMember]
[ObservableAsProperty] private int _passwordLength;
[IgnoreDataMember]
[ObservableAsProperty] private int _usernameLength;
[DataMember]
[Reactive] private string _password = string.Empty;
[DataMember]
[Reactive] private string _username = string.Empty;
public LoginViewModel ()
{
IObservable<bool> canLogin = this.WhenAnyValue (
x => x.Username,
x => x.Password,
(username, password) =>
!string.IsNullOrEmpty (username) && !string.IsNullOrEmpty (password)
);
InitializeCommands ();
IObservable<bool> canLogin = this.WhenAnyValue
(
x => x.Username,
x => x.Password,
(username, password) =>
!string.IsNullOrEmpty (username) && !string.IsNullOrEmpty (password)
);
_isValid = canLogin.ToProperty (this, x => x.IsValid);
_isValidHelper = canLogin.ToProperty (this, x => x.IsValid);
Login = ReactiveCommand.CreateFromTask<CancelEventArgs> (
e => Task.Delay (TimeSpan.FromSeconds (1)),
canLogin
);
Login = ReactiveCommand.CreateFromTask<HandledEventArgs>
(
e => Task.Delay (TimeSpan.FromSeconds (1)),
canLogin
);
_usernameLength = this
_usernameLengthHelper = this
.WhenAnyValue (x => x.Username)
.Select (name => name.Length)
.ToProperty (this, x => x.UsernameLength);
_passwordLength = this
_passwordLengthHelper = this
.WhenAnyValue (x => x.Password)
.Select (password => password.Length)
.ToProperty (this, x => x.PasswordLength);
Clear = ReactiveCommand.Create<CancelEventArgs> (e => { });
Clear.Subscribe (
ClearCommand.Subscribe (
unit =>
{
Username = string.Empty;
@@ -64,26 +76,9 @@ public class LoginViewModel : ReactiveObject
);
}
[IgnoreDataMember]
public ReactiveCommand<CancelEventArgs, Unit> Clear { get; }
[ReactiveCommand]
public void Clear (HandledEventArgs args) { }
[IgnoreDataMember]
public bool IsValid => _isValid.Value;
[IgnoreDataMember]
public ReactiveCommand<CancelEventArgs, Unit> Login { get; }
[Reactive]
[DataMember]
public string Password { get; set; } = string.Empty;
[IgnoreDataMember]
public int PasswordLength => _passwordLength.Value;
[Reactive]
[DataMember]
public string Username { get; set; } = string.Empty;
[IgnoreDataMember]
public int UsernameLength => _usernameLength.Value;
public ReactiveCommand<HandledEventArgs, Unit> Login { get; }
}

View File

@@ -12,7 +12,7 @@ public static class Program
RxApp.MainThreadScheduler = TerminalScheduler.Default;
RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
Application.Run (new LoginView (new LoginViewModel ()));
Application.Top.Dispose();
Application.Top.Dispose ();
Application.Shutdown ();
}
}

View File

@@ -1,4 +1,4 @@
This is a sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [Pharmacist](https://github.com/reactiveui/pharmacist) — a tool that converts all events in a NuGet package into observable wrappers.
This is a sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [ObservableEvents](https://github.com/reactivemarbles/ObservableEvents) — a Source Generator that turns events into observable wrappers.
<img src="https://user-images.githubusercontent.com/6759207/94748621-646a7280-038a-11eb-8ea0-34629dc799b3.gif" width="450">

View File

@@ -11,9 +11,9 @@
<InformationalVersion>2.0</InformationalVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI.Fody" Version="[19.5.41,21)" />
<PackageReference Include="ReactiveUI" Version="[20.1.1,21)" />
<PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" PrivateAssets="all" />
<PackageReference Include="ReactiveUI.SourceGenerators" Version="[1.0.3,2)" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />

View File

@@ -0,0 +1,24 @@
using System;
using Terminal.Gui;
namespace ReactiveExample;
public static class ViewExtensions
{
public static (Window MainView, TOut LastControl) AddControl<TOut> (this Window view, Action<TOut> action)
where TOut : View, new()
{
TOut result = new ();
action (result);
view.Add (result);
return (view, result);
}
public static (Window MainView, TOut LastControl) AddControlAfter<TOut> (this (Window MainView, View LastControl) view, Action<View, TOut> action)
where TOut : View, new()
{
TOut result = new ();
action (view.LastControl, result);
view.MainView.Add (result);
return (view.MainView, result);
}
}

View File

@@ -1,6 +1,8 @@
// This is a simple example application for a self-contained single file.
// This is a test application for a self-contained single file.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Terminal.Gui;
namespace SelfContained;
@@ -10,7 +12,28 @@ public static class Program
[RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, ConsoleDriver)")]
private static void Main (string [] args)
{
Application.Run<ExampleWindow> ().Dispose ();
Application.Init ();
#region The code in this region is not intended for use in a self-contained single-file. It's just here to make sure there is no functionality break with localization in Terminal.Gui using single-file
if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture) && Application.SupportedCultures.Count == 0)
{
// Only happens if the project has <InvariantGlobalization>true</InvariantGlobalization>
Debug.Assert (Application.SupportedCultures.Count == 0);
}
else
{
Debug.Assert (Application.SupportedCultures.Count > 0);
Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture));
}
#endregion
ExampleWindow app = new ();
Application.Run (app);
// Dispose the app object before shutdown
app.Dispose ();
// Before the application exits, reset Terminal.Gui for clean shutdown
Application.Shutdown ();

View File

@@ -1,6 +1,6 @@
# Terminal.Gui C# SelfContained
This example shows how to use the `Terminal.Gui` library to create a simple `self-contained` `single file` GUI application in C#.
This project aims to test the `Terminal.Gui` library to create a simple `self-contained` `single-file` GUI application in C#, ensuring that all its features are available.
With `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish`.

View File

@@ -8,7 +8,7 @@
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<PublishSingleFile>true</PublishSingleFile>
<InvariantGlobalization>true</InvariantGlobalization>
<InvariantGlobalization>false</InvariantGlobalization>
<DebugType>embedded</DebugType>
</PropertyGroup>

View File

@@ -2,6 +2,8 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Resources;
using Terminal.Gui.Resources;
namespace Terminal.Gui;
@@ -48,7 +50,7 @@ public static partial class Application
internal static List<CultureInfo> GetSupportedCultures ()
{
CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures);
CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures);
// Get the assembly
var assembly = Assembly.GetExecutingAssembly ();
@@ -57,15 +59,35 @@ public static partial class Application
string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory;
// Find the resource file name of the assembly
var resourceFilename = $"{Path.GetFileNameWithoutExtension (AppContext.BaseDirectory)}.resources.dll";
var resourceFilename = $"{assembly.GetName ().Name}.resources.dll";
// Return all culture for which satellite folder found with culture code.
return culture.Where (
cultureInfo =>
Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name))
&& File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
)
.ToList ();
if (cultures.Length > 1 && Directory.Exists (Path.Combine (assemblyLocation, "pt-PT")))
{
// Return all culture for which satellite folder found with culture code.
return cultures.Where (
cultureInfo =>
Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name))
&& File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
)
.ToList ();
}
// It's called from a self-contained single-file and get available cultures from the embedded resources strings.
return GetAvailableCulturesFromEmbeddedResources ();
}
internal static List<CultureInfo> GetAvailableCulturesFromEmbeddedResources ()
{
ResourceManager rm = new (typeof (Strings));
CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures);
return cultures.Where (
cultureInfo =>
!cultureInfo.Equals (CultureInfo.InvariantCulture)
&& rm.GetResourceSet (cultureInfo, true, false) is { }
)
.ToList ();
}
/// <summary>

View File

@@ -31,7 +31,7 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21,2)" />
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.4,4)" />
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.5,4)" />
<PackageReference Include="CsvHelper" Version="[33.0.1,34)" />
<PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
<PackageReference Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />

View File

@@ -1,5 +1,4 @@
using Microsoft.VisualBasic;
using Xunit.Abstractions;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
@@ -193,6 +192,7 @@ public class ApplicationTests
// Internal properties
Assert.False (Application._initialized);
Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures);
Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources(), Application.SupportedCultures);
Assert.False (Application._forceFakeConsole);
Assert.Equal (-1, Application._mainThreadId);
Assert.Empty (Application._topLevels);