diff --git a/common/Contracts/IDevHomeActionSetRender.cs b/common/Contracts/IDevHomeActionSetRender.cs new file mode 100644 index 0000000000..ee7424123c --- /dev/null +++ b/common/Contracts/IDevHomeActionSetRender.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; + +namespace DevHome.Common.Contracts; + +/// +/// Represents a renderer for an adaptive card action set that Dev Home can use to invoke actions from within +/// the action set. This is useful for invoking an adaptive card action from an abitraty button within Dev Home's UI. +/// +public interface IDevHomeActionSetRender : IAdaptiveElementRenderer +{ + /// + /// Attempts to validate the user inputs and initiate the action. + /// + /// The id of the button within the adaptive card Json template + /// The user input from the adaptive card session + /// True if the users inputs were validated and false otherwise + public bool TryValidateAndInitiateAction(string buttonId, AdaptiveInputs userInputs); + + /// + /// Initiates the action without validating the user inputs. + /// + /// The id of the button within the adaptive card Json template + public void InitiateAction(string buttonId); +} diff --git a/common/DevHomeAdaptiveCards/CardInterfaces/IDevHomeSettingsCard.cs b/common/DevHomeAdaptiveCards/CardInterfaces/IDevHomeSettingsCard.cs new file mode 100644 index 0000000000..2e523c7517 --- /dev/null +++ b/common/DevHomeAdaptiveCards/CardInterfaces/IDevHomeSettingsCard.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.ObjectModel.WinUI3; + +namespace DevHome.Common.DevHomeAdaptiveCards.CardInterfaces; + +/// +/// Represents a Dev Home settings card that can be rendered through an adaptive card. +/// +public interface IDevHomeSettingsCard : IAdaptiveCardElement +{ + /// + /// gets or sets the subtitle of the card. The description naming is used because that is what + /// is used in the Windows Community Toolkit settings cards. This is displayed below the header. + /// + public string Description { get; set; } + + /// + /// gets or sets the header of the card. This is the bolded text at the top of the card. + /// + public string Header { get; set; } + + /// + /// gets or sets the icon base64 string that represents an image. This is the icon that is + /// displayed to the left of the header. + /// + public string HeaderIcon { get; set; } + + // An element that is not expected to submit the adaptive card + public IDevHomeSettingsCardNonSubmitAction? NonSubmitActionElement { get; set; } + + // An element that is expected to submit the adaptive card + public IAdaptiveActionElement? SubmitActionElement { get; set; } +} diff --git a/common/DevHomeAdaptiveCards/CardInterfaces/IDevHomeSettingsCardNonSubmitAction.cs b/common/DevHomeAdaptiveCards/CardInterfaces/IDevHomeSettingsCardNonSubmitAction.cs new file mode 100644 index 0000000000..327f07b0a9 --- /dev/null +++ b/common/DevHomeAdaptiveCards/CardInterfaces/IDevHomeSettingsCardNonSubmitAction.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; +using CommunityToolkit.Mvvm.Input; + +namespace DevHome.Common.DevHomeAdaptiveCards.CardInterfaces; + +/// +/// Represents an action that can be invoked from a Dev Home settings card that does not submit the card. E.g launches a dialog. +/// +public interface IDevHomeSettingsCardNonSubmitAction : IAdaptiveCardElement +{ + /// + /// Gets the text that is displayed on the action element. E.g button text. + /// + public string ActionText { get; } + + /// + /// Invokes the action through a relay command + /// + /// The UI object that the command originated from + [RelayCommand] + public Task InvokeActionAsync(object sender); +} diff --git a/common/DevHomeAdaptiveCards/CardModels/DevHomeContentDialogContent.cs b/common/DevHomeAdaptiveCards/CardModels/DevHomeContentDialogContent.cs new file mode 100644 index 0000000000..c15373debd --- /dev/null +++ b/common/DevHomeAdaptiveCards/CardModels/DevHomeContentDialogContent.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.CardModels; + +/// +/// Represents a content dialog that can be rendered through an adaptive card based on the +/// Json template. +/// +public class DevHomeContentDialogContent : IAdaptiveCardElement +{ + // Specific properties for DevHomeContentDialogContent + public string Title { get; set; } = string.Empty; + + // This is the adaptive card that will be shown within + // a content dialogs body. + public JsonObject? ContentDialogInternalAdaptiveCardJson { get; set; } + + public string PrimaryButtonText { get; set; } = string.Empty; + + public string SecondaryButtonText { get; set; } = string.Empty; + + public static string AdaptiveElementType => "DevHome.ContentDialogContent"; + + // Properties for IAdaptiveCardElement + public string ElementTypeString { get; set; } = AdaptiveElementType; + + public JsonObject AdditionalProperties { get; set; } = new(); + + public ElementType ElementType { get; set; } = ElementType.Custom; + + public IAdaptiveCardElement? FallbackContent { get; set; } + + public FallbackType FallbackType { get; set; } + + public HeightType Height { get; set; } = HeightType.Stretch; + + public string Id { get; set; } = string.Empty; + + public bool IsVisible { get; set; } = true; + + public IList Requirements { get; set; } = new List(); + + public bool Separator { get; set; } + + public Spacing Spacing { get; set; } = Spacing.Default; + + public JsonObject? ToJson() => []; +} diff --git a/common/DevHomeAdaptiveCards/CardModels/DevHomeLaunchContentDialogButton.cs b/common/DevHomeAdaptiveCards/CardModels/DevHomeLaunchContentDialogButton.cs new file mode 100644 index 0000000000..355c3bb32d --- /dev/null +++ b/common/DevHomeAdaptiveCards/CardModels/DevHomeLaunchContentDialogButton.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.DevHomeAdaptiveCards.CardInterfaces; +using DevHome.Common.Views.AdaptiveCardViews; +using Microsoft.UI.Xaml.Controls; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.CardModels; + +public partial class DevHomeLaunchContentDialogButton : IDevHomeSettingsCardNonSubmitAction +{ + // Specific properties for DevHomeLaunchContentDialogButton + public string ActionText { get; set; } = string.Empty; + + public IAdaptiveCardElement? DialogContent { get; set; } + + public static string AdaptiveElementType => "DevHome.LaunchContentDialogButton"; + + // Properties for IAdaptiveActionElement + public string ElementTypeString { get; set; } = AdaptiveElementType; + + public JsonObject AdditionalProperties { get; set; } = new(); + + public ElementType ElementType { get; set; } = ElementType.Custom; + + public IAdaptiveCardElement? FallbackContent { get; set; } + + public FallbackType FallbackType { get; set; } + + public HeightType Height { get; set; } = HeightType.Stretch; + + public string Id { get; set; } = string.Empty; + + public bool IsVisible { get; set; } = true; + + public IList Requirements { get; set; } = new List(); + + public bool Separator { get; set; } + + public Spacing Spacing { get; set; } = Spacing.Default; + + public JsonObject? ToJson() => []; + + [RelayCommand] + public async Task InvokeActionAsync(object sender) + { + var senderObj = sender as Button; + if (DialogContent is not DevHomeContentDialogContent dialogContent || senderObj == null) + { + return; + } + + var dialog = new ContentDialogWithNonInteractiveContent(dialogContent); + + dialog.XamlRoot = senderObj.XamlRoot; + + await dialog.ShowAsync(); + } +} diff --git a/common/DevHomeAdaptiveCards/CardModels/DevHomeSettingsCard.cs b/common/DevHomeAdaptiveCards/CardModels/DevHomeSettingsCard.cs new file mode 100644 index 0000000000..d566e158b8 --- /dev/null +++ b/common/DevHomeAdaptiveCards/CardModels/DevHomeSettingsCard.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using AdaptiveCards.ObjectModel.WinUI3; +using DevHome.Common.DevHomeAdaptiveCards.CardInterfaces; +using Microsoft.UI.Xaml.Controls; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.CardModels; + +/// +/// Represents a settings card that can be rendered through an adaptive card based on the ElementTypeString. +/// +public class DevHomeSettingsCard : IDevHomeSettingsCard +{ + // Specific properties for DevHomeSettingsCard + // These properties relate to the Windows Community Toolkit's SettingsCard control. + // We'll allow extensions to provide the data for the SettingsCard control from an Adaptive Card. + // Then we'll render the actual SettingsCard control in the DevHome app. + /// + public string Description { get; set; } = string.Empty; + + /// + public string Header { get; set; } = string.Empty; + + /// + public string HeaderIcon { get; set; } = string.Empty; + + [JsonIgnore] + public ImageIcon? HeaderIconImage { get; set; } + + /// + /// Gets or sets the element that does not submit the card. + public IDevHomeSettingsCardNonSubmitAction? NonSubmitActionElement { get; set; } + + public IAdaptiveActionElement? SubmitActionElement { get; set; } + + public static string AdaptiveElementType => "DevHome.SettingsCard"; + + // Properties for IAdaptiveCardElement + public string ElementTypeString { get; set; } = AdaptiveElementType; + + public JsonObject AdditionalProperties { get; set; } = new(); + + public ElementType ElementType { get; set; } = ElementType.Custom; + + public IAdaptiveCardElement? FallbackContent { get; set; } + + public FallbackType FallbackType { get; set; } + + public HeightType Height { get; set; } = HeightType.Stretch; + + public string Id { get; set; } = string.Empty; + + public bool IsVisible { get; set; } = true; + + public IList Requirements { get; set; } = new List(); + + public bool Separator { get; set; } + + public Spacing Spacing { get; set; } = Spacing.Default; + + public JsonObject? ToJson() => []; +} diff --git a/common/DevHomeAdaptiveCards/CardModels/DevHomeSettingsCardChoiceSet.cs b/common/DevHomeAdaptiveCards/CardModels/DevHomeSettingsCardChoiceSet.cs new file mode 100644 index 0000000000..6dc43613f9 --- /dev/null +++ b/common/DevHomeAdaptiveCards/CardModels/DevHomeSettingsCardChoiceSet.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.CardModels; + +public class DevHomeSettingsCardChoiceSet : IAdaptiveCardElement, IAdaptiveInputElement +{ + // Specific properties for DevHomeAdaptiveSettingsCardItemsViewChoiceSet + public IList SettingsCards { get; set; } = []; + + public const int UnselectedIndex = -1; + + public bool IsSelectionDisabled { get; set; } + + public static string AdaptiveElementType => "DevHome.SettingsCardChoiceSet"; + + public bool IsMultiSelect { get; set; } + + public int SelectedValue { get; set; } = UnselectedIndex; + + // Specific properties for IAdaptiveInputElement + public ElementType ElementType { get; set; } = ElementType.Custom; + + public string ElementTypeString { get; set; } = AdaptiveElementType; + + // Specific properties for IAdaptiveCardElement + public string ErrorMessage { get; set; } = string.Empty; + + public bool IsRequired { get; set; } + + public string Label { get; set; } = string.Empty; + + public JsonObject AdditionalProperties { get; set; } = new(); + + public IAdaptiveCardElement? FallbackContent { get; set; } + + public FallbackType FallbackType { get; set; } + + public HeightType Height { get; set; } = HeightType.Stretch; + + public string Id { get; set; } = string.Empty; + + public bool IsVisible { get; set; } = true; + + public IList Requirements { get; set; } = new List(); + + public bool Separator { get; set; } + + public Spacing Spacing { get; set; } = Spacing.Default; + + public JsonObject? ToJson() => []; +} diff --git a/common/DevHomeAdaptiveCards/InputValues/ItemsViewInputValue.cs b/common/DevHomeAdaptiveCards/InputValues/ItemsViewInputValue.cs new file mode 100644 index 0000000000..7bb58db5f7 --- /dev/null +++ b/common/DevHomeAdaptiveCards/InputValues/ItemsViewInputValue.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Globalization; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using DevHome.Common.Environments.Helpers; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Common.DevHomeAdaptiveCards.InputValues; + +/// +/// Represents the current value of an ItemsView input element. +/// +public class ItemsViewInputValue : IAdaptiveInputValue +{ + private readonly ItemsView _itemsView; + + public ItemsViewInputValue(IAdaptiveInputElement input, ItemsView itemsView) + { + InputElement = input; + _itemsView = itemsView; + } + + /// + /// Gets the current value of the input element. This is the index of the current item. + /// + public string CurrentValue => _itemsView.CurrentItemIndex.ToString(CultureInfo.InvariantCulture); + + public UIElement? ErrorMessage { get; set; } + + public IAdaptiveInputElement InputElement { get; set; } + + public void SetFocus() + { + _itemsView.Focus(FocusState.Keyboard); + } + + // If the items view selection mode isn't None, then the user must select an item. + public bool Validate() + { + if (_itemsView.SelectionMode == ItemsViewSelectionMode.None) + { + return true; + } + + if ((_itemsView.SelectedItem == null) || (_itemsView.CurrentItemIndex < 0)) + { + var errorMessage = StringResourceHelper.GetResource("ItemsViewNonSelectedItemError"); + ErrorMessage = new TextBlock(); + ErrorMessage.SetValue(TextBlock.TextProperty, errorMessage); + return false; + } + + return true; + } +} diff --git a/common/DevHomeAdaptiveCards/Parsers/DevHomeContentDialogContentParser.cs b/common/DevHomeAdaptiveCards/Parsers/DevHomeContentDialogContentParser.cs new file mode 100644 index 0000000000..071150342c --- /dev/null +++ b/common/DevHomeAdaptiveCards/Parsers/DevHomeContentDialogContentParser.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.Environments.Helpers; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.Parsers; + +/// +/// Represents a parser for a Dev Home content dialog card that can be rendered through an adaptive card. +/// This parser will be used if the element type is "DevHome.ContentDialogContent". +/// +/// +/// the JsonObject is a Windows.Data.Json.JsonObject, which has methods that can throw an exception if the type is not correct. +/// +public class DevHomeContentDialogContentParser : IAdaptiveElementParser +{ + public IAdaptiveCardElement FromJson(JsonObject inputJson, AdaptiveElementParserRegistration elementParsers, AdaptiveActionParserRegistration actionParsers, IList warnings) + { + var dialog = new DevHomeContentDialogContent(); + bool isCorrectType; + + if (inputJson.TryGetValue("devHomeContentDialogTitle", out var devHomeContentDialogTitle)) + { + isCorrectType = devHomeContentDialogTitle.ValueType == JsonValueType.String; + dialog.Title = isCorrectType ? devHomeContentDialogTitle.GetString() : StringResourceHelper.GetResource("DevHomeContentDialogDefaultTitle"); + } + + if (inputJson.TryGetValue("devHomeContentDialogBodyAdaptiveCard", out var contentDialogInternalAdaptiveCardJson)) + { + isCorrectType = contentDialogInternalAdaptiveCardJson.ValueType == JsonValueType.Object; + dialog.ContentDialogInternalAdaptiveCardJson = isCorrectType ? contentDialogInternalAdaptiveCardJson.GetObject() : new JsonObject(); + } + + if (inputJson.TryGetValue("devHomeContentDialogPrimaryButtonText", out var devHomeContentDialogPrimaryButtonText)) + { + isCorrectType = devHomeContentDialogPrimaryButtonText.ValueType == JsonValueType.String; + dialog.PrimaryButtonText = isCorrectType ? devHomeContentDialogPrimaryButtonText.GetString() : StringResourceHelper.GetResource("DevHomeContentDialogDefaultPrimaryButtonText"); + } + + if (inputJson.TryGetValue("devHomeContentDialogSecondaryButtonText", out var devHomeContentDialogSecondaryButtonText)) + { + isCorrectType = devHomeContentDialogSecondaryButtonText.ValueType == JsonValueType.String; + dialog.SecondaryButtonText = isCorrectType ? devHomeContentDialogSecondaryButtonText.GetString() : StringResourceHelper.GetResource("DevHomeContentDialogDefaultSecondaryButtonText"); + } + + return dialog; + } +} diff --git a/common/DevHomeAdaptiveCards/Parsers/DevHomeLaunchContentDialogButtonParser.cs b/common/DevHomeAdaptiveCards/Parsers/DevHomeLaunchContentDialogButtonParser.cs new file mode 100644 index 0000000000..a8ad0357f1 --- /dev/null +++ b/common/DevHomeAdaptiveCards/Parsers/DevHomeLaunchContentDialogButtonParser.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.Environments.Helpers; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.Parsers; + +/// +/// Represents a parser for a Dev Home launch content dialog button that can be rendered through an adaptive card. +/// This parser will be used if the element type is "DevHome.LaunchContentDialogButton". +/// +/// +/// the JsonObject is a Windows.Data.Json.JsonObject, which has methods that can throw an exception if the type is not correct. +/// +public class DevHomeLaunchContentDialogButtonParser : IAdaptiveElementParser +{ + public IAdaptiveCardElement FromJson(JsonObject inputJson, AdaptiveElementParserRegistration elementParsers, AdaptiveActionParserRegistration actionParsers, IList warnings) + { + var action = new DevHomeLaunchContentDialogButton(); + bool isCorrectType; + + if (inputJson.TryGetValue("devHomeActionText", out var devHomeActionText)) + { + isCorrectType = devHomeActionText.ValueType == JsonValueType.String; + action.ActionText = isCorrectType ? devHomeActionText.GetString() : StringResourceHelper.GetResource("DevHomeActionDefaultText"); + } + + // Parse the content dialog element and place its content into our content dialog button property. + if (inputJson.TryGetValue("devHomeContentDialogContent", out var devHomeContentDialogContent)) + { + isCorrectType = devHomeContentDialogContent.ValueType == JsonValueType.Object; + var contentDialogJson = isCorrectType ? devHomeContentDialogContent.GetObject() : new JsonObject(); + var contentDialogParser = elementParsers.Get(DevHomeContentDialogContent.AdaptiveElementType); + action.DialogContent = contentDialogParser?.FromJson(contentDialogJson, elementParsers, actionParsers, warnings); + } + + return action; + } +} diff --git a/common/DevHomeAdaptiveCards/Parsers/DevHomeSettingsCardChoiceSetParser.cs b/common/DevHomeAdaptiveCards/Parsers/DevHomeSettingsCardChoiceSetParser.cs new file mode 100644 index 0000000000..eabe0c89a2 --- /dev/null +++ b/common/DevHomeAdaptiveCards/Parsers/DevHomeSettingsCardChoiceSetParser.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.Environments.Helpers; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.Parsers; + +/// +/// Represents a parser for a Dev Home settings card choice set that can be rendered through an adaptive card. +/// This parser will be used if the element type is "DevHome.SettingsCardChoiceSet". +/// +/// +/// the JsonObject is a Windows.Data.Json.JsonObject, which has methods that can throw an exception if the type is not correct. +/// +public class DevHomeSettingsCardChoiceSetParser : IAdaptiveElementParser +{ + public IAdaptiveCardElement FromJson(JsonObject inputJson, AdaptiveElementParserRegistration elementParsers, AdaptiveActionParserRegistration actionParsers, IList warnings) + { + var adaptiveSettingsCardChoiceSet = new DevHomeSettingsCardChoiceSet(); + bool isCorrectType; + + if (inputJson.TryGetValue("id", out var id)) + { + isCorrectType = id.ValueType == JsonValueType.String; + adaptiveSettingsCardChoiceSet.Id = isCorrectType ? id.GetString() : string.Empty; + } + + if (inputJson.TryGetValue("label", out var label)) + { + isCorrectType = label.ValueType == JsonValueType.String; + adaptiveSettingsCardChoiceSet.Label = isCorrectType ? label.GetString() : StringResourceHelper.GetResource("SettingsCardChoiceSetDefaultLabel"); + } + + if (inputJson.TryGetValue("isRequired", out var isRequired)) + { + isCorrectType = isRequired.ValueType == JsonValueType.Boolean; + adaptiveSettingsCardChoiceSet.IsRequired = isCorrectType ? isRequired.GetBoolean() : false; + } + + if (inputJson.TryGetValue("selectedValue", out var selectedValue)) + { + isCorrectType = selectedValue.ValueType == JsonValueType.Number; + adaptiveSettingsCardChoiceSet.SelectedValue = isCorrectType ? (int)selectedValue.GetNumber() : DevHomeSettingsCardChoiceSet.UnselectedIndex; + } + + if (inputJson.TryGetValue("isMultiSelect", out var isMultiSelect)) + { + isCorrectType = isMultiSelect.ValueType == JsonValueType.Boolean; + adaptiveSettingsCardChoiceSet.IsMultiSelect = isCorrectType ? isMultiSelect.GetBoolean() : false; + } + + // If IsSelectionDisabled is true, then IsMultiSelect should be false and no item should be selected. + if (inputJson.TryGetValue("devHomeSettingsCardIsSelectionDisabled", out var devHomeSettingsCardIsSelectionDisabled)) + { + isCorrectType = devHomeSettingsCardIsSelectionDisabled.ValueType == JsonValueType.Boolean; + adaptiveSettingsCardChoiceSet.IsSelectionDisabled = isCorrectType ? devHomeSettingsCardIsSelectionDisabled.GetBoolean() : false; + + if (adaptiveSettingsCardChoiceSet.IsSelectionDisabled) + { + adaptiveSettingsCardChoiceSet.SelectedValue = DevHomeSettingsCardChoiceSet.UnselectedIndex; + adaptiveSettingsCardChoiceSet.IsMultiSelect = false; + } + } + + // Parse the settings cards + if (inputJson.TryGetValue("devHomeSettingsCards", out var devHomeSettingsCards)) + { + isCorrectType = devHomeSettingsCards.ValueType == JsonValueType.Array; + var elementJson = isCorrectType ? devHomeSettingsCards.GetArray() : []; + adaptiveSettingsCardChoiceSet.SettingsCards = GetSettingsCards(elementJson, elementParsers, actionParsers, warnings); + } + + return adaptiveSettingsCardChoiceSet; + } + + private List GetSettingsCards(JsonArray elementJson, AdaptiveElementParserRegistration elementParsers, AdaptiveActionParserRegistration actionParsers, IList warnings) + { + List settingsCards = new(); + var parser = elementParsers.Get(DevHomeSettingsCard.AdaptiveElementType); + foreach (var element in elementJson) + { + if (element.ValueType != JsonValueType.Object) + { + continue; + } + + if (parser.FromJson(element.GetObject(), elementParsers, actionParsers, warnings) is DevHomeSettingsCard card) + { + settingsCards.Add(card); + } + } + + return settingsCards; + } +} diff --git a/common/DevHomeAdaptiveCards/Parsers/DevHomeSettingsCardParser.cs b/common/DevHomeAdaptiveCards/Parsers/DevHomeSettingsCardParser.cs new file mode 100644 index 0000000000..c4a382997a --- /dev/null +++ b/common/DevHomeAdaptiveCards/Parsers/DevHomeSettingsCardParser.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using DevHome.Common.DevHomeAdaptiveCards.CardInterfaces; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.Environments.Helpers; +using Windows.Data.Json; + +namespace DevHome.Common.DevHomeAdaptiveCards.Parsers; + +/// +/// Represents a parser for a Dev Home settings card that can be rendered through an adaptive card. +/// This parser will be used if the element type is "DevHome.SettingsCard". +/// +/// +/// the JsonObject is a Windows.Data.Json.JsonObject, which has methods that can throw an exception if the type is not correct. +/// +public class DevHomeSettingsCardParser : IAdaptiveElementParser +{ + public IAdaptiveCardElement FromJson(JsonObject inputJson, AdaptiveElementParserRegistration elementParsers, AdaptiveActionParserRegistration actionParsers, IList warnings) + { + var adaptiveSettingsCard = new DevHomeSettingsCard(); + bool isCorrectType; + + if (inputJson.TryGetValue("id", out var id)) + { + isCorrectType = id.ValueType == JsonValueType.String; + adaptiveSettingsCard.Id = isCorrectType ? id.GetString() : string.Empty; + } + + if (inputJson.TryGetValue("devHomeSettingsCardDescription", out var devHomeSettingsCardDescription)) + { + isCorrectType = devHomeSettingsCardDescription.ValueType == JsonValueType.String; + adaptiveSettingsCard.Description = isCorrectType ? devHomeSettingsCardDescription.GetString() : StringResourceHelper.GetResource("SettingsCardDescriptionError"); + } + + if (inputJson.TryGetValue("devHomeSettingsCardHeader", out var devHomeSettingsCardHeader)) + { + isCorrectType = devHomeSettingsCardHeader.ValueType == JsonValueType.String; + adaptiveSettingsCard.Header = isCorrectType ? devHomeSettingsCardHeader.GetString() : StringResourceHelper.GetResource("SettingsCardHeaderError"); + } + + if (inputJson.TryGetValue("devHomeSettingsCardHeaderIcon", out var devHomeSettingsCardHeaderIcon)) + { + isCorrectType = devHomeSettingsCardHeaderIcon.ValueType == JsonValueType.String; + adaptiveSettingsCard.HeaderIcon = isCorrectType ? devHomeSettingsCardHeaderIcon.GetString() : string.Empty; + } + + if (inputJson.TryGetValue("devHomeSettingsCardActionElement", out var devHomeSettingsCardActionElement)) + { + isCorrectType = devHomeSettingsCardActionElement.ValueType == JsonValueType.Object; + var elementJson = isCorrectType ? devHomeSettingsCardActionElement.GetObject() : new JsonObject(); + var elementType = elementJson.GetNamedString("type", string.Empty); + + // More action types can be added in the future by adding more cases here. Note this parser + // will only parse elements that don't submit the adaptive card. If we need to support elements + // that submit the adaptive card, we'll need to add a new parser action parser and add the IAdaptiveActionElement + // to the DevHomeSettingsCard's ActionElement property. + if (string.Equals(elementType, DevHomeLaunchContentDialogButton.AdaptiveElementType, StringComparison.OrdinalIgnoreCase)) + { + adaptiveSettingsCard.NonSubmitActionElement = CreateLaunchContentDialogButton(elementJson, elementParsers, actionParsers, warnings); + } + else + { + // If the action isn't one of our custom actions, we'll check if there is a built-in action parser that can parse it. + var elementParser = elementParsers.Get(elementType); + if (elementParser != null) + { + adaptiveSettingsCard.NonSubmitActionElement = elementParser.FromJson(elementJson, elementParsers, actionParsers, warnings) as IDevHomeSettingsCardNonSubmitAction; + } + } + } + + return adaptiveSettingsCard; + } + + private DevHomeLaunchContentDialogButton? CreateLaunchContentDialogButton(JsonObject inputJson, AdaptiveElementParserRegistration elementParsers, AdaptiveActionParserRegistration actionParsers, IList warnings) + { + var parser = elementParsers.Get(DevHomeLaunchContentDialogButton.AdaptiveElementType); + return parser?.FromJson(inputJson, elementParsers, actionParsers, warnings) as DevHomeLaunchContentDialogButton; + } +} diff --git a/common/Helpers/AdaptiveCardHelpers.cs b/common/Helpers/AdaptiveCardHelpers.cs new file mode 100644 index 0000000000..a662b5e7f9 --- /dev/null +++ b/common/Helpers/AdaptiveCardHelpers.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using DevHome.Common.Helpers; +using DevHome.Common.Models; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Serilog; + +namespace DevHome.Common.Helpers; + +public static class AdaptiveCardHelpers +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(AdaptiveCardHelpers)); + + // convert base64 string to image that can be used in a imageIcon control + public static ImageIcon ConvertBase64StringToImageIcon(string base64String) + { + try + { + var bytes = Convert.FromBase64String(base64String); + using var ms = new MemoryStream(bytes); + var bitmapImage = new BitmapImage(); + bitmapImage.SetSource(ms.AsRandomAccessStream()); + var icon = new ImageIcon() { Source = bitmapImage }; + return icon; + } + catch (Exception ex) + { + _log.Error($"Failed to load image icon", ex); + return new ImageIcon(); + } + } +} diff --git a/common/Models/ExtensionAdaptiveCard.cs b/common/Models/ExtensionAdaptiveCard.cs index 30be992375..a814cb4860 100644 --- a/common/Models/ExtensionAdaptiveCard.cs +++ b/common/Models/ExtensionAdaptiveCard.cs @@ -12,6 +12,10 @@ namespace DevHome.Common.Models; public class ExtensionAdaptiveCard : IExtensionAdaptiveCard { + private readonly AdaptiveElementParserRegistration? _elementParserRegistration; + + private readonly AdaptiveActionParserRegistration? _actionParserRegistration; + public event EventHandler? UiUpdate; public string DataJson { get; private set; } @@ -20,11 +24,14 @@ public class ExtensionAdaptiveCard : IExtensionAdaptiveCard public string TemplateJson { get; private set; } - public ExtensionAdaptiveCard() + public ExtensionAdaptiveCard(AdaptiveElementParserRegistration? elementParserRegistration = null, AdaptiveActionParserRegistration? actionParserRegistration = null) { TemplateJson = new JsonObject().ToJsonString(); DataJson = new JsonObject().ToJsonString(); State = string.Empty; + + _elementParserRegistration = elementParserRegistration ?? new AdaptiveElementParserRegistration(); + _actionParserRegistration = actionParserRegistration ?? new AdaptiveActionParserRegistration(); } public ProviderOperationResult Update(string templateJson, string dataJson, string state) @@ -36,7 +43,7 @@ public ProviderOperationResult Update(string templateJson, string dataJson, stri // an empty string. var adaptiveCardString = template.Expand(Newtonsoft.Json.JsonConvert.DeserializeObject(dataJson ?? DataJson)); - var parseResult = AdaptiveCard.FromJsonString(adaptiveCardString); + var parseResult = AdaptiveCard.FromJsonString(adaptiveCardString, _elementParserRegistration, _actionParserRegistration); if (parseResult.AdaptiveCard is null) { diff --git a/common/Models/ExtensionAdaptiveCardSession.cs b/common/Models/ExtensionAdaptiveCardSession.cs new file mode 100644 index 0000000000..9494a1c99c --- /dev/null +++ b/common/Models/ExtensionAdaptiveCardSession.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DevHome.Common.Helpers; + +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; + +namespace DevHome.Common.Models; + +/// +/// Wrapper class for the IExtensionAdaptiveCardSession and IExtensionAdaptiveCardSession2 interfaces. +/// +public class ExtensionAdaptiveCardSession +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExtensionAdaptiveCardSession)); + + private readonly string _componentName = "ExtensionAdaptiveCardSession"; + + public IExtensionAdaptiveCardSession Session { get; private set; } + + public event TypedEventHandler? Stopped; + + public ExtensionAdaptiveCardSession(IExtensionAdaptiveCardSession cardSession) + { + Session = cardSession; + + if (Session is IExtensionAdaptiveCardSession2 cardSession2) + { + cardSession2.Stopped += OnSessionStopped; + } + } + + public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) + { + try + { + return Session.Initialize(extensionUI); + } + catch (Exception ex) + { + _log.Error(_componentName, $"Initialize failed due to exception", ex); + return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message); + } + } + + public void Dispose() + { + try + { + if (Session is IExtensionAdaptiveCardSession2 cardSession2) + { + cardSession2.Stopped -= OnSessionStopped; + } + + Session.Dispose(); + } + catch (Exception ex) + { + _log.Error(_componentName, $"Dispose failed due to exception", ex); + } + } + + public async Task OnAction(string action, string inputs) + { + try + { + return await Session.OnAction(action, inputs); + } + catch (Exception ex) + { + _log.Error(_componentName, $"OnAction failed due to exception", ex); + return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message); + } + } + + public void OnSessionStopped(IExtensionAdaptiveCardSession2 sender, ExtensionAdaptiveCardSessionStoppedEventArgs args) + { + Stopped?.Invoke(this, args); + } +} diff --git a/common/Renderers/DevHomeActionSet.cs b/common/Renderers/DevHomeActionSet.cs new file mode 100644 index 0000000000..84ab1c00fc --- /dev/null +++ b/common/Renderers/DevHomeActionSet.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using DevHome.Common.Contracts; +using Microsoft.UI.Xaml; + +namespace DevHome.Common.Renderers; + +/// +/// Represents whether the adaptive card button in an action set should be visible or hidden in Dev Home's UI. +/// Although adaptive cards can natively hide elements, this is used on the Dev Home side to hide the action set +/// in cases where a page in Dev Home wants its own button to invoke the action in the adaptive card. +/// +public enum TopLevelCardActionSetVisibility +{ + Visible, + Hidden, +} + +/// +/// Allows Dev Home to attach an action to a button in the Dev Home UI. +/// Dev Home can use this to invoke an action from an adaptive card when +/// a button within Dev Home is clicked. It allows Dev Home to link buttons from +/// within its own UI the to top level action set in the adaptive card. +/// +/// +/// It is expected that the adaptive card will have a top level action set with buttons that Dev Home can link to. +/// +public class DevHomeActionSet : IDevHomeActionSetRender +{ + /// + /// Gets the visibility of the action in Dev Home's UI. + /// + private readonly TopLevelCardActionSetVisibility _actionSetVisibility; + + /// + /// Gets the adaptive card object that will invoke an action within the action set. + /// + public AdaptiveActionInvoker? ActionButtonInvoker { get; private set; } + + public AdaptiveCard? OriginalAdaptiveCard { get; private set; } + + private Dictionary ActionButtonMap { get; } = new(); + + public DevHomeActionSet(TopLevelCardActionSetVisibility cardActionSetVisibility) + { + _actionSetVisibility = cardActionSetVisibility; + } + + public UIElement? Render(IAdaptiveCardElement element, AdaptiveRenderContext context, AdaptiveRenderArgs renderArgs) + { + ActionButtonMap.Clear(); + + if (element is AdaptiveActionSet actionSet) + { + foreach (var action in actionSet.Actions) + { + if (action is AdaptiveExecuteAction executeAction) + { + context.LinkSubmitActionToCard(executeAction, renderArgs); + } + else if (action is AdaptiveSubmitAction submitAction) + { + context.LinkSubmitActionToCard(submitAction, renderArgs); + } + + ActionButtonMap.TryAdd(action.Id, action); + } + + ActionButtonInvoker = context.ActionInvoker; + OriginalAdaptiveCard = renderArgs.ParentCard; + + if (_actionSetVisibility == TopLevelCardActionSetVisibility.Hidden) + { + // the page in Dev Home does not want to show the action set in the card. + // So we return null to prevent the adaptive card buttons from appearing. + // Invoking the button from Dev Home will then trigger the action in the adaptive card. + return null; + } + } + + var renderer = new AdaptiveActionSetRenderer(); + return renderer.Render(element, context, renderArgs); + } + + /// + /// Invokes an adaptive card action from anywhere within Dev Home, like a method in a view Model for example. + /// A boolean is returned with the validation result of the card. We still send the action event so the adaptive + /// cards UI updates with error information. + /// + /// + /// A boolean indicating whether validation for the card passed or failed. + /// + public bool TryValidateAndInitiateAction(string buttonId, AdaptiveInputs userInputs) + { + ActionButtonMap.TryGetValue(buttonId, out var actionElement); + + if ((actionElement == null) || (userInputs == null)) + { + return false; + } + + var result = userInputs.ValidateInputs(actionElement); + + ActionButtonInvoker?.SendActionEvent(actionElement); + return result; + } + + public void InitiateAction(string buttonId) + { + ActionButtonMap.TryGetValue(buttonId, out var actionElement); + + if (actionElement == null) + { + return; + } + + ActionButtonInvoker?.SendActionEvent(actionElement); + } + + public string GetActionTitle(string buttonId) + { + if (!ActionButtonMap.TryGetValue(buttonId, out var action)) + { + return string.Empty; + } + + return action.Title; + } +} diff --git a/common/Renderers/ItemsViewChoiceSet.cs b/common/Renderers/ItemsViewChoiceSet.cs new file mode 100644 index 0000000000..546543f1b0 --- /dev/null +++ b/common/Renderers/ItemsViewChoiceSet.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.InputValues; +using DevHome.Common.Helpers; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Common.Renderers; + +/// +/// Renders a known Dev Home choice set as an ItemsView. +/// +public class ItemsViewChoiceSet : IAdaptiveElementRenderer +{ + private readonly double _defaultSpacing = 5; + + private readonly string? _choiceSetItemsTemplateName; + + public ItemsViewChoiceSet(string itemsTemplateResourceName) + { + // set the template for the items view. + _choiceSetItemsTemplateName = itemsTemplateResourceName; + } + + // Default template for the ItemsView will be used + public ItemsViewChoiceSet() + { + } + + public UIElement Render(IAdaptiveCardElement element, AdaptiveRenderContext context, AdaptiveRenderArgs renderArgs) + { + // As we add more types of choice sets, we can add more cases here. + if (element is DevHomeSettingsCardChoiceSet settingsCardChoiceSet) + { + return GetItemsViewElement(settingsCardChoiceSet, context, renderArgs); + } + + // Use default render for all other cases. + var renderer = new AdaptiveChoiceSetInputRenderer(); + return renderer.Render(element, context, renderArgs); + } + + private ItemsView GetItemsViewElement(DevHomeSettingsCardChoiceSet settingsCardChoiceSet, AdaptiveRenderContext context, AdaptiveRenderArgs renderArgs) + { + // Set default spacing for the ItemsView. + var choiceSetItemsView = new ItemsView(); + var defaultLayout = new StackLayout(); + defaultLayout.Spacing = _defaultSpacing; + choiceSetItemsView.Layout = defaultLayout; + + // If there is a template for the items view, set it. + if (!string.IsNullOrEmpty(_choiceSetItemsTemplateName)) + { + choiceSetItemsView.ItemTemplate = Application.Current.Resources[_choiceSetItemsTemplateName] as DataTemplate; + } + + // Check if the choice set is multi-select, and if it is make sure the ItemsView is set to allow multiple selection. + if (settingsCardChoiceSet.IsMultiSelect) + { + choiceSetItemsView.SelectionMode = ItemsViewSelectionMode.Multiple; + } + + // If selection is disabled, set the ItemsView to not allow selection of items in the items view. + if (settingsCardChoiceSet.IsSelectionDisabled) + { + choiceSetItemsView.SelectionMode = ItemsViewSelectionMode.None; + } + + // Go through all the items in the choice set and make an item for each one. + for (var i = 0; i < settingsCardChoiceSet.SettingsCards.Count; i++) + { + var curCard = settingsCardChoiceSet.SettingsCards[i]; + curCard.HeaderIconImage = AdaptiveCardHelpers.ConvertBase64StringToImageIcon(curCard.HeaderIcon); + } + + // Set up the ItemsSource for the ItemsView and add the input value to the context. + // the input value is used to get the current index of the items view in relation + // to the item in the choice set. + choiceSetItemsView.ItemsSource = settingsCardChoiceSet.SettingsCards; + + // Set the automation name of the list to be the label of the choice set. + context.AddInputValue(new ItemsViewInputValue(settingsCardChoiceSet, choiceSetItemsView), renderArgs); + AutomationProperties.SetName(choiceSetItemsView, settingsCardChoiceSet.Label); + + // Return the ItemsView. + return choiceSetItemsView; + } +} diff --git a/common/Strings/en-us/Resources.resw b/common/Strings/en-us/Resources.resw index 1873f71307..3cf7a21ab0 100644 --- a/common/Strings/en-us/Resources.resw +++ b/common/Strings/en-us/Resources.resw @@ -233,4 +233,48 @@ The current user is not a {0} administrators. {0} virtual machines will not load. Please add the user to the {0} administrators group and reboot. Locked="{0}" Text explaining that the user is not in the {0} administrators group and that we need to add them. + + Open + Default text for a button that launches a content dialog in the UI + + + Ok + Default text for a content dialogs primary action button + + + Close + Default text for a content dialogs secondary action button + + + No title was provide by the provider + Default text when a provider does not provide Dev Home with a title for the dialog + + + Settings card items + Default text that will be displayed and read out to the reader if an extension does not provide us with label text for their list of values + + + Provider failed to provide a description for this item + Error text to be displayed when a provider does not provide a description for an item in a list of UI cards + + + Provider failed to provide a Header title for this item + Error text to be displayed when an provider does not provide header text for an item in a list of UI cards + + + Ok + Primary button text that will be displayed to the user as the main action when they are presented with a content dialog with two buttons + + + Cancel + Secondary button text that will be displayed to the user as a secondary action when they are presented with a content dialog with two buttons + + + Couldn't get title from the extension + Error text to be displayed to the user when a dev home extension that wants a content dialog to be displayed does not provide Dev Home with a title for the content dialog + + + An item must be selected + Error text advising the user that they must select an item in the list before proceeding + \ No newline at end of file diff --git a/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml b/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml new file mode 100644 index 0000000000..c5cc1f1a63 --- /dev/null +++ b/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml @@ -0,0 +1,26 @@ + + + + + + + + +