diff --git a/Tryouts/Prototypes/Shell/App.xaml.cs b/Tryouts/Prototypes/Shell/App.xaml.cs index e30b2c5d4..0d17da1f1 100644 --- a/Tryouts/Prototypes/Shell/App.xaml.cs +++ b/Tryouts/Prototypes/Shell/App.xaml.cs @@ -12,14 +12,8 @@ // * and limitations under the License. // */ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using System.Windows; +using Shell.Utilities; namespace Shell { @@ -30,17 +24,24 @@ public partial class App : Application { private void Application_Startup(object sender, StartupEventArgs e) { - if (e.Args.Length != 0) + if (e.Args.Length != 0 + && CommandLineParser.TryParse(e.Args, out var webWindowOptions) + && webWindowOptions.Url != null) { - MainWebWindowOptions webWindowOptions = MainWebWindowOptionsParser.Parse(e.Args); - Application.Current.MainWindow = new MainWebWindow(webWindowOptions); - Application.Current.MainWindow.Show(); - } - else - { - Application.Current.MainWindow = new MainWindow(); - Application.Current.MainWindow.Show(); + StartWithWebWindowOptions(webWindowOptions); + + return; } + + Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose; + new MainWindow().Show(); + } + + private void StartWithWebWindowOptions(WebWindowOptions options) + { + var webWindow = new WebWindow(options); + Application.Current.ShutdownMode = ShutdownMode.OnLastWindowClose; + webWindow.Show(); } } } diff --git a/Tryouts/Prototypes/Shell/ImageSource/IImageSourcePolicy.cs b/Tryouts/Prototypes/Shell/ImageSource/IImageSourcePolicy.cs index 49f32a572..191bd35d9 100644 --- a/Tryouts/Prototypes/Shell/ImageSource/IImageSourcePolicy.cs +++ b/Tryouts/Prototypes/Shell/ImageSource/IImageSourcePolicy.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shell.ImageSource { diff --git a/Tryouts/Prototypes/Shell/MainWebWindow.xaml.cs b/Tryouts/Prototypes/Shell/MainWebWindow.xaml.cs deleted file mode 100644 index cba8a62f8..000000000 --- a/Tryouts/Prototypes/Shell/MainWebWindow.xaml.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; -using System.CommandLine; -using System.IO; -using System.CommandLine.Binding; -using System.Security.Policy; -using Shell.ImageSource; - -namespace Shell -{ - /// - /// Interaction logic for MainWebWindow.xaml - /// - public partial class MainWebWindow : Window - { - ImageSourceProvider _iconProvider = new ImageSourceProvider(new EnvironmentImageSourcePolicy()); - - public MainWebWindow(MainWebWindowOptions webWindowOptions) - { - InitializeComponent(); - - var appUrl = new Uri(webWindowOptions.Url ?? MainWebWindowOptions.DefaultUrl); - var iconUrl = webWindowOptions.IconUrl != null ? new Uri(webWindowOptions.IconUrl, UriKind.RelativeOrAbsolute) : null; - - Title = webWindowOptions.Title ?? MainWebWindowOptions.DefaultTitle; - Width = webWindowOptions.Width ?? MainWebWindowOptions.DefaultWidth; - Height = webWindowOptions.Height ?? MainWebWindowOptions.DefaultHeight; - if (iconUrl != null) - { - Icon = _iconProvider.GetImageSource(iconUrl, appUrl); - } - webView.Source = appUrl; - } - } -} \ No newline at end of file diff --git a/Tryouts/Prototypes/Shell/MainWebWindowOptions.cs b/Tryouts/Prototypes/Shell/MainWebWindowOptions.cs deleted file mode 100644 index f2f6b44c4..000000000 --- a/Tryouts/Prototypes/Shell/MainWebWindowOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shell -{ - public sealed class MainWebWindowOptions - { - public double? Height { get; set; } - public string? Title { get; set; } - public string? Url { get; set; } - public string? IconUrl { get; set; } - public double? Width { get; set; } - - public const double DefaultHeight = 450d; - public const string? DefaultTitle = "Compose Web Container"; - public const string? DefaultUrl = "about:blank"; - public const double DefaultWidth = 800d; - } -} diff --git a/Tryouts/Prototypes/Shell/MainWebWindowOptionsParser.cs b/Tryouts/Prototypes/Shell/MainWebWindowOptionsParser.cs deleted file mode 100644 index 1f2e082ba..000000000 --- a/Tryouts/Prototypes/Shell/MainWebWindowOptionsParser.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Binding; -using System.CommandLine.Parsing; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media.Media3D; - -namespace Shell -{ - internal static class MainWebWindowOptionsParser - { - private static Option titleOption = new Option("--title", description: "Set title for the window"); - private static Option urlOption = new Option("--url", description: "Set url for the webview. default: about:blank"); - private static Option widthOption = new Option("--width", description: "Set width for the window"); - private static Option heightOption = new Option("--height", description: "Set height for the window"); - private static Option iconOption = new Option("--icon", description: "Set the icon for the window"); - private static RootCommand rootCommand = new RootCommand - { - titleOption, - urlOption, - widthOption, - heightOption, - iconOption - }; - - public static MainWebWindowOptions Parse(string[] args) - { - Parser parser = new Parser(rootCommand); - ParseResult parseResult = parser.Parse(args); - - MainWebWindowOptions options = new MainWebWindowOptions - { - Title = parseResult.GetValueForOption(titleOption), - Url = parseResult.GetValueForOption(urlOption), - Width = parseResult.GetValueForOption(widthOption), - Height = parseResult.GetValueForOption(heightOption), - IconUrl = parseResult.GetValueForOption(iconOption) - }; - - return options; - } - } -} diff --git a/Tryouts/Prototypes/Shell/MainWindow.xaml.cs b/Tryouts/Prototypes/Shell/MainWindow.xaml.cs index 7f031ce6e..81d6a4fd7 100644 --- a/Tryouts/Prototypes/Shell/MainWindow.xaml.cs +++ b/Tryouts/Prototypes/Shell/MainWindow.xaml.cs @@ -12,24 +12,11 @@ // * and limitations under the License. // */ -using Manifest; -using Shell.ImageSource; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using Manifest; namespace Shell { @@ -38,47 +25,45 @@ namespace Shell /// public partial class MainWindow : Window { - internal List WebContentList { get; set; } = new List(); - private ManifestModel _config; - private ModuleModel[]? _modules; - private ImageSourceProvider _iconProvider = new ImageSourceProvider(new EnvironmentImageSourcePolicy()); + internal List webWindows { get; set; } = new List(); + private ManifestModel config; + private ModuleModel[]? modules; public MainWindow() { InitializeComponent(); - _config = ManifestParser.OpenManifestFile("exampleManifest.json"); - _modules = _config.Modules; - DataContext = _modules; + config = ManifestParser.OpenManifestFile("exampleManifest.json"); + modules = config.Modules; + DataContext = modules; } - private void CreateViews(ModuleModel item) + private void CreateWebWindow(ModuleModel item) { - var opt = new WebContentOptions() + var options = new WebWindowOptions { Title = item.AppName, - Uri = new Uri(item.Url), - IconUri = string.IsNullOrEmpty(item.IconUrl) ? null : new Uri(item.IconUrl) + Url = item.Url, + IconUrl = item.IconUrl }; - var webContent = new WebContent(opt, _iconProvider); - webContent.Owner = this; - webContent.Closed += WebContent_Closed; - - WebContentList.Add(webContent); - webContent.Show(); + var webWindow = new WebWindow(options); + webWindow.Owner = this; + webWindow.Closed += WebWindowClosed; + webWindows.Add(webWindow); + webWindow.Show(); } - private void WebContent_Closed(object? sender, EventArgs e) + private void WebWindowClosed(object? sender, EventArgs e) { - WebContentList.Remove((WebContent)sender); + webWindows.Remove((WebWindow)sender!); } - + private void ShowChild_Click(object sender, RoutedEventArgs e) { var context = ((Button)sender).DataContext; - - CreateViews((ModuleModel)context); + + CreateWebWindow((ModuleModel)context); } } } diff --git a/Tryouts/Prototypes/Shell/Manifest/ManifestModel.cs b/Tryouts/Prototypes/Shell/Manifest/ManifestModel.cs index 5726bd6e9..63979e4b9 100644 --- a/Tryouts/Prototypes/Shell/Manifest/ManifestModel.cs +++ b/Tryouts/Prototypes/Shell/Manifest/ManifestModel.cs @@ -12,10 +12,6 @@ // * and limitations under the License. // */ -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.Json.Serialization; using System.Text.Json; namespace Manifest diff --git a/Tryouts/Prototypes/Shell/Manifest/ManifestParser.cs b/Tryouts/Prototypes/Shell/Manifest/ManifestParser.cs index 90e275ceb..b2314d5ae 100644 --- a/Tryouts/Prototypes/Shell/Manifest/ManifestParser.cs +++ b/Tryouts/Prototypes/Shell/Manifest/ManifestParser.cs @@ -13,14 +13,8 @@ // */ using System; -using System.Collections.Generic; using System.IO; -using System.Security.Policy; -using System.Text; using System.Text.Json; -using System.Windows.Automation; -using System.Windows.Controls.Primitives; -using System.Windows.Documents; namespace Manifest { diff --git a/Tryouts/Prototypes/Shell/Properties/launchSettings.json b/Tryouts/Prototypes/Shell/Properties/launchSettings.json index be34b65a9..fd14c0999 100644 --- a/Tryouts/Prototypes/Shell/Properties/launchSettings.json +++ b/Tryouts/Prototypes/Shell/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Shell": { "commandName": "Project", - "commandLineArgs": "--title=Jane --width=1800 --height=800 --url=http://www.google.com --icon=/images/branding/googleg/1x/googleg_standard_color_128dp.png" + "commandLineArgs": "--title Jane --width 1800 --height 800 --url http://www.google.com --icon /images/branding/googleg/1x/googleg_standard_color_128dp.png" } } } \ No newline at end of file diff --git a/Tryouts/Prototypes/Shell/Shell.csproj b/Tryouts/Prototypes/Shell/Shell.csproj index 101a349db..2f07c1144 100644 --- a/Tryouts/Prototypes/Shell/Shell.csproj +++ b/Tryouts/Prototypes/Shell/Shell.csproj @@ -9,9 +9,8 @@ - - + @@ -20,19 +19,20 @@ - + MSBuild:Compile - - MSBuild:Compile - Always + + + + diff --git a/Tryouts/Prototypes/Shell/Utilities/CommandLineParser.cs b/Tryouts/Prototypes/Shell/Utilities/CommandLineParser.cs new file mode 100644 index 000000000..845ae5bab --- /dev/null +++ b/Tryouts/Prototypes/Shell/Utilities/CommandLineParser.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + +namespace Shell.Utilities; + +/// +/// Simple helper class that parses strongly typed objects from command line arguments. +/// +/// +/// No checks or conversions are performed at the moment, the properties of the +/// deserialized class must be compatible with System.CommandLine options. +/// Nullable primitive types and strings are supported. +/// To add a description for a property, annotate with and use the property. +/// +public static class CommandLineParser +{ + public static T Parse(string[] args) where T : new() + { + return (T)_parsers.GetOrAdd(typeof(T), CreateParser)(args); + } + + public static bool TryParse(string[] args, out T value) where T : new() + { + try + { + value = Parse(args); + + return true; + } + catch + { + value = default!; + + return false; + } + } + + private static readonly ConcurrentDictionary> _parsers = new(); + + private static Func CreateParser(Type type) + { + var rootCommand = new RootCommand(); + var optionToProperty = new Dictionary(); + + foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.DeclaringType != typeof(object))) + { + var displayAttribute = property.GetCustomAttribute(); + var optionName = displayAttribute?.Name ?? ToCamelCase(property.Name); + + var option = (Option)Activator.CreateInstance( + typeof(Option<>).MakeGenericType(property.PropertyType), + "--" + optionName, + displayAttribute?.Description ?? property.Name)!; + + rootCommand.Add(option); + optionToProperty.Add(option, property); + } + + var parser = new Parser(rootCommand); + + // TODO: Build and compile a lambda instead for better performance + return args => + { + var parseResult = parser.Parse(args); + var result = Activator.CreateInstance(type)!; + + foreach (var mapping in optionToProperty) + { + var value = parseResult.GetValueForOption(mapping.Key); + + if (value != null) + { + mapping.Value.SetValue(result, value); + } + } + + return result; + }; + } + + private static string? ToCamelCase(string? value) => + value switch + { + null => null, + "" => "", + _ => char.ToLower(value[0]) + value[1..] + }; +} diff --git a/Tryouts/Prototypes/Shell/WebContent.xaml b/Tryouts/Prototypes/Shell/WebContent.xaml deleted file mode 100644 index d7743743f..000000000 --- a/Tryouts/Prototypes/Shell/WebContent.xaml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - diff --git a/Tryouts/Prototypes/Shell/WebContent.xaml.cs b/Tryouts/Prototypes/Shell/WebContent.xaml.cs deleted file mode 100644 index 54206c83c..000000000 --- a/Tryouts/Prototypes/Shell/WebContent.xaml.cs +++ /dev/null @@ -1,39 +0,0 @@ -// /* -// * Morgan Stanley makes this available to you under the Apache License, -// * Version 2.0 (the "License"). You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0. -// * -// * See the NOTICE file distributed with this work for additional information -// * regarding copyright ownership. Unless required by applicable law or agreed -// * to in writing, software distributed under the License is distributed on an -// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -// * or implied. See the License for the specific language governing permissions -// * and limitations under the License. -// */ - -using Shell.ImageSource; -using System; -using System.Windows; - -namespace Shell -{ - /// - /// Interaction logic for WebContent.xaml - /// - public partial class WebContent : Window - { - public WebContent(WebContentOptions options, ImageSourceProvider iconProvider) - { - InitializeComponent(); - - webView.Source = options.Uri; - this.Title = options.Title; - if (options.IconUri != null) - { - this.Icon = iconProvider.GetImageSource(options.IconUri, options.Uri); - } - - } - } -} diff --git a/Tryouts/Prototypes/Shell/WebContentOptions.cs b/Tryouts/Prototypes/Shell/WebContentOptions.cs deleted file mode 100644 index 20cf1bef2..000000000 --- a/Tryouts/Prototypes/Shell/WebContentOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shell -{ - public sealed class WebContentOptions - { - public string Title = string.Empty; - public Uri Uri { get; set; } = new Uri("about:blank"); - public Uri? IconUri { get; set; } = null; - } -} diff --git a/Tryouts/Prototypes/Shell/MainWebWindow.xaml b/Tryouts/Prototypes/Shell/WebWindow.xaml similarity index 87% rename from Tryouts/Prototypes/Shell/MainWebWindow.xaml rename to Tryouts/Prototypes/Shell/WebWindow.xaml index 35408f190..c95a93ddf 100644 --- a/Tryouts/Prototypes/Shell/MainWebWindow.xaml +++ b/Tryouts/Prototypes/Shell/WebWindow.xaml @@ -1,4 +1,4 @@ - diff --git a/Tryouts/Prototypes/Shell/WebWindow.xaml.cs b/Tryouts/Prototypes/Shell/WebWindow.xaml.cs new file mode 100644 index 000000000..f4bb72357 --- /dev/null +++ b/Tryouts/Prototypes/Shell/WebWindow.xaml.cs @@ -0,0 +1,68 @@ +using System; +using System.Windows; +using Microsoft.Web.WebView2.Core; +using Shell.ImageSource; + +namespace Shell +{ + /// + /// Interaction logic for WebWindow.xaml + /// + public partial class WebWindow : Window + { + public WebWindow(WebWindowOptions webWindowOptions) + { + InitializeComponent(); + + Title = webWindowOptions.Title ?? WebWindowOptions.DefaultTitle; + Width = webWindowOptions.Width ?? WebWindowOptions.DefaultWidth; + Height = webWindowOptions.Height ?? WebWindowOptions.DefaultHeight; + webView.Source = new Uri(webWindowOptions.Url ?? WebWindowOptions.DefaultUrl); + TrySetIconUrl(webWindowOptions); + + webView.CoreWebView2InitializationCompleted += (sender, args) => + { + if (args.IsSuccess) + { + webView.CoreWebView2.NewWindowRequested += (sender, args) => OnNewWindowRequested(args); + } + }; + } + + private readonly ImageSourceProvider _iconProvider = new ImageSourceProvider(new EnvironmentImageSourcePolicy()); + + private void TrySetIconUrl(WebWindowOptions webWindowOptions) + { + if (webWindowOptions.IconUrl == null) + return; + + // TODO: What's the default URL if the app is running from a manifest? We should probably not allow relative urls in that case. + var appUrl = new Uri(webWindowOptions.Url ?? WebWindowOptions.DefaultUrl); + var iconUrl = webWindowOptions.IconUrl != null ? new Uri(webWindowOptions.IconUrl, UriKind.RelativeOrAbsolute) : null; + + if (iconUrl != null) + { + Icon = _iconProvider.GetImageSource(iconUrl, appUrl); + } + } + + private async void OnNewWindowRequested(CoreWebView2NewWindowRequestedEventArgs e) + { + e.Handled = true; + var deferral = e.GetDeferral(); + var windowOptions = new WebWindowOptions { Url = e.Uri }; + + if (e.WindowFeatures.HasSize) + { + windowOptions.Width = e.WindowFeatures.Width; + windowOptions.Height = e.WindowFeatures.Height; + } + + var window = new WebWindow(windowOptions); + window.Show(); + await window.webView.EnsureCoreWebView2Async(); + e.NewWindow = window.webView.CoreWebView2; + deferral.Complete(); + } + } +} \ No newline at end of file diff --git a/Tryouts/Prototypes/Shell/WebWindowOptions.cs b/Tryouts/Prototypes/Shell/WebWindowOptions.cs new file mode 100644 index 000000000..88d1e5530 --- /dev/null +++ b/Tryouts/Prototypes/Shell/WebWindowOptions.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace Shell +{ + public sealed class WebWindowOptions + { + [Display(Description = "Set the height of the window. Default: 450")] + public double? Height { get; set; } + + [Display(Description = $"Set the title of the window. Default: {DefaultTitle}")] + public string? Title { get; set; } + + [Display(Description = $"Set the url for the web view. Default: {DefaultUrl}")] + public string? Url { get; set; } + + [Display(Name = "icon", Description = $"Set the icon url for the window.")] + public string? IconUrl { get; set; } + + [Display(Description = $"Set the width of the window. Default: 800")] + public double? Width { get; set; } + + public const double DefaultHeight = 450; + public const string DefaultTitle = "Compose Web Container"; + public const string DefaultUrl = "about:blank"; + public const double DefaultWidth = 800; + } +}