diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..e7e05d9ebc --- /dev/null +++ b/.clang-format @@ -0,0 +1,95 @@ + +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +#AllowAllArgumentsOnNextLine: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +#AllowAllConstructorInitializersOnNextLine: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortCaseLabelsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +#AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +ColumnLimit: 0 +CommentPragmas: "suppress" +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +FixNamespaceComments: false +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^.*(precomp|pch|stdafx)' + Priority: -1 + - Regex: '^".*"' + Priority: 1 + - Regex: '^<.*>' + Priority: 2 + - Regex: '.*' + Priority: 3 +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +ForEachMacros: ['TEST_CLASS', 'TEST_METHOD'] +MacroBlockBegin: "TEST_METHOD|TEST_CLASS|BEGIN_TEST_METHOD_PROPERTIES|BEGIN_MODULE|BEGIN_TEST_CLASS|BEGIN_TEST_METHOD" +MacroBlockEnd: "END_TEST_METHOD_PROPERTIES|END_MODULE|END_TEST_CLASS|END_TEST_METHOD" +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +#SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..2a341ececb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,202 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the Template Studio codebase. +# You can modify the rules from these initially generated values to suit your own policies. +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference. + +[*.cs] + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - new line options + +#place else statements on a new line +csharp_new_line_before_else = true +#require braces to be on a new line for lambdas, methods, control_blocks, types, properties, and accessors (also known as "Allman" style) +csharp_new_line_before_open_brace = all + +#Formatting - organize using options + +#sort System.* using directives alphabetically, and place them before other usings +dotnet_sort_system_directives_first = true + +#Formatting - spacing options + +#require NO space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on separate lines +csharp_preserve_single_line_blocks = false + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer expression bodies for accessors +csharp_style_expression_bodied_accessors = true:warning +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer expression bodies for methods +csharp_style_expression_bodied_methods = when_on_single_line:silent +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:warning + +#Style - expression level options + +#prefer out variables to be declared before the method call +csharp_style_inlined_variable_declaration = false:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer default over default(T) +csharp_prefer_simple_default_expression = true:suggestion +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - Language rules +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_var_for_built_in_types = true:warning + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,private,protected,internal,static,async,readonly,override,sealed,abstract,virtual:warning +dotnet_style_readonly_field = true:warning + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:warning + +#Style - qualification options + +#prefer events not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_event = false:suggestion +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:warning +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +[*.{cs,vb}] + +#Style - Unnecessary code rules +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_interpolation = true:suggestion diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..38b5ce7ed1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,399 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msixbundle +*.msm +*.msp + +# JetBrains Rider +*.sln.iml \ No newline at end of file diff --git a/.sscignore b/.sscignore new file mode 100644 index 0000000000..fae7a35670 --- /dev/null +++ b/.sscignore @@ -0,0 +1 @@ +{ "cfs": ["CFS0013"] } \ No newline at end of file diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000000..fbe05567d5 --- /dev/null +++ b/.vsconfig @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Component.MSBuild", + "Microsoft.NetCore.Component.Runtime.6.0", + "Microsoft.NetCore.Component.SDK", + "Microsoft.VisualStudio.Component.ManagedDesktop.Core", + "Microsoft.VisualStudio.Component.ManagedDesktop.Prerequisites", + "Microsoft.VisualStudio.Component.NuGet", + "Microsoft.VisualStudio.Component.Windows10SDK.19041", + "Microsoft.VisualStudio.Component.Windows10SDK", + "Microsoft.VisualStudio.ComponentGroup.MSIX.Packaging", + "Microsoft.VisualStudio.ComponentGroup.WindowsAppSDK.Cs", + "Microsoft.VisualStudio.Workload.ManagedDesktop" + ] +} \ No newline at end of file diff --git a/Build.cmd b/Build.cmd new file mode 100644 index 0000000000..ddd4210c38 --- /dev/null +++ b/Build.cmd @@ -0,0 +1,5 @@ +@echo off + +powershell -ExecutionPolicy Unrestricted -NoLogo -NoProfile -File %~dp0\Build.ps1 %* + +exit /b %ERRORLEVEL% \ No newline at end of file diff --git a/Build.ps1 b/Build.ps1 new file mode 100644 index 0000000000..6ec71dfa29 --- /dev/null +++ b/Build.ps1 @@ -0,0 +1,144 @@ +Param( + [string]$Platform = "x64", + [string]$Configuration = "Debug", + [string]$SDKVersion, + [string]$SDKNugetSource, + [string]$Version, + [string]$BuildStep = "all", + [switch]$IsAzurePipelineBuild = $false, + [switch]$Help = $false +) + +$StartTime = Get-Date + +if ($Help) { + Write-Host @" +Copyright (c) Microsoft Corporation and Contributors. +Licensed under the MIT License. + +Syntax: + Build.cmd [options] + +Description: + Builds Dev Home. + +Options: + + -Platform + Only buil the selected platform(s) + Example: -Platform x64 + Example: -Platform "x86,x64,arm64" + + -Configuration + Only build the selected configuration(s) + Example: -Configuration Release + Example: -Configuration "Debug,Release" + + -Help + Display this usage message. +"@ + Exit +} + +if (($BuildStep -ieq "all") -Or ($BuildStep -ieq "sdk")) { + pluginsdk\Build.ps1 -SDKVersion $SDKVersion -IsAzurePipelineBuild $IsAzurePipelineBuild +} + +$env:Build_RootDirectory = (Split-Path $MyInvocation.MyCommand.Path) +$env:Build_Platform = $Platform.ToLower() +$env:Build_Configuration = $Configuration.ToLower() +$env:msix_version = build\Scripts\CreateBuildInfo.ps1 -Version $Version -IsAzurePipelineBuild $IsAzurePipelineBuild +$env:sdk_version = build\Scripts\CreateBuildInfo.ps1 -Version $SDKVersion -IsSdkVersion $true -IsAzurePipelineBuild $IsAzurePipelineBuild + +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator') + +$msbuildPath = &"${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe +if ($IsAzurePipelineBuild) { + $nugetPath = "nuget.exe"; +} else { + $nugetPath = (Join-Path $env:Build_RootDirectory "build\NugetWrapper.cmd") +} + +$ErrorActionPreference = "Stop" + +# Install NuGet Cred Provider +Invoke-Expression "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) } -AddNetfx" + +if (-not([string]::IsNullOrWhiteSpace($SDKNugetSource))) { + & $nugetPath sources add -Source $SDKNugetSource +} + +. build\Scripts\CertSignAndInstall.ps1 + +Try { + if (($BuildStep -ieq "all") -Or ($BuildStep -ieq "msix")) { + # Update the msix version in the appxmanifest + [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") + $appxmanifestPath = (Join-Path $env:Build_RootDirectory "src\Package.appxmanifest") + $appxmanifest = [System.Xml.Linq.XDocument]::Load($appxmanifestPath) + $xName = [System.Xml.Linq.XName]::Get("{http://schemas.microsoft.com/appx/manifest/foundation/windows10}Identity"); + $appxmanifest.Root.Element($xName).Attribute("Version").Value = $env:msix_version + $appxmanifest.Save($appxmanifestPath) + + foreach ($platform in $env:Build_Platform.Split(",")) { + foreach ($configuration in $env:Build_Configuration.Split(",")) { + $appxPackageDir = (Join-Path $env:Build_RootDirectory "AppxPackages\$configuration") + $msbuildArgs = @( + ("DevHome.sln"), + ("/p:Platform="+$platform), + ("/p:Configuration="+$configuration), + ("/p:DevHomeSDKVersion="+$env:sdk_version), + ("/restore"), + ("/binaryLogger:DevHome.$platform.$configuration.binlog"), + ("/p:AppxPackageOutput=$appxPackageDir\DevHome-$platform.msix"), + ("/p:AppxPackageSigningEnabled=false"), + ("/p:GenerateAppxPackageOnBuild=true") + ) + + & $msbuildPath $msbuildArgs + if (-not($IsAzurePipelineBuild) -And $isAdmin) { + Invoke-SignPackage "$appxPackageDir\DevHome-$platform.msix" + } + } + } + } + + if (($BuildStep -ieq "all") -Or ($BuildStep -ieq "msixbundle")) { + foreach ($configuration in $env:Build_Configuration.Split(",")) { + .\build\scripts\Create-AppxBundle.ps1 -InputPath (Join-Path $env:Build_RootDirectory "AppxPackages\$configuration") -ProjectName DevHome -BundleVersion ([version]$env:msix_version) -OutputPath (Join-Path $env:Build_RootDirectory ("AppxBundles\$configuration\DevHome_" + $env:msix_version + "_8wekyb3d8bbwe.msixbundle")) + if (-not($IsAzurePipelineBuild) -And $isAdmin) { + Invoke-SignPackage ("AppxBundles\$configuration\DevHome_" + $env:msix_version + "_8wekyb3d8bbwe.msixbundle") + } + } + } +} Catch { + $formatString = "`n{0}`n`n{1}`n`n" + $fields = $_, $_.ScriptStackTrace + Write-Host ($formatString -f $fields) -ForegroundColor RED + Exit 1 +} + +$TotalTime = (Get-Date)-$StartTime +$TotalMinutes = [math]::Floor($TotalTime.TotalMinutes) +$TotalSeconds = [math]::Ceiling($TotalTime.TotalSeconds) + +if (-not($isAdmin)) { + Write-Host @" + +WARNING: Cert signing requires admin privileges. To sign, run the following in an elevated Developer Command Prompt. +"@ -ForegroundColor GREEN + foreach ($platform in $env:Build_Platform.Split(",")) { + foreach ($configuration in $env:Build_Configuration.Split(",")) { + $appxPackageDir = (Join-Path $env:Build_RootDirectory "AppxPackages\$platform\$configuration") + Write-Host @" +powershell -command "& { . build\scripts\CertSignAndInstall.ps1; Invoke-SignPackage $appxPackageDir\DevHome.msix }" +"@ -ForegroundColor GREEN + } + } +} + +Write-Host @" + +Total Running Time: +$TotalMinutes minutes and $TotalSeconds seconds +"@ -ForegroundColor CYAN \ No newline at end of file diff --git a/DevHome.sln b/DevHome.sln new file mode 100644 index 0000000000..4c847f7966 --- /dev/null +++ b/DevHome.sln @@ -0,0 +1,508 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5934D46A-E416-4600-B711-99A7CAAE8F1B}" + ProjectSection(SolutionItems) = preProject + src\.editorconfig = src\.editorconfig + .vsconfig = .vsconfig + Directory.Build.props = Directory.Build.props + Solution.props = Solution.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome", "src\DevHome.csproj", "{60E0FD98-5396-436D-BAB7-187A853A5DC6}" + ProjectSection(ProjectDependencies) = postProject + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D} = {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D} + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE} = {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Common", "common\DevHome.Common.csproj", "{8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Test", "test\DevHome.Test.csproj", "{3B409BF0-59D5-4AA3-8927-8E11476E6CEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.UITest", "uitest\DevHome.UITest.csproj", "{E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleTool", "tools\SampleTool\src\SampleTool.csproj", "{CD512D91-FDA6-4908-89D5-4106F090A7BE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleTool.UITest", "tools\SampleTool\uitest\SampleTool.UITest.csproj", "{5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Telemetry", "telemetry\DevHome.Telemetry\DevHome.Telemetry.csproj", "{C45241F7-8B4A-44F2-A78B-AFB6288F55B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow", "tools\SetupFlow\DevHome.SetupFlow\DevHome.SetupFlow.csproj", "{0901B260-1B88-4B99-A9F8-477ED0A74FBD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.UITest", "tools\SetupFlow\DevHome.SetupFlow.UITest\DevHome.SetupFlow.UITest.csproj", "{60CAE3AD-C786-47B0-820C-65CA47492FFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.AppManagement", "tools\SetupFlow\DevHome.SetupFlow.AppManagement\DevHome.SetupFlow.AppManagement.csproj", "{5613EF29-63F6-4487-91C0-95F07CC37413}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.Common", "tools\SetupFlow\DevHome.SetupFlow.Common\DevHome.SetupFlow.Common.csproj", "{B7312416-3E61-440F-B17E-A9D10DA29E3F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.DevVolume", "tools\SetupFlow\DevHome.SetupFlow.DevVolume\DevHome.SetupFlow.DevVolume.csproj", "{CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.Loading", "tools\SetupFlow\DevHome.SetupFlow.Loading\DevHome.SetupFlow.Loading.csproj", "{027F1F3B-A1F4-4508-BCA5-B222F12A2D42}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.MainPage", "tools\SetupFlow\DevHome.SetupFlow.MainPage\DevHome.SetupFlow.MainPage.csproj", "{4B979440-E731-42B7-AA7C-A8BAF98789CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.RepoConfig", "tools\SetupFlow\DevHome.SetupFlow.RepoConfig\DevHome.SetupFlow.RepoConfig.csproj", "{14D351F0-3F40-48A8-9887-F317A37C422E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.Review", "tools\SetupFlow\DevHome.SetupFlow.Review\DevHome.SetupFlow.Review.csproj", "{0E4630CC-C77A-4690-BCCD-87707882F752}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.Summary", "tools\SetupFlow\DevHome.SetupFlow.Summary\DevHome.SetupFlow.Summary.csproj", "{931F17CC-E660-4955-9D33-8D186111A0D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.UnitTest", "tools\SetupFlow\DevHome.SetupFlow.UnitTest\DevHome.SetupFlow.UnitTest.csproj", "{9A62BDDC-F33E-4EBE-B407-533263A92511}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleTool.UnitTest", "tools\SampleTool\unittest\SampleTool.UnitTest.csproj", "{F65759A2-AF44-4211-9817-76E6D02F37D0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SetupFlow", "SetupFlow", "{4179A05E-37F1-46CD-9218-0889EA2BB75B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BE6229BE-72EE-4A32-BE20-F8C6FC629047}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{A972EC5B-FC61-4964-A6FF-F9633EB75DFD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.ComInterop.Projection", "tools\SetupFlow\DevHome.SetupFlow.ComInterop.Projection\DevHome.SetupFlow.ComInterop.Projection.csproj", "{54082587-A435-423F-AE1B-01B906FFA7C5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dashboard", "Dashboard", "{222B92B1-AC7A-409D-957B-A3851D3F41B0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Dashboard", "tools\Dashboard\DevHome.Dashboard\DevHome.Dashboard.csproj", "{D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Dashboard.UITest", "tools\Dashboard\DevHome.Dashboard.UITest\DevHome.Dashboard.UITest.csproj", "{C5184A1B-6C10-4346-AE64-87D2157F99D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.UnitTest", "tools\Dashboard\DevHome.Dashboard.UnitTest\DevHome.SetupFlow.UnitTest.csproj", "{6254ADB1-B6FD-4D74-AF13-40C997919178}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.SetupFlow.ConfigurationFile", "tools\SetupFlow\DevHome.SetupFlow.ConfigurationFlow\DevHome.SetupFlow.ConfigurationFile.csproj", "{AA13E2A1-7F04-419C-9E53-9E8CD45487A7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|Any CPU.ActiveCfg = Debug|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|Any CPU.Build.0 = Debug|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|Any CPU.Deploy.0 = Debug|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|arm64.ActiveCfg = Debug|arm64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|arm64.Build.0 = Debug|arm64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|arm64.Deploy.0 = Debug|arm64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|x64.ActiveCfg = Debug|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|x64.Build.0 = Debug|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|x64.Deploy.0 = Debug|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|x86.ActiveCfg = Debug|x86 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|x86.Build.0 = Debug|x86 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Debug|x86.Deploy.0 = Debug|x86 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|Any CPU.ActiveCfg = Release|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|Any CPU.Build.0 = Release|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|Any CPU.Deploy.0 = Release|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|arm64.ActiveCfg = Release|arm64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|arm64.Build.0 = Release|arm64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|arm64.Deploy.0 = Release|arm64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|x64.ActiveCfg = Release|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|x64.Build.0 = Release|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|x64.Deploy.0 = Release|x64 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|x86.ActiveCfg = Release|x86 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|x86.Build.0 = Release|x86 + {60E0FD98-5396-436D-BAB7-187A853A5DC6}.Release|x86.Deploy.0 = Release|x86 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|Any CPU.ActiveCfg = Debug|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|Any CPU.Build.0 = Debug|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|arm64.ActiveCfg = Debug|arm64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|arm64.Build.0 = Debug|arm64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|x64.ActiveCfg = Debug|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|x64.Build.0 = Debug|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|x86.ActiveCfg = Debug|x86 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Debug|x86.Build.0 = Debug|x86 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|Any CPU.ActiveCfg = Release|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|Any CPU.Build.0 = Release|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|arm64.ActiveCfg = Release|arm64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|arm64.Build.0 = Release|arm64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|x64.ActiveCfg = Release|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|x64.Build.0 = Release|x64 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|x86.ActiveCfg = Release|x86 + {8BE0016E-5BBD-459E-A382-B1CE56E7CA5D}.Release|x86.Build.0 = Release|x86 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|Any CPU.ActiveCfg = Debug|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|Any CPU.Build.0 = Debug|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|arm64.ActiveCfg = Debug|arm64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|arm64.Build.0 = Debug|arm64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|x64.ActiveCfg = Debug|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|x64.Build.0 = Debug|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|x86.ActiveCfg = Debug|x86 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Debug|x86.Build.0 = Debug|x86 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|Any CPU.ActiveCfg = Release|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|Any CPU.Build.0 = Release|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|arm64.ActiveCfg = Release|arm64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|arm64.Build.0 = Release|arm64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|x64.ActiveCfg = Release|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|x64.Build.0 = Release|x64 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|x86.ActiveCfg = Release|x86 + {3B409BF0-59D5-4AA3-8927-8E11476E6CEB}.Release|x86.Build.0 = Release|x86 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|Any CPU.ActiveCfg = Debug|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|Any CPU.Build.0 = Debug|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|arm64.ActiveCfg = Debug|arm64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|arm64.Build.0 = Debug|arm64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|x64.ActiveCfg = Debug|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|x64.Build.0 = Debug|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|x86.ActiveCfg = Debug|x86 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Debug|x86.Build.0 = Debug|x86 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|Any CPU.ActiveCfg = Release|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|Any CPU.Build.0 = Release|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|arm64.ActiveCfg = Release|arm64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|arm64.Build.0 = Release|arm64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|x64.ActiveCfg = Release|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|x64.Build.0 = Release|x64 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|x86.ActiveCfg = Release|x86 + {E9F49E1C-C15D-4B87-92CA-9003C1C31DB5}.Release|x86.Build.0 = Release|x86 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|Any CPU.Build.0 = Debug|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|arm64.ActiveCfg = Debug|arm64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|arm64.Build.0 = Debug|arm64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|x64.ActiveCfg = Debug|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|x64.Build.0 = Debug|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|x86.ActiveCfg = Debug|x86 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Debug|x86.Build.0 = Debug|x86 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|Any CPU.ActiveCfg = Release|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|Any CPU.Build.0 = Release|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|arm64.ActiveCfg = Release|arm64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|arm64.Build.0 = Release|arm64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|x64.ActiveCfg = Release|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|x64.Build.0 = Release|x64 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|x86.ActiveCfg = Release|x86 + {CD512D91-FDA6-4908-89D5-4106F090A7BE}.Release|x86.Build.0 = Release|x86 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|Any CPU.ActiveCfg = Debug|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|Any CPU.Build.0 = Debug|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|arm64.ActiveCfg = Debug|arm64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|arm64.Build.0 = Debug|arm64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|x64.ActiveCfg = Debug|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|x64.Build.0 = Debug|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|x86.ActiveCfg = Debug|x86 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Debug|x86.Build.0 = Debug|x86 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|Any CPU.ActiveCfg = Release|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|Any CPU.Build.0 = Release|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|arm64.ActiveCfg = Release|arm64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|arm64.Build.0 = Release|arm64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|x64.ActiveCfg = Release|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|x64.Build.0 = Release|x64 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|x86.ActiveCfg = Release|x86 + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D}.Release|x86.Build.0 = Release|x86 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|Any CPU.Build.0 = Debug|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|arm64.ActiveCfg = Debug|arm64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|arm64.Build.0 = Debug|arm64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|x64.ActiveCfg = Debug|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|x64.Build.0 = Debug|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|x86.ActiveCfg = Debug|x86 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Debug|x86.Build.0 = Debug|x86 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|Any CPU.ActiveCfg = Release|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|Any CPU.Build.0 = Release|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|arm64.ActiveCfg = Release|arm64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|arm64.Build.0 = Release|arm64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|x64.ActiveCfg = Release|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|x64.Build.0 = Release|x64 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|x86.ActiveCfg = Release|x86 + {C45241F7-8B4A-44F2-A78B-AFB6288F55B9}.Release|x86.Build.0 = Release|x86 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|Any CPU.ActiveCfg = Debug|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|Any CPU.Build.0 = Debug|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|arm64.ActiveCfg = Debug|arm64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|arm64.Build.0 = Debug|arm64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|x64.ActiveCfg = Debug|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|x64.Build.0 = Debug|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|x86.ActiveCfg = Debug|x86 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Debug|x86.Build.0 = Debug|x86 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|Any CPU.ActiveCfg = Release|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|Any CPU.Build.0 = Release|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|arm64.ActiveCfg = Release|arm64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|arm64.Build.0 = Release|arm64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|x64.ActiveCfg = Release|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|x64.Build.0 = Release|x64 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|x86.ActiveCfg = Release|x86 + {0901B260-1B88-4B99-A9F8-477ED0A74FBD}.Release|x86.Build.0 = Release|x86 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|Any CPU.Build.0 = Debug|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|arm64.ActiveCfg = Debug|arm64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|arm64.Build.0 = Debug|arm64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|x64.ActiveCfg = Debug|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|x64.Build.0 = Debug|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|x86.ActiveCfg = Debug|x86 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Debug|x86.Build.0 = Debug|x86 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|Any CPU.ActiveCfg = Release|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|Any CPU.Build.0 = Release|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|arm64.ActiveCfg = Release|arm64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|arm64.Build.0 = Release|arm64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|x64.ActiveCfg = Release|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|x64.Build.0 = Release|x64 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|x86.ActiveCfg = Release|x86 + {60CAE3AD-C786-47B0-820C-65CA47492FFA}.Release|x86.Build.0 = Release|x86 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|Any CPU.ActiveCfg = Debug|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|Any CPU.Build.0 = Debug|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|arm64.ActiveCfg = Debug|ARM64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|arm64.Build.0 = Debug|ARM64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|x64.ActiveCfg = Debug|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|x64.Build.0 = Debug|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|x86.ActiveCfg = Debug|x86 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Debug|x86.Build.0 = Debug|x86 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|Any CPU.ActiveCfg = Release|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|Any CPU.Build.0 = Release|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|arm64.ActiveCfg = Release|ARM64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|arm64.Build.0 = Release|ARM64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|x64.ActiveCfg = Release|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|x64.Build.0 = Release|x64 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|x86.ActiveCfg = Release|x86 + {5613EF29-63F6-4487-91C0-95F07CC37413}.Release|x86.Build.0 = Release|x86 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|Any CPU.Build.0 = Debug|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|arm64.ActiveCfg = Debug|ARM64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|arm64.Build.0 = Debug|ARM64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|x64.ActiveCfg = Debug|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|x64.Build.0 = Debug|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|x86.ActiveCfg = Debug|x86 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Debug|x86.Build.0 = Debug|x86 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|Any CPU.ActiveCfg = Release|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|Any CPU.Build.0 = Release|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|arm64.ActiveCfg = Release|ARM64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|arm64.Build.0 = Release|ARM64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|x64.ActiveCfg = Release|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|x64.Build.0 = Release|x64 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|x86.ActiveCfg = Release|x86 + {B7312416-3E61-440F-B17E-A9D10DA29E3F}.Release|x86.Build.0 = Release|x86 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|Any CPU.ActiveCfg = Debug|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|Any CPU.Build.0 = Debug|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|arm64.ActiveCfg = Debug|ARM64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|arm64.Build.0 = Debug|ARM64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|x64.ActiveCfg = Debug|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|x64.Build.0 = Debug|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|x86.ActiveCfg = Debug|x86 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Debug|x86.Build.0 = Debug|x86 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|Any CPU.ActiveCfg = Release|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|Any CPU.Build.0 = Release|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|arm64.ActiveCfg = Release|ARM64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|arm64.Build.0 = Release|ARM64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|x64.ActiveCfg = Release|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|x64.Build.0 = Release|x64 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|x86.ActiveCfg = Release|x86 + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4}.Release|x86.Build.0 = Release|x86 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|Any CPU.ActiveCfg = Debug|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|Any CPU.Build.0 = Debug|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|arm64.ActiveCfg = Debug|arm64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|arm64.Build.0 = Debug|arm64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|x64.ActiveCfg = Debug|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|x64.Build.0 = Debug|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|x86.ActiveCfg = Debug|x86 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Debug|x86.Build.0 = Debug|x86 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|Any CPU.ActiveCfg = Release|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|Any CPU.Build.0 = Release|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|arm64.ActiveCfg = Release|arm64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|arm64.Build.0 = Release|arm64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|x64.ActiveCfg = Release|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|x64.Build.0 = Release|x64 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|x86.ActiveCfg = Release|x86 + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42}.Release|x86.Build.0 = Release|x86 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|Any CPU.ActiveCfg = Debug|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|Any CPU.Build.0 = Debug|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|arm64.ActiveCfg = Debug|arm64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|arm64.Build.0 = Debug|arm64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|x64.ActiveCfg = Debug|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|x64.Build.0 = Debug|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|x86.ActiveCfg = Debug|x86 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Debug|x86.Build.0 = Debug|x86 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|Any CPU.ActiveCfg = Release|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|Any CPU.Build.0 = Release|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|arm64.ActiveCfg = Release|arm64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|arm64.Build.0 = Release|arm64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|x64.ActiveCfg = Release|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|x64.Build.0 = Release|x64 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|x86.ActiveCfg = Release|x86 + {4B979440-E731-42B7-AA7C-A8BAF98789CF}.Release|x86.Build.0 = Release|x86 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|Any CPU.ActiveCfg = Debug|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|Any CPU.Build.0 = Debug|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|arm64.ActiveCfg = Debug|arm64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|arm64.Build.0 = Debug|arm64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|x64.ActiveCfg = Debug|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|x64.Build.0 = Debug|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|x86.ActiveCfg = Debug|x86 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Debug|x86.Build.0 = Debug|x86 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|Any CPU.ActiveCfg = Release|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|Any CPU.Build.0 = Release|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|arm64.ActiveCfg = Release|arm64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|arm64.Build.0 = Release|arm64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|x64.ActiveCfg = Release|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|x64.Build.0 = Release|x64 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|x86.ActiveCfg = Release|x86 + {14D351F0-3F40-48A8-9887-F317A37C422E}.Release|x86.Build.0 = Release|x86 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|Any CPU.ActiveCfg = Debug|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|Any CPU.Build.0 = Debug|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|arm64.ActiveCfg = Debug|arm64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|arm64.Build.0 = Debug|arm64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|x64.ActiveCfg = Debug|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|x64.Build.0 = Debug|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|x86.ActiveCfg = Debug|x86 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Debug|x86.Build.0 = Debug|x86 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|Any CPU.ActiveCfg = Release|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|Any CPU.Build.0 = Release|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|arm64.ActiveCfg = Release|arm64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|arm64.Build.0 = Release|arm64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|x64.ActiveCfg = Release|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|x64.Build.0 = Release|x64 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|x86.ActiveCfg = Release|x86 + {0E4630CC-C77A-4690-BCCD-87707882F752}.Release|x86.Build.0 = Release|x86 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|Any CPU.ActiveCfg = Debug|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|Any CPU.Build.0 = Debug|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|arm64.ActiveCfg = Debug|arm64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|arm64.Build.0 = Debug|arm64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|x64.ActiveCfg = Debug|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|x64.Build.0 = Debug|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|x86.ActiveCfg = Debug|x86 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Debug|x86.Build.0 = Debug|x86 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|Any CPU.ActiveCfg = Release|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|Any CPU.Build.0 = Release|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|arm64.ActiveCfg = Release|arm64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|arm64.Build.0 = Release|arm64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|x64.ActiveCfg = Release|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|x64.Build.0 = Release|x64 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|x86.ActiveCfg = Release|x86 + {931F17CC-E660-4955-9D33-8D186111A0D9}.Release|x86.Build.0 = Release|x86 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|Any CPU.ActiveCfg = Debug|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|Any CPU.Build.0 = Debug|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|arm64.ActiveCfg = Debug|arm64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|arm64.Build.0 = Debug|arm64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|x64.ActiveCfg = Debug|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|x64.Build.0 = Debug|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|x86.ActiveCfg = Debug|x86 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Debug|x86.Build.0 = Debug|x86 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|Any CPU.ActiveCfg = Release|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|Any CPU.Build.0 = Release|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|arm64.ActiveCfg = Release|arm64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|arm64.Build.0 = Release|arm64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|x64.ActiveCfg = Release|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|x64.Build.0 = Release|x64 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|x86.ActiveCfg = Release|x86 + {9A62BDDC-F33E-4EBE-B407-533263A92511}.Release|x86.Build.0 = Release|x86 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|Any CPU.ActiveCfg = Debug|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|Any CPU.Build.0 = Debug|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|arm64.ActiveCfg = Debug|arm64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|arm64.Build.0 = Debug|arm64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|x64.ActiveCfg = Debug|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|x64.Build.0 = Debug|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|x86.ActiveCfg = Debug|x86 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Debug|x86.Build.0 = Debug|x86 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|Any CPU.ActiveCfg = Release|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|Any CPU.Build.0 = Release|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|arm64.ActiveCfg = Release|arm64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|arm64.Build.0 = Release|arm64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|x64.ActiveCfg = Release|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|x64.Build.0 = Release|x64 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|x86.ActiveCfg = Release|x86 + {F65759A2-AF44-4211-9817-76E6D02F37D0}.Release|x86.Build.0 = Release|x86 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|Any CPU.ActiveCfg = Debug|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|Any CPU.Build.0 = Debug|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|arm64.ActiveCfg = Debug|arm64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|arm64.Build.0 = Debug|arm64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|x64.ActiveCfg = Debug|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|x64.Build.0 = Debug|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|x86.ActiveCfg = Debug|x86 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Debug|x86.Build.0 = Debug|x86 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|Any CPU.ActiveCfg = Release|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|Any CPU.Build.0 = Release|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|arm64.ActiveCfg = Release|arm64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|arm64.Build.0 = Release|arm64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|x64.ActiveCfg = Release|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|x64.Build.0 = Release|x64 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|x86.ActiveCfg = Release|x86 + {54082587-A435-423F-AE1B-01B906FFA7C5}.Release|x86.Build.0 = Release|x86 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|Any CPU.Build.0 = Debug|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|arm64.ActiveCfg = Debug|arm64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|arm64.Build.0 = Debug|arm64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|x64.ActiveCfg = Debug|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|x64.Build.0 = Debug|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|x86.ActiveCfg = Debug|x86 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Debug|x86.Build.0 = Debug|x86 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|Any CPU.ActiveCfg = Release|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|Any CPU.Build.0 = Release|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|arm64.ActiveCfg = Release|arm64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|arm64.Build.0 = Release|arm64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|x64.ActiveCfg = Release|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|x64.Build.0 = Release|x64 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|x86.ActiveCfg = Release|x86 + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE}.Release|x86.Build.0 = Release|x86 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|Any CPU.Build.0 = Debug|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|arm64.ActiveCfg = Debug|arm64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|arm64.Build.0 = Debug|arm64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|x64.ActiveCfg = Debug|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|x64.Build.0 = Debug|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|x86.ActiveCfg = Debug|x86 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Debug|x86.Build.0 = Debug|x86 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|Any CPU.ActiveCfg = Release|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|Any CPU.Build.0 = Release|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|arm64.ActiveCfg = Release|arm64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|arm64.Build.0 = Release|arm64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|x64.ActiveCfg = Release|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|x64.Build.0 = Release|x64 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|x86.ActiveCfg = Release|x86 + {C5184A1B-6C10-4346-AE64-87D2157F99D2}.Release|x86.Build.0 = Release|x86 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|Any CPU.ActiveCfg = Debug|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|Any CPU.Build.0 = Debug|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|arm64.ActiveCfg = Debug|arm64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|arm64.Build.0 = Debug|arm64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|x64.ActiveCfg = Debug|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|x64.Build.0 = Debug|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|x86.ActiveCfg = Debug|x86 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Debug|x86.Build.0 = Debug|x86 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|Any CPU.ActiveCfg = Release|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|Any CPU.Build.0 = Release|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|arm64.ActiveCfg = Release|arm64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|arm64.Build.0 = Release|arm64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|x64.ActiveCfg = Release|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|x64.Build.0 = Release|x64 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|x86.ActiveCfg = Release|x86 + {6254ADB1-B6FD-4D74-AF13-40C997919178}.Release|x86.Build.0 = Release|x86 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|Any CPU.Build.0 = Debug|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|arm64.ActiveCfg = Debug|arm64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|arm64.Build.0 = Debug|arm64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|x64.ActiveCfg = Debug|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|x64.Build.0 = Debug|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|x86.ActiveCfg = Debug|x86 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Debug|x86.Build.0 = Debug|x86 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|Any CPU.ActiveCfg = Release|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|Any CPU.Build.0 = Release|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|arm64.ActiveCfg = Release|arm64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|arm64.Build.0 = Release|arm64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|x64.ActiveCfg = Release|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|x64.Build.0 = Release|x64 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|x86.ActiveCfg = Release|x86 + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CD512D91-FDA6-4908-89D5-4106F090A7BE} = {BE6229BE-72EE-4A32-BE20-F8C6FC629047} + {5C796E1F-8905-41C7-8C1D-D921F8E0AE3D} = {BE6229BE-72EE-4A32-BE20-F8C6FC629047} + {0901B260-1B88-4B99-A9F8-477ED0A74FBD} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {60CAE3AD-C786-47B0-820C-65CA47492FFA} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {5613EF29-63F6-4487-91C0-95F07CC37413} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {B7312416-3E61-440F-B17E-A9D10DA29E3F} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {CB65FC14-ACC3-49DD-B804-FA4C80BFF1B4} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {027F1F3B-A1F4-4508-BCA5-B222F12A2D42} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {4B979440-E731-42B7-AA7C-A8BAF98789CF} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {14D351F0-3F40-48A8-9887-F317A37C422E} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {0E4630CC-C77A-4690-BCCD-87707882F752} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {931F17CC-E660-4955-9D33-8D186111A0D9} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {9A62BDDC-F33E-4EBE-B407-533263A92511} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {F65759A2-AF44-4211-9817-76E6D02F37D0} = {BE6229BE-72EE-4A32-BE20-F8C6FC629047} + {4179A05E-37F1-46CD-9218-0889EA2BB75B} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} + {54082587-A435-423F-AE1B-01B906FFA7C5} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + {222B92B1-AC7A-409D-957B-A3851D3F41B0} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} + {D2303635-3DD9-4DCA-A38A-F5306D0BB8FE} = {222B92B1-AC7A-409D-957B-A3851D3F41B0} + {C5184A1B-6C10-4346-AE64-87D2157F99D2} = {222B92B1-AC7A-409D-957B-A3851D3F41B0} + {6254ADB1-B6FD-4D74-AF13-40C997919178} = {222B92B1-AC7A-409D-957B-A3851D3F41B0} + {AA13E2A1-7F04-419C-9E53-9E8CD45487A7} = {4179A05E-37F1-46CD-9218-0889EA2BB75B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {030B5641-B206-46BB-BF71-36FF009088FA} + EndGlobalSection +EndGlobal diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000000..83026353fc --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,40 @@ + + + + Copyright (C) 2022 Microsoft Corporation + Microsoft Corp. + Copyright (C) 2022 Microsoft Corporation + DevHome + Microsoft Corporation + en-US + x64;x86;ARM64 + DevHome + true + Recommended + $(Platform) + + + + true + + + + <_PropertySheetDisplayName>DevHome.Root.Props + $(MsbuildThisFileDirectory)\Cpp.Build.props + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/SamplePlugin/Assets/LockScreenLogo.scale-200.png b/SamplePlugin/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/SamplePlugin/Assets/LockScreenLogo.scale-200.png differ diff --git a/SamplePlugin/Assets/SplashScreen.scale-200.png b/SamplePlugin/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..32f486a867 Binary files /dev/null and b/SamplePlugin/Assets/SplashScreen.scale-200.png differ diff --git a/SamplePlugin/Assets/Square150x150Logo.scale-200.png b/SamplePlugin/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..53ee3777ea Binary files /dev/null and b/SamplePlugin/Assets/Square150x150Logo.scale-200.png differ diff --git a/SamplePlugin/Assets/Square44x44Logo.scale-200.png b/SamplePlugin/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..f713bba67f Binary files /dev/null and b/SamplePlugin/Assets/Square44x44Logo.scale-200.png differ diff --git a/SamplePlugin/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/SamplePlugin/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..dc9f5bea0c Binary files /dev/null and b/SamplePlugin/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/SamplePlugin/Assets/StoreLogo.png b/SamplePlugin/Assets/StoreLogo.png new file mode 100644 index 0000000000..a4586f26bd Binary files /dev/null and b/SamplePlugin/Assets/StoreLogo.png differ diff --git a/SamplePlugin/Assets/Wide310x150Logo.scale-200.png b/SamplePlugin/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..8b4a5d0dd5 Binary files /dev/null and b/SamplePlugin/Assets/Wide310x150Logo.scale-200.png differ diff --git a/SamplePlugin/DevIDProvider.cs b/SamplePlugin/DevIDProvider.cs new file mode 100644 index 0000000000..1d0bf7bf43 --- /dev/null +++ b/SamplePlugin/DevIDProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Windows.DevHome.SDK; + +namespace SamplePlugin; + +internal class DevIDProvider : IDevIdProvider +{ + public IEnumerable GetLoggedInDeveloperIds() => throw new NotImplementedException(); + + public string GetName() => "Sample Dev ID Provider"; + + public IPluginAdaptiveCardController GetAdaptiveCardController(string[] args) + { + if (args.Length > 0 && args[0] == "LoginUI") + { + return new LoginUI(); + } + + return null; + } + + public void LoginNewDeveloperId() => throw new NotImplementedException(); + + public void LogoutDeveloperId(IDeveloperId developerId) => throw new NotImplementedException(); + + public string LogoutUI() => throw new NotImplementedException(); +} diff --git a/SamplePlugin/LoginUI.cs b/SamplePlugin/LoginUI.cs new file mode 100644 index 0000000000..6b41e0a463 --- /dev/null +++ b/SamplePlugin/LoginUI.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Windows.DevHome.SDK; + +namespace SamplePlugin; +internal class LoginUI : IPluginAdaptiveCardController +{ + private IPluginAdaptiveCard pluginUI; + + public void Create(IPluginAdaptiveCard pluginUI) + { + this.pluginUI = pluginUI; + + pluginUI.Update( + @" +{ + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""type"": ""AdaptiveCard"", + ""version"": ""1.4"", + ""body"": [ + { + ""type"": ""TextBlock"", + ""text"": ""This is a plugin Adaptive Card! Click Submit!"" + }, + { + ""type"": ""Input.Text"", + ""id"": ""firstName"", + ""label"": ""What is your first name?"" + }, + { + ""type"": ""Input.Text"", + ""id"": ""lastName"", + ""label"": ""What is your last name?"" + } + ], + ""actions"": [ + { + ""type"": ""Action.Execute"", + ""title"": ""Action.Execute"", + ""verb"": ""doStuff"", + ""data"": { + ""x"": 13 + } + } + ] +} +", null, + null); + } + + public void Dispose() + { + } + + public void OnAction(string actionVerb, string args) + { + pluginUI.Update( + @" +{ + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""type"": ""AdaptiveCard"", + ""version"": ""1.4"", + ""body"": [ + { + ""type"": ""TextBlock"", + ""text"": ""Form **submitted**! Thank you :)"" + } + ] +} +", null, + null); + } +} diff --git a/SamplePlugin/Package.appxmanifest b/SamplePlugin/Package.appxmanifest new file mode 100644 index 0000000000..bbf611f24e --- /dev/null +++ b/SamplePlugin/Package.appxmanifest @@ -0,0 +1,79 @@ + + + + + + + + Dev Home Sample Plugin + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SamplePlugin/Program.cs b/SamplePlugin/Program.cs new file mode 100644 index 0000000000..9be596e92e --- /dev/null +++ b/SamplePlugin/Program.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32; +using Microsoft.Windows.DevHome.SDK; +using Windows.ApplicationModel.Background; +using Windows.Globalization.DateTimeFormatting; + +namespace SamplePlugin; + +public class Program +{ + [MTAThread] + public static void Main(string[] args) + { + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + using var server = new PluginServer(); + var pluginDisposedEvent = new ManualResetEvent(false); + var pluginInstance = new SamplePlugin(pluginDisposedEvent); + + // We are instantiating plugin instance once above, and returning it every time the callback in RegisterPlugin below is called. + // This makes sure that only one instance of SamplePlugin is alive, which is returned every time the host asks for the IPlugin object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + server.RegisterPlugin(() => pluginInstance); + + // This will make the main thread wait until the event is signalled by the plugin class. + // Since we have single instance of the plugin object, we exit as sooon as it is disposed. + pluginDisposedEvent.WaitOne(); + } + else + { + Console.WriteLine("Not being launched as a Plugin... exiting."); + } + } +} diff --git a/SamplePlugin/Properties/launchSettings.json b/SamplePlugin/Properties/launchSettings.json new file mode 100644 index 0000000000..3c39f6cb54 --- /dev/null +++ b/SamplePlugin/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "Provider on launch": { + "commandName": "MsixPackage", + "commandLineArgs": "-RegisterProcessAsComServer", + "doNotLaunchApp": true + }, + "UI App": { + "commandName": "MsixPackage" + }, + "Provider": { + "commandName": "MsixPackage", + "commandLineArgs": "-RegisterProcessAsComServer" + } + } +} \ No newline at end of file diff --git a/SamplePlugin/RepositoryProvider.cs b/SamplePlugin/RepositoryProvider.cs new file mode 100644 index 0000000000..41a90c70a5 --- /dev/null +++ b/SamplePlugin/RepositoryProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Windows.DevHome.SDK; +using Windows.Foundation; + +namespace SamplePlugin; +internal class RepositoryProvider : IRepositoryProvider +{ + public string GetDisplayName() => "Repository Provider"; + + public IAsyncOperation> GetRepositoriesAsync(IDeveloperId developerId) => throw new NotImplementedException(); +} diff --git a/SamplePlugin/SamplePlugin.cs b/SamplePlugin/SamplePlugin.cs new file mode 100644 index 0000000000..cef3485d8a --- /dev/null +++ b/SamplePlugin/SamplePlugin.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using Microsoft.Windows.DevHome.SDK; +using Microsoft.Windows.Widgets.Providers; +using Windows.ApplicationModel.Background; +using Windows.Foundation; +using Windows.Storage; +using Windows.Storage.FileProperties; +using Windows.Storage.Streams; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. +namespace SamplePlugin; + +[ComVisible(true)] +[Guid("BEA53870-57BA-4741-B849-DBC8A3A06CC6")] +[ComDefaultInterface(typeof(IPlugin))] +public sealed class SamplePlugin : IPlugin +{ + private readonly ManualResetEvent _pluginDisposedEvent; + + public SamplePlugin(ManualResetEvent pluginDisposedEvent) + { + this._pluginDisposedEvent = pluginDisposedEvent; + } + + public object GetProvider(ProviderType providerType) + { + switch (providerType) + { + case ProviderType.DevId: + return new DevIDProvider(); + case ProviderType.Repository: + return new RepositoryProvider(); + default: + return null; + } + } + + public void Dispose() + { + this._pluginDisposedEvent.Set(); + } + +} diff --git a/SamplePlugin/SamplePlugin.csproj b/SamplePlugin/SamplePlugin.csproj new file mode 100644 index 0000000000..fcb96b0ade --- /dev/null +++ b/SamplePlugin/SamplePlugin.csproj @@ -0,0 +1,58 @@ + + + Exe + net6.0-windows10.0.19041.0 + 10.0.17763.0 + SamplePlugin + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + false + true + false + DevHomeSamplePlugin + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + + + + + + true + + diff --git a/SamplePlugin/SamplePlugin.sln b/SamplePlugin/SamplePlugin.sln new file mode 100644 index 0000000000..4521a132a8 --- /dev/null +++ b/SamplePlugin/SamplePlugin.sln @@ -0,0 +1,101 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5934D46A-E416-4600-B711-99A7CAAE8F1B}" + ProjectSection(SolutionItems) = preProject + ..\src\.editorconfig = ..\src\.editorconfig + ..\.vsconfig = ..\.vsconfig + ..\Directory.Build.props = ..\Directory.Build.props + ..\Solution.props = ..\Solution.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePlugin", "SamplePlugin.csproj", "{81215577-330C-4EAC-B9C0-574B2176ED18}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PluginSdk", "PluginSdk", "{A4E5B553-54A3-4063-8186-6CCE376CE195}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Windows.DevHome.SDK", "..\pluginsdk\Microsoft.Windows.DevHome.SDK\Microsoft.Windows.DevHome.SDK.vcxproj", "{295DD37E-C85D-4B08-AAFE-7381FA890463}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Windows.DevHome.SDK.Lib", "..\pluginsdk\Microsoft.Windows.DevHome.SDK.Lib\Microsoft.Windows.DevHome.SDK.Lib.csproj", "{CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|AnyCPU = Release|AnyCPU + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|AnyCPU.ActiveCfg = Debug|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|AnyCPU.Build.0 = Debug|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|AnyCPU.Deploy.0 = Debug|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|arm64.ActiveCfg = Debug|arm64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|arm64.Build.0 = Debug|arm64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|arm64.Deploy.0 = Debug|arm64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|x64.ActiveCfg = Debug|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|x64.Build.0 = Debug|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|x64.Deploy.0 = Debug|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|x86.ActiveCfg = Debug|x86 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|x86.Build.0 = Debug|x86 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Debug|x86.Deploy.0 = Debug|x86 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|AnyCPU.ActiveCfg = Release|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|AnyCPU.Build.0 = Release|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|AnyCPU.Deploy.0 = Release|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|arm64.ActiveCfg = Release|arm64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|arm64.Build.0 = Release|arm64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|arm64.Deploy.0 = Release|arm64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|x64.ActiveCfg = Release|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|x64.Build.0 = Release|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|x64.Deploy.0 = Release|x64 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|x86.ActiveCfg = Release|x86 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|x86.Build.0 = Release|x86 + {81215577-330C-4EAC-B9C0-574B2176ED18}.Release|x86.Deploy.0 = Release|x86 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|AnyCPU.ActiveCfg = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|AnyCPU.Build.0 = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|arm64.ActiveCfg = Debug|ARM64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|arm64.Build.0 = Debug|ARM64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.ActiveCfg = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.Build.0 = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x86.ActiveCfg = Debug|Win32 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x86.Build.0 = Debug|Win32 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|AnyCPU.ActiveCfg = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|AnyCPU.Build.0 = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|arm64.ActiveCfg = Release|ARM64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|arm64.Build.0 = Release|ARM64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.ActiveCfg = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.Build.0 = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x86.ActiveCfg = Release|Win32 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x86.Build.0 = Release|Win32 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|arm64.ActiveCfg = Debug|ARM64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|arm64.Build.0 = Debug|ARM64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|x64.ActiveCfg = Debug|x64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|x64.Build.0 = Debug|x64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|x86.ActiveCfg = Debug|x86 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Debug|x86.Build.0 = Debug|x86 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|AnyCPU.Build.0 = Release|AnyCPU + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|arm64.ActiveCfg = Release|ARM64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|arm64.Build.0 = Release|ARM64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|x64.ActiveCfg = Release|x64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|x64.Build.0 = Release|x64 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|x86.ActiveCfg = Release|x86 + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {295DD37E-C85D-4B08-AAFE-7381FA890463} = {A4E5B553-54A3-4063-8186-6CCE376CE195} + {CAD6D70B-6A44-4CDD-86E8-9BD64BD40F39} = {A4E5B553-54A3-4063-8186-6CCE376CE195} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {030B5641-B206-46BB-BF71-36FF009088FA} + EndGlobalSection +EndGlobal diff --git a/Solution.props b/Solution.props new file mode 100644 index 0000000000..374bea9b43 --- /dev/null +++ b/Solution.props @@ -0,0 +1,6 @@ + + + + $(IntDir)Generated Files\ + + diff --git a/Test.cmd b/Test.cmd new file mode 100644 index 0000000000..cc404252e9 --- /dev/null +++ b/Test.cmd @@ -0,0 +1,5 @@ +@echo off + +powershell -ExecutionPolicy Unrestricted -NoLogo -NoProfile -File %~dp0\Test.ps1 %* + +exit /b %ERRORLEVEL% \ No newline at end of file diff --git a/Test.ps1 b/Test.ps1 new file mode 100644 index 0000000000..384c0a2e6c --- /dev/null +++ b/Test.ps1 @@ -0,0 +1,150 @@ +Param( + [string]$Platform = "x64", + [string]$Configuration = "debug", + [switch]$IsAzurePipelineBuild = $false, + [switch]$Help = $false +) + +$StartTime = Get-Date + +if ($Help) { + Write-Host @" +Copyright (c) Microsoft Corporation and Contributors. +Licensed under the MIT License. + +Syntax: + Test.cmd [options] + +Description: + Runs Dev Home tests. + +Options: + + -Platform + Only build the selected platform(s) + Example: -Platform x64 + Example: -Platform "x86,x64,arm64" + + -Configuration + Only build the selected configuration(s) + Example: -Configuration release + Example: -Configuration "debug,release" + + -Help + Display this usage message. +"@ + Exit +} + +$env:Build_SourcesDirectory = (Split-Path $MyInvocation.MyCommand.Path) +$env:Build_Platform = $Platform.ToLower() +$env:Build_Configuration = $Configuration.ToLower() + +$vstestPath = &"${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -find **\TestPlatform\vstest.console.exe + +$ErrorActionPreference = "Stop" + +$isInstalled = Get-ChildItem HKLM:\SOFTWARE\$_\Microsoft\Windows\CurrentVersion\Uninstall\ | ? {($_.GetValue("DisplayName")) -like "*Windows Application Driver*"} + +if (-not($IsAzurePipelineBuild)) { + if ($isInstalled){ + Write-Host "WinAppDriver is already installed on this computer." + } + else { + Write-Host "WinAppDriver will be installed in the background." + $url = "https://github.com/microsoft/WinAppDriver/releases/download/v1.2.99/WindowsApplicationDriver-1.2.99-win-x64.exe" + $outpath = "$env:Build_SourcesDirectory\temp" + if (-not(Test-Path -Path $outpath)) { + New-Item -ItemType Directory -Path $outpath | Out-Null + } + Invoke-WebRequest -Uri $url -OutFile "$env:Build_SourcesDirectory\temp\WinAppDriverx64.exe" + + Start-Process -Wait -Filepath $env:Build_SourcesDirectory\temp\WinAppDriverx64.exe -ArgumentList "/S" -PassThru + } + + Start-Process -FilePath "C:\Program Files\Windows Application Driver\WinAppDriver.exe" +} + +Function ShutDownTests { + if (-not($IsAzurePipelineBuild)) { + Stop-Process -Name "WinAppDriver" + } + + $TotalTime = (Get-Date)-$StartTime + $TotalMinutes = [math]::Floor($TotalTime.TotalMinutes) + $TotalSeconds = [math]::Ceiling($TotalTime.TotalSeconds) + + Write-Host @" + + Total Running Time: + $TotalMinutes minutes and $TotalSeconds seconds +"@ -ForegroundColor CYAN +} + +if (-not(Test-Path -Path "AppxPackages")) { + Write-Host "Nothing to test. Ensure you have built via the command line before running tests. Exiting." -ForegroundColor YELLOW + Exit 1 +} + +Try { + foreach ($platform in $env:Build_Platform.Split(",")) { + foreach ($configuration in $env:Build_Configuration.Split(",")) { + # TODO: UI tests are currently disabled in pipeline until signing is solved + if (-not($IsAzurePipelineBuild)) { + $DevHomePackage = Get-AppPackage "Microsoft.DevHome" + if ($DevHomePackage) { + Write-Host "Uninstalling old Dev Home" + Remove-AppPackage -Package $DevHomePackage.PackageFullName + } + Write-Host "Installing Dev Home" + Add-AppPackage "AppxPackages\$configuration\DevHome-$platform.msix" + } + + $vstestArgs = @( + ("/Platform:$platform"), + ("/Logger:trx;LogFileName=DevHome.Test-$platform-$configuration.trx"), + ("test\bin\$platform\$configuration\net6.0-windows10.0.19041.0\DevHome.Test.dll") + ) + $winAppTestArgs = @( + ("/Platform:$platform"), + ("/Logger:trx;LogFileName=DevHome.UITest-$platform-$configuration.trx"), + ("uitest\bin\$platform\$configuration\net6.0-windows10.0.19041.0\DevHome.UITest.dll") + ) + + & $vstestPath $vstestArgs + # TODO: UI tests are currently disabled in pipeline until signing is solved + if (-not($IsAzurePipelineBuild)) { + & $vstestPath $winAppTestArgs + } + + foreach ($toolPath in (Get-ChildItem "tools")) { + $tool = $toolPath.Name + $vstestArgs = @( + ("/Platform:$platform"), + ("/Logger:trx;LogFileName=$tool.Test-$platform-$configuration.trx"), + ("tools\$tool\*UnitTest\bin\$platform\$configuration\net6.0-windows10.0.19041.0\*.UnitTest.dll") + ) + + $winAppTestArgs = @( + ("/Platform:$platform"), + ("/Logger:trx;LogFileName=$tool.UITest-$platform-$configuration.trx"), + ("tools\$tool\*UITest\bin\$platform\$configuration\net6.0-windows10.0.19041.0\*.UITest.dll") + ) + + & $vstestPath $vstestArgs + # TODO: UI tests are currently disabled in pipeline until signing is solved + if (-not($IsAzurePipelineBuild)) { + & $vstestPath $winAppTestArgs + } + } + } + } +} Catch { + $formatString = "`n{0}`n`n{1}`n`n" + $fields = $_, $_.ScriptStackTrace + Write-Host ($formatString -f $fields) -ForegroundColor RED + ShutDownTests + Exit 1 +} + +ShutDownTests diff --git a/build/NugetWrapper.cmd b/build/NugetWrapper.cmd new file mode 100644 index 0000000000..b3cebe214e --- /dev/null +++ b/build/NugetWrapper.cmd @@ -0,0 +1,23 @@ +@echo OFF +setlocal + +if "%VisualStudioVersion%" == "" set VisualStudioVersion=15.0 + +if defined NUGETEXETOOLPATH goto :AzurePipelines + +if not exist %TEMP%\nuget.5.8.0-preview.2.exe ( + echo Nuget.exe not found in the temp dir, downloading. + powershell -Command "& { Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/v5.8.0-preview.2/nuget.exe -outfile $env:TEMP\nuget.5.8.0-preview.2.exe }" +) + +%TEMP%\nuget.5.8.0-preview.2.exe %* + +exit /B %ERRORLEVEL% + + + +:AzurePipelines +echo NUGETEXETOOLPATH = %NUGETEXETOOLPATH% + +%NUGETEXETOOLPATH% %* +exit /B %ERRORLEVEL% diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml new file mode 100644 index 0000000000..a269c0a91f --- /dev/null +++ b/build/azure-pipelines.yml @@ -0,0 +1,325 @@ +trigger: +- main + +pool: + vmImage: 'windows-latest' + +parameters: + - name: SignOutput + type: boolean + default: False + - name: Platforms + type: object + default: + - x86 + - x64 + - arm64 + - name: Configurations + type: object + default: + - Debug + - Release + +variables: + MSIXVersion: '0.100' + SDKVersion: '0.100' + solution: '**/DevHome.sln' + appxPackageDir: 'AppxPackages' + testOutputArtifactDir: 'TestResults' + +resources: + repositories: + - repository: templates_onebranch + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +stages: +- stage: SourceAnalysis + # This stage is expected to run in parallel with BuildWindowsAppSDK-Stages. + dependsOn: '' + jobs: + - job: SDLSourcesScanning + steps: + # Required by the Packaged ES SDL Templates. + - checkout: self + + # Typically this has been done. + - task: NodeTool@0 + displayName: 'Use Node >=8.6.0' + inputs: + versionSpec: '>=8.6.0' + + - template: v2/Steps/PackageES/Windows.SDL.Sources.Analysis.OS.Undocked.yml@templates_onebranch + parameters: + globalsdl: + tsa: + # Not filing bugs for issues found by the scans just yet. + #enabled: true + enabled: false + codeql: + # We currently don't disable other scans that are enabled by default. Just ensure that + # scans that we are interested in are enabled. + psscriptanalyzer: + enable: true + break: true + credscan: + enable: true + break: true + policheck: + enable: true + break: true + antimalwarescan: + enable: true + break: true + armory: + enable: true + break: true + eslint: + enable: true + break: true + + - task: Flawfinder@2 + displayName: 'Run Flawfinder' + inputs: + targetFlawfinderPattern: '$(Build.SourcesDirectory)' + minRiskLevel: 2 + onlyInputs: false + +- stage: Build_SDK + jobs: + - job: Build_SDK + steps: + - task: NuGetToolInstaller@1 + + - task: NuGetAuthenticate@0 + inputs: + nuGetServiceConnections: 'DevHomeInternal' + + - task: PowerShell@2 + displayName: Build SDK + name: BuildSDKCommand + inputs: + filePath: 'Build.ps1' + arguments: -Configuration "Release" -SDKVersion $(SDKVersion) -BuildStep "sdk" -IsAzurePipelineBuild + + - task: PublishPipelineArtifact@0 + displayName: Publish Artifacts + inputs: + artifactName: SdkNugetPackage + targetPath: pluginsdk\_build + + - task: PublishSymbols@2 + displayName: Publish Symbols + inputs: + SearchPattern: '**/bin/**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + SymbolsProduct: DevHomeSDK + +- stage: Build_DevHome + dependsOn: Build_SDK + variables: + SDKVersion: $[stageDependencies.Build_SDK.Build_SDK.outputs['BuildSDKCommand.SDKVersion']] + jobs: + - ${{ each configuration in parameters.Configurations }}: + - ${{ each platform in parameters.Platforms }}: + - job: Build_${{ platform }}_${{ configuration }} + steps: + - task: NuGetToolInstaller@1 + + - task: NuGetAuthenticate@0 + inputs: + nuGetServiceConnections: 'DevHomeInternal' + + - task: PowerShell@2 + displayName: Replace Stubbed Files + inputs: + filePath: 'build/scripts/Unstub.ps1' + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'SdkNugetPackage' + targetPath: '$(Pipeline.Workspace)\sdkArtifacts\' + + - task: PowerShell@2 + displayName: Build Dev Home + inputs: + filePath: 'Build.ps1' + arguments: -Platform "${{ platform }}" -Configuration "${{ configuration }}" -Version $(MSIXVersion) -SDKVersion $(SDKVersion) -SDKNugetSource $(Pipeline.Workspace)\sdkArtifacts\ -BuildStep "msix" -IsAzurePipelineBuild + + - task: EsrpCodeSigning@2 + inputs: + ConnectedServiceName: 'Xlang Code Signing' + FolderPath: '$(appxPackageDir)\${{ configuration }}' + Pattern: '*.msix' + signConfigType: 'inlineSignParams' + inlineOperation: | + [ + { + "keycode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd sha256" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + } + ] + SessionTimeout: '60' + MaxConcurrency: '50' + MaxRetryAttempts: '5' + + - task: PublishPipelineArtifact@0 + displayName: Publish MSIX Artifacts + inputs: + artifactName: msix_${{ platform }}_${{ configuration }} + targetPath: $(appxPackageDir)\${{ configuration }} + + - task: AzureKeyVault@1 + inputs: + azureSubscription: 'DevHomeAzureServiceConnection' + KeyVaultName: 'DevHomeKeyVault' + SecretsFilter: 'ApiScanConnectionString' + RunAsPreJob: false + + - task: APIScan@2 + inputs: + softwareFolder: '$(Build.StagingDirectory)' + softwareName: 'Dev Home' + softwareVersionNum: '1.0' + softwareBuildNum: '$(Build.BuildId)' + symbolsFolder: 'SRV*http://symweb' + env: + AzureServicesAuthConnectionString: $(ApiScanConnectionString) + + - task: Windows Application Driver@0 + condition: and(always(), ne('${{ platform}}', 'arm64')) + inputs: + OperationType: 'Start' + + - task: PowerShell@2 + displayName: 'Run Unittests' + condition: ne('${{ platform}}', 'arm64') + inputs: + filePath: 'Test.ps1' + arguments: -Platform "${{ platform }}" -Configuration "${{ configuration }}" -IsAzurePipelineBuild + + - task: Windows Application Driver@0 + condition: and(always(), ne('${{ platform}}', 'arm64')) + inputs: + OperationType: 'Stop' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Test Results' + condition: and(always(), ne('${{ platform}}', 'arm64')) + inputs: + PathtoPublish: $(testOutputArtifactDir) + artifactName: TestResults + + - task: PublishTestResults@2 + displayName: 'Add Test Results to ADO' + condition: and(always(), ne('${{ platform}}', 'arm64')) + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '*.trx' + searchFolder: '$(testOutputArtifactDir)' + mergeTestResults: true + failTaskOnFailedTests: true + testRunTitle: '$(Agent.JobName)' + buildPlatform: '${{ platform }}' + buildConfiguration: '${{ configuration }}' + + - task: PublishSymbols@2 + displayName: Publish Symbols + inputs: + SearchPattern: '**/bin/**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + SymbolsProduct: DevHome + +- stage: Build_MsixBundle + dependsOn: Build_DevHome + jobs: + - job: Build_MsixBundles + steps: + - ${{ each configuration in parameters.Configurations }}: + - ${{ each platform in parameters.Platforms }}: + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: msix_${{ platform }}_${{ configuration }} + targetPath: $(appxPackageDir)\${{ configuration }} + + - task: PowerShell@2 + displayName: Build MsixBundle + inputs: + filePath: 'Build.ps1' + arguments: -Configuration "${{ configuration }}" -Version $(MSIXVersion) -BuildStep "msixbundle" -IsAzurePipelineBuild + + - task: EsrpCodeSigning@2 + inputs: + ConnectedServiceName: 'Xlang Code Signing' + FolderPath: 'AppxBundles\${{ configuration }}' + Pattern: '*.msixbundle' + signConfigType: 'inlineSignParams' + inlineOperation: | + [ + { + "keycode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd sha256" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + } + ] + SessionTimeout: '60' + MaxConcurrency: '50' + MaxRetryAttempts: '5' + + - task: PublishPipelineArtifact@0 + displayName: Publish MSIX Bundle Artifacts + inputs: + artifactName: MsixBundle_${{ configuration }} + targetPath: AppxBundles\${{ configuration }} \ No newline at end of file diff --git a/build/scripts/CertSignAndInstall.ps1 b/build/scripts/CertSignAndInstall.ps1 new file mode 100644 index 0000000000..adf7e0f17f --- /dev/null +++ b/build/scripts/CertSignAndInstall.ps1 @@ -0,0 +1,44 @@ +function Invoke-SignPackage([string]$Path) { + if (-not($Path)) { + Write-Host "Path parameter cannot be empty" + return + } + + if (-not(Test-Path $Path -PathType Leaf)) { + Write-Host $Path is not a valid file + return + } + + $certName = "Microsoft.DevHome" + $cert = Get-ChildItem 'Cert:\CurrentUser\My' | Where-Object {$_.FriendlyName -match $certName} | Select-Object -First 1 + + if ($cert) { + $expiration = $cert.NotAfter + $now = Get-Date + if ( $expiration -lt $now) + { + Write-Host "Test certificate for $($cert.Thumbprint)...Expired ($expiration). Replacing it with a new one." + Remove-Item $cert + $cert = $Null + } + } + + if (-not($cert)) { + Write-Host "No certificate found. Creating a new certificate for signing." + $cert = & New-SelfSignedCertificate -Type Custom -Subject "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" -KeyUsage DigitalSignature -FriendlyName $certName -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") + } + + SignTool sign /fd SHA256 /sha1 $($cert.Thumbprint) $Path + + if (-not(Test-Path Cert:\LocalMachine\TrustedPeople\$($cert.Thumbprint))) { + Export-Certificate -Cert $cert -FilePath "$($PSScriptRoot)\Microsoft.DevHome.cer" -Type CERT + Import-Certificate -FilePath "$($PSScriptRoot)\Microsoft.DevHome.cer" -CertStoreLocation Cert:\LocalMachine\TrustedPeople + Remove-Item -Path "$($PSScriptRoot)\Microsoft.DevHome.cer" + (Get-ChildItem Cert:\LocalMachine\TrustedPeople\$($cert.Thumbprint)).FriendlyName = $certName + } +} + +function Remove-DevHomeCertificates() { + Get-ChildItem 'Cert:\CurrentUser\My' | Where-Object {$_.FriendlyName -match 'Microsoft.DevHome'} | Remove-Item + Get-ChildItem 'Cert:\LocalMachine\TrustedPeople' | Where-Object {$_.FriendlyName -match 'Microsoft.DevHome'} | Remove-Item +} \ No newline at end of file diff --git a/build/scripts/Create-AppxBundle.ps1 b/build/scripts/Create-AppxBundle.ps1 new file mode 100644 index 0000000000..69556920f5 --- /dev/null +++ b/build/scripts/Create-AppxBundle.ps1 @@ -0,0 +1,58 @@ +Param( + [Parameter(Mandatory, + HelpMessage="Base name for input .appx files")] + [string] + $ProjectName, + + [Parameter(Mandatory, + HelpMessage="Appx Bundle Version")] + [version] + $BundleVersion, + + [Parameter(Mandatory, + HelpMessage="Path under which to locate appx/msix files")] + [string] + $InputPath, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath, + + [Parameter(HelpMessage="Path to makeappx.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakeAppxPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x86\MakeAppx.exe" +) + +If ($null -Eq (Get-Item $MakeAppxPath -EA:SilentlyContinue)) { + Write-Error "Could not find MakeAppx.exe at `"$MakeAppxPath`".`nMake sure that -MakeAppxPath points to a valid SDK." + Exit 1 +} + +# Enumerates a set of appx files beginning with a project name +# and generates a temporary file containing a bundle content map. +Function Create-AppxBundleMapping { + Param( + [Parameter(Mandatory)] + [string] + $InputPath, + + [Parameter(Mandatory)] + [string] + $ProjectName + ) + + $lines = @("[Files]") + Get-ChildItem -Path:$InputPath -Recurse -Filter:*$ProjectName* -Include *.appx, *.msix | % { + $lines += ("`"{0}`" `"{1}`"" -f ($_.FullName, $_.Name)) + } + + $outputFile = New-TemporaryFile + $lines | Out-File -Encoding:ASCII $outputFile + $outputFile +} + +$NewMapping = Create-AppxBundleMapping -InputPath:$InputPath -ProjectName:$ProjectName + +& $MakeAppxPath bundle /v /bv $BundleVersion.ToString() /f $NewMapping.FullName /p $OutputPath diff --git a/build/scripts/CreateBuildInfo.ps1 b/build/scripts/CreateBuildInfo.ps1 new file mode 100644 index 0000000000..1ca098f662 --- /dev/null +++ b/build/scripts/CreateBuildInfo.ps1 @@ -0,0 +1,58 @@ +[CmdLetBinding()] +Param( + [string]$Version, + [bool]$IsSdkVersion = $false, + [bool]$IsAzurePipelineBuild = $false +) + +$Major = "0" +$Minor = "0" +$Patch = "99" # default to 99 for local builds + +$versionSplit = $Version.Split("."); +if ($versionSplit.length -gt 3) { $Build = $versionSplit[3] } +if ($versionSplit.length -gt 2) { $Elapsed = $versionSplit[2] } +if ($versionSplit.length -gt 1) { + if ($versionSplit[1].length -gt 2) { + $Minor = $versionSplit[1].SubString(0,$versionSplit[1].length-2); + $Patch = $versionSplit[1].SubString($versionSplit[1].length-2, 2); + } else { + $Minor = $versionSplit[1] + } +} +if ($versionSplit.length -gt 0) { $Major = $versionSplit[0] } + +# Compute/Verify the MSIX version +# +# MSIX Version = M.NPP.E.B +# where +# M = Major (max <= 65535) +# N = Minor (max <= 654) +# P = Patch (max <= 99) +# E = Elapsed (max <= 65535) +# B = Build (max <= 65535) +# +# NOTE: Elapsed is the number of days since the epoch (Jan'1, 2021). +# NOTE: Make sure to compute Elapsed using Universal Time (UTC). +# +# NOTE: Build is computed as HHMM i.e. the time (hour and minute) when building locally, 0 otherwise. +# Build is not included when building the sdk nupkg. +# +$epoch = (Get-Date -Year 2023 -Month 1 -Day 1).ToUniversalTime() +$now = (Get-Date).ToUniversalTime() +if ([string]::IsNullOrWhiteSpace($Elapsed)) { + $Elapsed = $(New-Timespan -Start $epoch -End $now).Days +} +# +$version_h = $now.Hour +$version_m = $now.Minute +if (-not($IsAzurePipelineBuild) -And [string]::IsNullOrWhiteSpace($Build)) { + $Build = ($version_h * 100 + $version_m).ToString() +} +# +if ($IsSdkVersion) { + $version_dotquad = [int[]]($Major, ($Minor + $Patch), $Elapsed) +} else { + $version_dotquad = [int[]]($Major, ($Minor + $Patch), $Elapsed, $Build) +} +return ($version_dotquad -Join ".") \ No newline at end of file diff --git a/build/scripts/Unstub.ps1 b/build/scripts/Unstub.ps1 new file mode 100644 index 0000000000..8a0381ebf6 --- /dev/null +++ b/build/scripts/Unstub.ps1 @@ -0,0 +1,21 @@ +# This script unstubs the telemetry at build time and replaces the stubbed file with a reference internal nuget package + +Remove-Item "$($PSScriptRoot)\..\..\telemetry\DevHome.Telemetry\TelemetryEventSource.cs" + +$projFile = "$($PSScriptRoot)\..\..\telemetry\DevHome.Telemetry\DevHome.Telemetry.csproj" +$projFileContent = Get-Content $projFile -Encoding UTF8 -Raw + +if ($projFileContent.Contains('Microsoft.Telemetry.Inbox.Managed')) { + Write-Output "Project file already contains a reference to the internal package." + return; +} + +$xml = [xml]$projFileContent +$xml.PreserveWhitespace = $true +$packageRef = $xml.SelectSingleNode("//ItemGroup/PackageReference") +$newNode = $packageRef.Clone() +$newNode.Include="Microsoft.Telemetry.Inbox.Managed" +$newNode.Version="10.0.25148.1001-220626-1600.rs-fun-deploy-dev5" +$parentNode = $packageRef.ParentNode +$parentNode.AppendChild($newNode) +$xml.Save($projFile) \ No newline at end of file diff --git a/codeAnalysis/GlobalSuppressions.cs b/codeAnalysis/GlobalSuppressions.cs new file mode 100644 index 0000000000..d269e13471 --- /dev/null +++ b/codeAnalysis/GlobalSuppressions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:ClosingParenthesisMustBeSpacedCorrectly", Justification = "All current violations are due to Tuple shorthand and so valid.")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:ClosingSquareBracketsMustBeSpacedCorrectly", Justification = "Optional arrays need to be supported. Ex []?")] + +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "We follow the C# Core Coding Style which avoids using `this` unless absolutely necessary.")] + +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "We follow the C# Core Coding Style which puts using statements outside the namespace.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "It is not a priority and have hight impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "It is not a priority and have hight impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1203:ConstantsMustAppearBeforeFields", Justification = "It is not a priority and have hight impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:StaticElementsMustAppearBeforeInstanceElements", Justification = "It is not a priority and have hight impact in code changes.")] + +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:FieldNamesMustNotBeginWithUnderscore", Justification = "We follow the C# Core Coding Style which uses underscores as prefixes rather than using `this.`.")] + +[assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001:XmlCommentAnalysisDisabled", Justification = "Not enabled as we don't want or need XML documentation.")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1629:DocumentationTextMustEndWithAPeriod", Justification = "Not enabled as we don't want or need XML documentation.")] + +[assembly: SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Scope = "member", Target = "Microsoft.Templates.Core.Locations.TemplatesSynchronization.#SyncStatusChanged", Justification = "Using an Action does not allow the required notation")] + +// Non general suppressions +[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "This is part of the markdown processing", MessageId = "System.Windows.Documents.Run.#ctor(System.String)", Scope = "member", Target = "Microsoft.Templates.UI.Controls.Markdown.#ImageInlineEvaluator(System.Text.RegularExpressions.Match)")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Core.ITemplateInfoExtensions.#GetQueryableProperties(Microsoft.TemplateEngine.Abstractions.ITemplateInfo)")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Core.Composition.CompositionQuery.#Match(System.Collections.Generic.IEnumerable`1,Microsoft.Templates.Core.Composition.QueryablePropertyDictionary)")] +[assembly: SuppressMessage("Usage", "VSTHRD103:Call async methods when in an async method", Justification = "Resource DictionaryWriter does not implement flush async", Scope = "member", Target = "~M:Microsoft.Templates.Core.PostActions.Catalog.Merge.MergeResourceDictionaryPostAction.ExecuteInternalAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Used in a lot of places for meaningful method names")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Static methods may improve performance but decrease maintainability")] +[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Renaming everything would be a lot of work. It does not do any harm if an EventHandler delegate ends with the suffix EventHandler. Besides this, the Rule causes some false positives.")] +[assembly: SuppressMessage("Performance", "CA1838:Avoid 'StringBuilder' parameters for P/Invokes", Justification = "We are not concerned about the performance impact of marshaling a StringBuilder")] + +// Threading suppressions +[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.Controls.Notification.OnClose")] +[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel.OnDelete")] +[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.WizardNavigation.GoBack")] +[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.WizardNavigation.GoForward")] +[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel.OnDelete(Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel)")] + +// Localization suppressions +[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#CreateJunction(System.String,System.String,System.Boolean)", Justification = "Only used for local generation")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#DeleteJunction(System.String)", Justification = "Only used for local generation")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#InternalGetTarget(Microsoft.Win32.SafeHandles.SafeFileHandle)", Justification = "Only used for local generation")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#OpenReparsePoint(System.String,Microsoft.Templates.Core.Locations.JunctionNativeMethods+EFileAccess)", Justification = "Only used for local generation")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Windows.Documents.InlineCollection.Add(System.String)", Scope = "member", Target = "Microsoft.Templates.UI.Extensions.TextBlockExtensions.#OnSequentialFlowStepChanged(System.Windows.DependencyObject,System.Windows.DependencyPropertyChangedEventArgs)", Justification = "No text here")] + +// Uninstantiated TestFixture classes +[assembly: SuppressMessage("Microsoft.Performance", "CA1812: Avoid uninstantiated internal classes", Scope = "module", Justification = "CA1812 will be thrown for every file in the test project. This is mentioned here: dotnet/roslyn-analyzers#1830")] + +// Code quality +[assembly: SuppressMessage("CodeQuality", "IDE0076:Invalid global 'SuppressMessageAttribute'", Justification = "Affect predefined suppressions.")] + +// Generated code +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1636:FileHeaderCopyrightTextMustMatch", Justification = "CommunityToolkit generated files are causing analyzer error because the file header does not match the expected value.")] diff --git a/codeAnalysis/Rules.ruleset b/codeAnalysis/Rules.ruleset new file mode 100644 index 0000000000..5d240cbc30 --- /dev/null +++ b/codeAnalysis/Rules.ruleset @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/codeAnalysis/StyleCop.json b/codeAnalysis/StyleCop.json new file mode 100644 index 0000000000..de9eda9009 --- /dev/null +++ b/codeAnalysis/StyleCop.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Microsoft Corporation", + "copyrightText": "Copyright (c) {companyName} and Contributors\r\nLicensed under the MIT license.", + "xmlHeader": false, + "headerDecoration": "", + "fileNamingConvention": "metadata", + "documentInterfaces": false, + "documentExposedElements": false, + "documentInternalElements": false + }, + "layoutRules": { + "newlineAtEndOfFile": "require" + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace" + } + } +} \ No newline at end of file diff --git a/codeAnalysis/format_sources.ps1 b/codeAnalysis/format_sources.ps1 new file mode 100644 index 0000000000..97a95c0e53 --- /dev/null +++ b/codeAnalysis/format_sources.ps1 @@ -0,0 +1,54 @@ +param ( + [switch]$all = $false +) + +if(!(Get-Command "git" -ErrorAction SilentlyContinue)) { + throw "You need to have a git in path to be able to format only the dirty files!" +} + +$clangFormat = "clang-format.exe" +if(!(Get-Command $clangFormat -ErrorAction SilentlyContinue)) { + Write-Information "Can't find clang-format.exe in %PATH%, trying to use %VCINSTALLDIR%..." + $clangFormat="$env:VCINSTALLDIR\Tools\Llvm\bin\clang-format.exe" + if(!(Test-Path -Path $clangFormat -PathType leaf)) { + throw "Can't find clang-format.exe executable. Make sure you either have it in %PATH% or run this script from vcvars.bat!" + } +} + +$sourceExtensions = New-Object System.Collections.Generic.HashSet[string] +$sourceExtensions.Add(".cpp") | Out-Null +$sourceExtensions.Add(".h") | Out-Null + +function Get-Dirty-Files-From-Git() { + $repo_root = & git rev-parse --show-toplevel + + $staged = & git diff --name-only --diff-filter=d --cached | % { $repo_root + "/" + $_ } + $unstaged = & git ls-files -m + $untracked = & git ls-files --others --exclude-standard + $result = New-Object System.Collections.Generic.List[string] + $staged, $unstaged, $untracked | % { + $_.Split(" ") | + where {Test-Path $_ -PathType Leaf} | + where {$sourceExtensions.Contains((Get-Item $_).Extension)} | + foreach {$result.Add($_)} + } + return $result +} + +if($all) { + $filesToFormat = + Get-ChildItem -Recurse -File ..\src | + Resolve-Path -Relative | + where { (Get-Item $_).Directory -notmatch "(Generated Files)|node_modules" -And + $sourceExtensions.Contains((Get-Item $_).Extension)} +} +else { + $filesToFormat = Get-Dirty-Files-From-Git +} + +$filesToFormat | % { + Write-Host "Formatting $_" + & $clangFormat -i -style=file -fallback-style=none $_ 2>&1 +} + +Write-Host "Done!" \ No newline at end of file diff --git a/common/Behaviors/NavigationViewHeaderBehavior.cs b/common/Behaviors/NavigationViewHeaderBehavior.cs new file mode 100644 index 0000000000..cdf3893054 --- /dev/null +++ b/common/Behaviors/NavigationViewHeaderBehavior.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.Xaml.Interactivity; + +namespace DevHome.Common.Behaviors; + +public class NavigationViewHeaderBehavior : Behavior +{ + private static NavigationViewHeaderBehavior? _current; + + private Page? _currentPage; + + public DataTemplate? DefaultHeaderTemplate + { + get; set; + } + + public object DefaultHeader + { + get => GetValue(DefaultHeaderProperty); + set => SetValue(DefaultHeaderProperty, value); + } + + public static readonly DependencyProperty DefaultHeaderProperty = + DependencyProperty.Register("DefaultHeader", typeof(object), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(null, (d, e) => _current!.UpdateHeader())); + + public static NavigationViewHeaderMode GetHeaderMode(Page item) => (NavigationViewHeaderMode)item.GetValue(HeaderModeProperty); + + public static void SetHeaderMode(Page item, NavigationViewHeaderMode value) => item.SetValue(HeaderModeProperty, value); + + public static readonly DependencyProperty HeaderModeProperty = + DependencyProperty.RegisterAttached("HeaderMode", typeof(bool), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(NavigationViewHeaderMode.Always, (d, e) => _current!.UpdateHeader())); + + public static object GetHeaderContext(Page item) => item.GetValue(HeaderContextProperty); + + public static void SetHeaderContext(Page item, object value) => item.SetValue(HeaderContextProperty, value); + + public static readonly DependencyProperty HeaderContextProperty = + DependencyProperty.RegisterAttached("HeaderContext", typeof(object), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(null, (d, e) => _current!.UpdateHeader())); + + public static DataTemplate GetHeaderTemplate(Page item) => (DataTemplate)item.GetValue(HeaderTemplateProperty); + + public static void SetHeaderTemplate(Page item, DataTemplate value) => item.SetValue(HeaderTemplateProperty, value); + + public static readonly DependencyProperty HeaderTemplateProperty = + DependencyProperty.RegisterAttached("HeaderTemplate", typeof(DataTemplate), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(null, (d, e) => _current!.UpdateHeaderTemplate())); + + protected override void OnAttached() + { + base.OnAttached(); + + var navigationService = Application.Current.GetService(); + navigationService.Navigated += OnNavigated; + + _current = this; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + + var navigationService = Application.Current.GetService(); + navigationService.Navigated -= OnNavigated; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame && frame.Content is Page page) + { + _currentPage = page; + + UpdateHeader(); + UpdateHeaderTemplate(); + } + } + + private void UpdateHeader() + { + if (_currentPage != null) + { + var headerMode = GetHeaderMode(_currentPage); + if (headerMode == NavigationViewHeaderMode.Never) + { + AssociatedObject.Header = null; + AssociatedObject.AlwaysShowHeader = false; + } + else + { + var headerFromPage = GetHeaderContext(_currentPage); + if (headerFromPage != null) + { + AssociatedObject.Header = headerFromPage; + } + else + { + AssociatedObject.Header = DefaultHeader; + } + + if (headerMode == NavigationViewHeaderMode.Always) + { + AssociatedObject.AlwaysShowHeader = true; + } + else + { + AssociatedObject.AlwaysShowHeader = false; + } + } + } + } + + private void UpdateHeaderTemplate() + { + if (_currentPage != null) + { + var headerTemplate = GetHeaderTemplate(_currentPage); + AssociatedObject.HeaderTemplate = headerTemplate ?? DefaultHeaderTemplate; + } + } +} diff --git a/common/Behaviors/NavigationViewHeaderMode.cs b/common/Behaviors/NavigationViewHeaderMode.cs new file mode 100644 index 0000000000..e3b30c9d77 --- /dev/null +++ b/common/Behaviors/NavigationViewHeaderMode.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Common.Behaviors; + +public enum NavigationViewHeaderMode +{ + Always, + Never, + Minimal, +} diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj new file mode 100644 index 0000000000..b99c8c52c7 --- /dev/null +++ b/common/DevHome.Common.csproj @@ -0,0 +1,29 @@ + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.Common + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + enable + true + 0.100.* + + + + + + + + + + + + + + + + + + + diff --git a/common/Extensions/ApplicationExtensions.cs b/common/Extensions/ApplicationExtensions.cs new file mode 100644 index 0000000000..e20f022792 --- /dev/null +++ b/common/Extensions/ApplicationExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Common.Services; +using Microsoft.UI.Xaml; + +namespace DevHome.Common.Extensions; + +/// +/// Extension class implementing extension methods for . +/// +public static class ApplicationExtensions +{ + /// + /// Get registered services at the application level from anywhere in the + /// application. + /// + /// Note: + /// https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current + /// "Application is a singleton that implements the static Current property + /// to provide shared access to the Application instance for the current + /// application. The singleton pattern ensures that state managed by + /// Application, including shared resources and properties, is available + /// from a single, shared location." + /// + /// Example of usage: + /// + /// Application.Current.GetService() + /// + /// + /// Service type. + /// Current application. + /// Service reference. + public static T GetService(this Application application) + where T : class + { + return (application as IApp)!.GetService(); + } +} diff --git a/common/Extensions/IEnumerableExtensions.cs b/common/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000000..52414413e3 --- /dev/null +++ b/common/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; + +namespace DevHome.Common.Extensions; +public static class IEnumerableExtensions +{ + /// + /// Creates a readonly collection for the enumerable input. + /// + /// Readonly collection. + public static ReadOnlyCollection ToReadOnlyCollection(this IEnumerable items) + { + return new ReadOnlyCollectionBuilder(items).ToReadOnlyCollection(); + } +} diff --git a/common/Extensions/IHostExtensions.cs b/common/Extensions/IHostExtensions.cs new file mode 100644 index 0000000000..0eccd1436a --- /dev/null +++ b/common/Extensions/IHostExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace DevHome.Common.Extensions; +public static class IHostExtensions +{ + /// + /// + /// + public static T CreateInstance(this IHost host, params object[] parameters) + { + return ActivatorUtilities.CreateInstance(host.Services, parameters); + } + + /// + /// Gets the service object for the specified type, or throws an exception + /// if type was not registered. + /// + /// Service type + /// Host object + /// Service object + /// Throw an exception if the specified + /// type is not registered + public static T GetService(this IHost host) + where T : class + { + if (host.Services.GetService(typeof(T)) is not T service) + { + throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices."); + } + + return service; + } +} diff --git a/common/Extensions/WindowExExtensions.cs b/common/Extensions/WindowExExtensions.cs new file mode 100644 index 0000000000..b53da67b30 --- /dev/null +++ b/common/Extensions/WindowExExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.UI.Xaml.Controls; +using WinUIEx; + +namespace DevHome.Common.Extensions; + +/// +/// This class add extension methods to the class. +/// +public static class WindowExExtensions +{ + /// + /// Show an error message on the window. + /// + /// Target window. + /// Dialog title. + /// Dialog content. + /// Close button text. + public static async Task ShowErrorMessageDialogAsync(this WindowEx window, string title, string content, string buttonText) + { + await window.ShowMessageDialogAsync(dialog => + { + dialog.Title = title; + dialog.Content = new TextBlock() + { + Text = content, + TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords, + }; + dialog.PrimaryButtonText = buttonText; + }); + } + + /// + /// Generic implementation for creating and displaying a message dialog on + /// a window. + /// + /// This extension method overloads . + /// + /// Target window. + /// Action performed on the created dialog. + private static async Task ShowMessageDialogAsync(this WindowEx window, Action action) + { + var dialog = new ContentDialog() + { + XamlRoot = window.Content.XamlRoot, + }; + action(dialog); + await dialog.ShowAsync(); + } +} diff --git a/common/Models/PluginAdaptiveCard.cs b/common/Models/PluginAdaptiveCard.cs new file mode 100644 index 0000000000..b2952441e1 --- /dev/null +++ b/common/Models/PluginAdaptiveCard.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.Windows.DevHome.SDK; + +namespace DevHome.Common.Models; +public class PluginAdaptiveCard : IPluginAdaptiveCard +{ + public event EventHandler? UiUpdate; + + public string Data { get; private set; } + + public string State { get; private set; } + + public string Template { get; private set; } + + public PluginAdaptiveCard() + { + Template = new JsonObject().ToJsonString(); + Data = new JsonObject().ToJsonString(); + State = string.Empty; + } + + public void Update(string template, string data, string state) + { + var parseResult = AdaptiveCard.FromJsonString(template); + if (parseResult.AdaptiveCard is null) + { + throw new ArgumentException(JsonSerializer.Serialize(parseResult.Errors)); + } + + Template = template; + Data = data; + State = state; + + if (UiUpdate is not null) + { + UiUpdate.Invoke(this, parseResult.AdaptiveCard); + } + } +} diff --git a/common/PluginHelpers.cs b/common/PluginHelpers.cs new file mode 100644 index 0000000000..54243c764a --- /dev/null +++ b/common/PluginHelpers.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevHome.Common; + +public enum ExtensionType +{ + DevDoctor, + Settings, + Setup, + Widget, +} diff --git a/common/Services/IApp.cs b/common/Services/IApp.cs new file mode 100644 index 0000000000..59c54907c8 --- /dev/null +++ b/common/Services/IApp.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Common.Services; + +/// +/// Interface for the current application singleton object exposing the API +/// that can be accessed from anywhere in the application. +/// +public interface IApp +{ + /// + /// Gets services registered at the application level. + /// + public T GetService() + where T : class; +} diff --git a/common/Services/INavigationService.cs b/common/Services/INavigationService.cs new file mode 100644 index 0000000000..2242acde8a --- /dev/null +++ b/common/Services/INavigationService.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.Common.Services; + +public interface INavigationService +{ + event NavigatedEventHandler Navigated; + + bool CanGoBack + { + get; + } + + Frame? Frame + { + get; set; + } + + bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false); + + bool GoBack(); +} diff --git a/common/Services/IPluginService.cs b/common/Services/IPluginService.cs new file mode 100644 index 0000000000..fe00dcd404 --- /dev/null +++ b/common/Services/IPluginService.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevHome.Common.Services; +public interface IPluginService +{ + Task> GetInstalledPluginsAsync(); + + Task> GetInstalledPluginsAsync(Microsoft.Windows.DevHome.SDK.ProviderType providerType); +} diff --git a/common/Services/IPluginWrapper.cs b/common/Services/IPluginWrapper.cs new file mode 100644 index 0000000000..f8db02a208 --- /dev/null +++ b/common/Services/IPluginWrapper.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.UI.Xaml.Documents; +using Microsoft.Windows.DevHome.SDK; +using WinRT; + +namespace DevHome.Common.Services; +public interface IPluginWrapper +{ + string Name + { + get; + } + + bool IsRunning(); + + Task StartPlugin(); + + void Kill(); + + IPlugin? GetPluginObject(); + + void AddProviderType(ProviderType providerType); + + bool HasProviderType(ProviderType providerType); +} diff --git a/common/Services/IStringResource.cs b/common/Services/IStringResource.cs new file mode 100644 index 0000000000..0571271940 --- /dev/null +++ b/common/Services/IStringResource.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Common.Services; + +public interface IStringResource +{ + public string GetLocalized(string key, params object[] args); +} diff --git a/common/Services/IThemeSelectorService.cs b/common/Services/IThemeSelectorService.cs new file mode 100644 index 0000000000..ce0f737ef7 --- /dev/null +++ b/common/Services/IThemeSelectorService.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Threading.Tasks; +using Microsoft.UI.Xaml; + +namespace DevHome.Contracts.Services; + +public interface IThemeSelectorService +{ + ElementTheme Theme + { + get; + } + + Task InitializeAsync(); + + Task SetThemeAsync(ElementTheme theme); + + Task SetRequestedThemeAsync(); +} diff --git a/common/Services/StringResource.cs b/common/Services/StringResource.cs new file mode 100644 index 0000000000..b3cd7c8c99 --- /dev/null +++ b/common/Services/StringResource.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Globalization; +using Windows.ApplicationModel.Resources; + +namespace DevHome.Common.Services; + +public class StringResource : IStringResource +{ + private readonly ResourceLoader _resourceLoader; + + /// + /// Initializes a new instance of the class. + /// + /// + public StringResource() + { + _resourceLoader = new ResourceLoader(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// fsa + public StringResource(string name) + { + _resourceLoader = new ResourceLoader(name); + } + + /// + /// Gets the localized string of a resource key. + /// + /// Resource key. + /// Placeholder arguments. + /// Localized value, or resource key if the value is empty or an exception occurred. + public string GetLocalized(string key, params object[] args) + { + string value; + + try + { + value = _resourceLoader.GetString(key); + value = string.Format(CultureInfo.CurrentCulture, value, args); + } + catch + { + value = string.Empty; + } + + return string.IsNullOrEmpty(value) ? key : value; + } +} diff --git a/common/ToolPage.cs b/common/ToolPage.cs new file mode 100644 index 0000000000..665e910225 --- /dev/null +++ b/common/ToolPage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Common; + +public abstract class ToolPage : Page +{ + public abstract string ShortName { get; } +} diff --git a/common/Views/PluginAdaptiveCardPanel.cs b/common/Views/PluginAdaptiveCardPanel.cs new file mode 100644 index 0000000000..2f1ddfd1d7 --- /dev/null +++ b/common/Views/PluginAdaptiveCardPanel.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using DevHome.Common.Models; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; +using Newtonsoft.Json; + +namespace DevHome.Common.Views; + +// XAML element to contain a single instance of plugin UI. +// Use this element where plugin UI is expected to pop up. +// TODO: Should ideally not allow external children to be added through the `Children` property. +public class PluginAdaptiveCardPanel : StackPanel +{ + public event EventHandler? UiUpdate; + + public void Bind(IPluginAdaptiveCardController pluginAdaptiveCardController) + { + var adaptiveCardRenderer = new AdaptiveCardRenderer(); + + if (Children.Count != 0) + { + throw new ArgumentException("The PluginUI element must be binded to an empty container."); + } + + var uiDispatcher = DispatcherQueue.GetForCurrentThread(); + var pluginUI = new PluginAdaptiveCard(); + + pluginUI.UiUpdate += (object? sender, AdaptiveCard adaptiveCard) => + { + uiDispatcher.TryEnqueue(() => + { + var renderedAdaptivecard = adaptiveCardRenderer.RenderAdaptiveCard(adaptiveCard); + renderedAdaptivecard.Action += (RenderedAdaptiveCard? sender, AdaptiveActionEventArgs args) => + { + pluginAdaptiveCardController.OnAction(JsonConvert.SerializeObject(args.Action), JsonConvert.SerializeObject(args.Inputs)); + }; + + Children.Clear(); + Children.Add(renderedAdaptivecard.FrameworkElement); + + if (this.UiUpdate != null) + { + this.UiUpdate.Invoke(this, renderedAdaptivecard.FrameworkElement); + } + }); + }; + + pluginAdaptiveCardController.Initialize(pluginUI); + } +} diff --git a/docs/common.md b/docs/common.md new file mode 100644 index 0000000000..0c12d7246e --- /dev/null +++ b/docs/common.md @@ -0,0 +1,4 @@ +# Classes and structures + +#### class ToolPage: [header](/common/ToolPage.cs) +Interface for framework to discover and enable tools. \ No newline at end of file diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 0000000000..bc064e6d1f --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,72 @@ +# ExtensionHost definition + +DevHome is an app extension host with four possible extensions. Plugins (app extensions) may connect +to any subset of extensions as needed. Below is the appxmanifest code and description of each extension point. + +```xml + + + com.microsoft.DevHome.devdoctor + com.microsoft.DevHome.settings + com.microsoft.DevHome.setup + com.microsoft.DevHome.widget + + +``` + +## devdoctor + +Not Yet Implemented + +Will enable a plugin to add rules and UI (via Adaptive Cards) to dev doctor. + +## settings + +Not Yet Implemented + +Will enable a plugin to add UI through Adaptive Cards to the Dev Home settings page. + +## setup + +Not Yet Implemented + +Will enable a plugin to add UI through Adaptive Cards to the Dev Home setup flow. + +## widget + +Not Yet Implemented + +Will enable a plugin to add Widgets that can be added to the Dashboard. + +# Runtime logic + +At startup, Dev Home iterates the app catalog for any extensions and adds them to a dictionary. +```cs +private readonly IDictionary> _extensions = new Dictionary>(); +``` + +Any internal tools can retrieve a readonly list of extensions for a given extension point via: +```cs +var extensionService = App.GetService(); +var extensions = extensionService.GetExtensions("widget"); +``` + +# Extensions definition + +Here's an example of how a plugin can register to be a Dev Home extension: +```xml + + + + +``` + +# Extension Interface definions + +Not Yet Implemented + +These are being designed right now and will evolve over time before shipping. \ No newline at end of file diff --git a/docs/interface.md b/docs/interface.md new file mode 100644 index 0000000000..da86e304c0 --- /dev/null +++ b/docs/interface.md @@ -0,0 +1,32 @@ +# Interface definition + +```cs +public abstract class ToolPage : Page +{ + public abstract string ShortName { get; } +} +``` + +# Runtime logic + +The Dev Home framework will look at all types in its assembly for any inheriting from ToolPage: + +On a found type, the framework will use: + - [`ShortName`](#ShortName) to get the name of the tool, + +# Method definition + +This section contains a more detailed description of each of the interface methods. + +## ShortName + +```cs +public abstract string ShortName { get; } +``` + +Returns the name of the tool. This is used for the navigation menu text. + +# Code organization + +### [`toolpage.cs`](/Common/ToolPage.cs) +Contains the interface definition for Dev Home tools. \ No newline at end of file diff --git a/docs/navconfig.md b/docs/navconfig.md new file mode 100644 index 0000000000..c81d470e26 --- /dev/null +++ b/docs/navconfig.md @@ -0,0 +1,41 @@ +# NavConfig.json schema + +- navMenu `required` + - Wrapper object for the navigation menu + - Type: `object` + - path: #/navMenu + - **_Properties_** + - groups `required` + - Future: Items in navigation menu can be put into expand/collapsable groupings + - This is not yet supported and all tools are placed in top-level list + - Type: `array` + - path: #/navMenu/groups + - **_Properties_** + - identity `required` + - Unique identity of group + - Type: `string` + - path: #/navMenu/groups/items/identity + - tools `required` + - List of tools in navigation menu + - Type: `array` + - path: #/navMenu/groups/items/tools + - **_Items_** + - Type: `object` + - path: #/navMenu/groups/items/tools/items + - **_Properties_** + - identity `required` + - Unique identity of tool. This will appear in the navigation pane + - Type: `string` + - path: #/navMenu/groups/items/tools/items/identity + - assembly `required` + - Assembly name (dll) that tool is located in + - Type: `string` + - path: #/navMenu/groups/items/tools/items/assembly + - viewFullName `required` + - Full class name of tool's view + - Type: `string` + - path: #/navMenu/groups/items/tools/items/viewFullName + - viewModelFullName `required` + - Full class name of tool's view model + - Type: `string` + - path: #/navMenu/groups/items/tools/items/viewModelFullName \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000000..ad84de9d2c --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,88 @@ +# Dev Documentation + +## Fork, Clone, Branch and Create your PR + +Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed on an approach or a spec has been written and approved, it's time to start development: + +1. Fork the repo if you haven't already +1. Clone your fork locally +1. Create & push a feature branch +1. Work on your changes + +## Rules + +- **Follow the pattern of what you already see in the code.** +- [Coding style](style.md). +- Try to package new ideas/components into libraries that have nicely defined interfaces. +- Package new ideas into classes or refactor existing ideas into a class as you extend. +- When adding new classes/methods/changing existing code: add new unit tests or update the existing tests. + +## Compiling Dev Home + +### Compiling Source Code + +There are two ways to compile locally. + +- Open the Developer Command Prompt for Visual Studio +- Run `Build` from Dev Home's root directory. You can pass in a list of platforms/configurations +- The Dev Home MSIX will be in your repo under `AppxPackages\x64\debug` + +Alternatively + +- Open `DevHome.sln` in Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` or `Debug`, from the `Build` menu choose `Build Solution`. + +## How to create new Tools + +1. Create a new directory with your tool's name under `tools` with three subdirectories `src`, `test`, and `uitest` +1. Create a new `WinUI 3 Class Library` project in your `src` directory +1. Create the `Strings\en-us` directories under `src`. Add `Resources.resw` and include the following code: + ```xml + + [Name of your tool that will appear in navigation menu] + + ``` +1. Add a project reference from `DevHome` to your project +1. Add a project reference from your project to `DevHome.Common` project under [common](\common) +1. Create your XAML view and viewmodel. Your view class must inherit from `ToolPage` and implement requirements. Specifications for the [Dev Home tools API](interface.md). +1. Update [NavConfig.json](\src\NavConfig.json) with your tool. Specifications for the [NavConfig.json schema](navconfig.md). + +Example: +```cs +public partial class SampleToolPage : ToolPage +{ + public override string ShortName => "SampleTool"; +} +``` + +## Implementation details + +### Dev Home framework + +The Dev Home project contains the wrapping framework for the Dev Home application. +It's responsible for: +- Loading the individual Dev Home tools. + + +### [`Interface`](interface.md) + +- Definition of the interface used by Dev Home framework to manage the tools. All tools must implement this interface. + + +### [`DevHome.Common`](common.md) + +The common lib, as the name suggests, contains code shared by multiple tools and the Dev Home framework \ No newline at end of file diff --git a/docs/style.md b/docs/style.md new file mode 100644 index 0000000000..470ab5868b --- /dev/null +++ b/docs/style.md @@ -0,0 +1,12 @@ +# Coding Style + +## Philosophy +1. If it's inserting something into the existing classes/functions, try to follow the existing style as closely as possible. +1. If it's brand new code or refactoring a complete class or area of the code, please follow as Modern C# of a style as you can and reference the [.NET Engineering Guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines) as much as you possibly can. + +## Formatting + +- We use [`.clang-format`](/.clang-format) style file to enable automatic code formatting. You can [easily format source files from Visual Studio](https://devblogs.microsoft.com/cppblog/clangformat-support-in-visual-studio-2017-15-7-preview-1/). For example, `CTRL+K CTRL+D` formats the current document. +- If you prefer another text editor or have ClangFormat disabled in Visual Studio, you could invoke [`format_sources`](/codeAnalysis/format_sources.ps1) powershell script from command line. It gets a list of all currently modified files from `git` and invokes clang-format on them. +Please note that you should also have `clang-format.exe` in `%PATH%` for it to work. The script can infer the path of `clang-format.exe` version which is shipped with Visual Studio at `%VCINSTALLDIR%\Tools\Llvm\bin\`, if you launch it from the *Native Tools Command Prompt for VS*. +- CI doesn't enforce code formatting yet, since we're gradually applying code formatting to the codebase, but please adhere to our formatting style for any new code. \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000000..12076d710d --- /dev/null +++ b/nuget.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/pluginsdk/Build.cmd b/pluginsdk/Build.cmd new file mode 100644 index 0000000000..ca36ff8ed9 --- /dev/null +++ b/pluginsdk/Build.cmd @@ -0,0 +1,5 @@ +@echo off + +powershell -ExecutionPolicy Unrestricted -NoLogo -NoProfile -File %~dp0\Build.ps1 %* + +exit /b %ERRORLEVEL% \ No newline at end of file diff --git a/pluginsdk/Build.ps1 b/pluginsdk/Build.ps1 new file mode 100644 index 0000000000..6259f93bf3 --- /dev/null +++ b/pluginsdk/Build.ps1 @@ -0,0 +1,85 @@ +Param( + [string]$Configuration = "Release", + [string]$SDKVersion, + [bool]$IsAzurePipelineBuild = $false, + [switch]$Help = $false +) + +$StartTime = Get-Date + +if ($Help) { + Write-Host @" +Copyright (c) Microsoft Corporation and Contributors. +Licensed under the MIT License. + +Syntax: + Build.cmd [options] + +Description: + Builds Dev Home SDK. + +Options: + + -Configuration + Only build the selected configuration(s) + Example: -Configuration Release + Example: -Configuration "Debug,Release" + + -Help + Display this usage message. +"@ + Exit +} + +$ErrorActionPreference = "Stop" + +$buildPlatforms = "x64","x86","arm64","AnyCPU" +$env:Build_RootDirectory = (Split-Path $MyInvocation.MyCommand.Path) +$env:Build_Configuration = $Configuration +$env:sdk_version = & (Join-Path $PSScriptRoot "..\build\Scripts\CreateBuildInfo.ps1") -Version $SDKVersion -IsSdkVersion $true -IsAzurePipelineBuild $IsAzurePipelineBuild + +$msbuildPath = &"${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe +if ($IsAzurePipelineBuild) { + $nugetPath = "nuget.exe"; +} else { + $nugetPath = (Join-Path $PSScriptRoot "..\build\NugetWrapper.cmd") +} + +New-Item -ItemType Directory -Force -Path "$PSScriptRoot\_build" +& $nugetPath restore (Join-Path $PSScriptRoot DevHomeSDK.sln) + +Try { + foreach ($platform in $buildPlatforms) { + foreach ($configuration in $env:Build_Configuration.Split(",")) { + $msbuildArgs = @( + ("$PSScriptRoot\DevHomeSDK.sln"), + ("/p:Platform="+$platform), + ("/p:Configuration="+$configuration), + ("/binaryLogger:DevHome.SDK.$platform.$configuration.binlog") + ) + + & $msbuildPath $msbuildArgs + } + } +} Catch { + $formatString = "`n{0}`n`n{1}`n`n" + $fields = $_, $_.ScriptStackTrace + Write-Host ($formatString -f $fields) -ForegroundColor RED + Exit 1 +} + +& $nugetPath pack (Join-Path $PSScriptRoot "nuget\Microsoft.Windows.DevHome.SDK.nuspec") -Version $env:sdk_version -OutputDirectory "$PSScriptRoot\_build" + +if ($IsAzurePipelineBuild) { + Write-Host "##vso[task.setvariable variable=SDKVersion;]$env:sdk_version" + Write-Host "##vso[task.setvariable variable=SDKVersion;isOutput=true;]$env:sdk_version" +} + +$TotalTime = (Get-Date)-$StartTime +$TotalMinutes = [math]::Floor($TotalTime.TotalMinutes) +$TotalSeconds = [math]::Ceiling($TotalTime.TotalSeconds) + +Write-Host @" +Total Running Time: +$TotalMinutes minutes and $TotalSeconds seconds +"@ -ForegroundColor CYAN \ No newline at end of file diff --git a/pluginsdk/DevHomeSDK.sln b/pluginsdk/DevHomeSDK.sln new file mode 100644 index 0000000000..e094c6ac6b --- /dev/null +++ b/pluginsdk/DevHomeSDK.sln @@ -0,0 +1,71 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Windows.DevHome.SDK", "Microsoft.Windows.DevHome.SDK\Microsoft.Windows.DevHome.SDK.vcxproj", "{295DD37E-C85D-4B08-AAFE-7381FA890463}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Windows.DevHome.SDK.Lib", "Microsoft.Windows.DevHome.SDK.Lib\Microsoft.Windows.DevHome.SDK.Lib.csproj", "{D238B284-AEA7-4F77-AB27-C86261D5B7D3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Debug|ARM = Debug|ARM + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|AnyCPU = Release|AnyCPU + Release|ARM = Release|ARM + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|AnyCPU.ActiveCfg = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|AnyCPU.Build.0 = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|ARM.ActiveCfg = Debug|ARM + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|ARM.Build.0 = Debug|ARM + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|arm64.ActiveCfg = Debug|arm64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|arm64.Build.0 = Debug|arm64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.ActiveCfg = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.Build.0 = Debug|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x86.ActiveCfg = Debug|Win32 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x86.Build.0 = Debug|Win32 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|AnyCPU.ActiveCfg = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|AnyCPU.Build.0 = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|ARM.ActiveCfg = Release|ARM + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|ARM.Build.0 = Release|ARM + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|arm64.ActiveCfg = Release|arm64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|arm64.Build.0 = Release|arm64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.ActiveCfg = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.Build.0 = Release|x64 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x86.ActiveCfg = Release|Win32 + {295DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x86.Build.0 = Release|Win32 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|ARM.ActiveCfg = Debug|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|ARM.Build.0 = Debug|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|arm64.ActiveCfg = Debug|arm64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|arm64.Build.0 = Debug|arm64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|x64.ActiveCfg = Debug|x64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|x64.Build.0 = Debug|x64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|x86.ActiveCfg = Debug|x86 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Debug|x86.Build.0 = Debug|x86 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|AnyCPU.Build.0 = Release|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|ARM.ActiveCfg = Release|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|ARM.Build.0 = Release|AnyCPU + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|arm64.ActiveCfg = Release|arm64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|arm64.Build.0 = Release|arm64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|x64.ActiveCfg = Release|x64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|x64.Build.0 = Release|x64 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|x86.ActiveCfg = Release|x86 + {D238B284-AEA7-4F77-AB27-C86261D5B7D3}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B00DE33B-30B3-4324-95CD-7F3E6D0B1C78} + EndGlobalSection +EndGlobal diff --git a/pluginsdk/Directory.Build.props b/pluginsdk/Directory.Build.props new file mode 100644 index 0000000000..95baf2d68c --- /dev/null +++ b/pluginsdk/Directory.Build.props @@ -0,0 +1,22 @@ + + + + Copyright (C) 2022 Microsoft Corporation + Microsoft Corp. + Copyright (C) 2022 Microsoft Corporation + Dev Home SDK + Microsoft Corporation + en-US + x64;x86;ARM64;AnyCPU + DevHome + true + Recommended + $(Platform) + + + + <_PropertySheetDisplayName>DevHome.Root.Props + $(MsbuildThisFileDirectory)\Cpp.Build.props + + + \ No newline at end of file diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/Microsoft.Windows.DevHome.SDK.Lib.csproj b/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/Microsoft.Windows.DevHome.SDK.Lib.csproj new file mode 100644 index 0000000000..e86e6550d0 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/Microsoft.Windows.DevHome.SDK.Lib.csproj @@ -0,0 +1,25 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + x86;x64;arm64;AnyCPU + None + enable + enable + + + + Microsoft.Windows.DevHome.SDK + $(OutDir) + + + + + + + + + + + diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/PluginInstanceManager.cs b/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/PluginInstanceManager.cs new file mode 100644 index 0000000000..87f4d49e47 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/PluginInstanceManager.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Windows.ApplicationModel.Background; +using Windows.Foundation; +using Windows.Storage; +using WinRT; + +namespace Microsoft.Windows.DevHome.SDK; + +[ComVisible(true)] +internal class PluginInstanceManager : IClassFactory + where T : IPlugin +{ +#pragma warning disable SA1310 // Field names should not contain underscore + + private const int E_NOINTERFACE = unchecked((int)0x800004002); + + private const int CLASS_E_NOAGGREGATION = unchecked((int)0x80040110); + + private static readonly Guid IID_IUnknown = Guid.Parse("00000000-0000-0000-C000-000000000046"); + +#pragma warning restore SA1310 // Field names should not contain underscore + + private readonly Func _createPlugin; + + public PluginInstanceManager(Func createPlugin) + { + this._createPlugin = createPlugin; + } + + public void CreateInstance( + [MarshalAs(UnmanagedType.Interface)] object pUnkOuter, + ref Guid riid, + out IntPtr ppvObject) + { + ppvObject = IntPtr.Zero; + + if (pUnkOuter != null) + { + Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION); + } + + if (riid == typeof(T).GUID || riid == IID_IUnknown) + { + // Create the instance of the .NET object + ppvObject = MarshalInspectable.FromManaged(_createPlugin()); + } + else + { + // The object that ppvObject points to does not support the + // interface identified by riid. + Marshal.ThrowExceptionForHR(E_NOINTERFACE); + } + } + + public void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock) + { + } +} + +// https://docs.microsoft.com/windows/win32/api/unknwn/nn-unknwn-iclassfactory +[ComImport] +[ComVisible(false)] +[Guid("00000001-0000-0000-C000-000000000046")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IClassFactory +{ + void CreateInstance( + [MarshalAs(UnmanagedType.Interface)] object pUnkOuter, + ref Guid riid, + out IntPtr ppvObject); + + void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock); +} diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/PluginServer.cs b/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/PluginServer.cs new file mode 100644 index 0000000000..8b2cebe0ce --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK.Lib/PluginServer.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.Windows.DevHome.SDK; + +public sealed class PluginServer : IDisposable +{ + private readonly HashSet registrationCookies = new (); + + public void RegisterPlugin(Func createPlugin) + where T : IPlugin + { + Trace.WriteLine($"Registering class object:"); + Trace.Indent(); + Trace.WriteLine($"CLSID: {typeof(T).GUID:B}"); + Trace.WriteLine($"Type: {typeof(T)}"); + + int cookie; + var clsid = typeof(T).GUID; + var hr = Ole32.CoRegisterClassObject( + ref clsid, + new PluginInstanceManager(createPlugin), + Ole32.CLSCTX_LOCAL_SERVER, + Ole32.REGCLS_MULTIPLEUSE | Ole32.REGCLS_SUSPENDED, + out cookie); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + registrationCookies.Add(cookie); + Trace.WriteLine($"Cookie: {cookie}"); + Trace.Unindent(); + + hr = Ole32.CoResumeClassObjects(); + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + } + + public void Run() + { + // TODO : We need to handle lifetime management of the server. + // For details around ref counting and locking of out-of-proc COM servers, see + // https://docs.microsoft.com/windows/win32/com/out-of-process-server-implementation-helpers + Console.ReadLine(); + } + + public void Dispose() + { + Trace.WriteLine($"Revoking class object registrations:"); + Trace.Indent(); + foreach (var cookie in registrationCookies) + { + Trace.WriteLine($"Cookie: {cookie}"); + var hr = Ole32.CoRevokeClassObject(cookie); + Debug.Assert(hr >= 0, $"CoRevokeClassObject failed ({hr:x}). Cookie: {cookie}"); + } + + Trace.Unindent(); + } + + private class Ole32 + { +#pragma warning disable SA1310 // Field names should not contain underscore + // https://docs.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx + public const int CLSCTX_LOCAL_SERVER = 0x4; + + // https://docs.microsoft.com/windows/win32/api/combaseapi/ne-combaseapi-regcls + public const int REGCLS_MULTIPLEUSE = 1; + public const int REGCLS_SUSPENDED = 4; +#pragma warning restore SA1310 // Field names should not contain underscore + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coregisterclassobject + [DllImport(nameof(Ole32))] + public static extern int CoRegisterClassObject(ref Guid guid, [MarshalAs(UnmanagedType.IUnknown)] object obj, int context, int flags, out int register); + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coresumeclassobjects + [DllImport(nameof(Ole32))] + public static extern int CoResumeClassObjects(); + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-corevokeclassobject + [DllImport(nameof(Ole32))] + public static extern int CoRevokeClassObject(int register); + } +} diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.PluginUI.idl b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.PluginUI.idl new file mode 100644 index 0000000000..77580db984 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.PluginUI.idl @@ -0,0 +1,20 @@ +namespace Microsoft.Windows.DevHome.SDK +{ + [version(1.0)] + interface IPluginAdaptiveCardController + { + void Initialize(IPluginAdaptiveCard pluginUI); + void Dispose(); + void OnAction(String action, String inputs); + }; + + [version(1.0)] + interface IPluginAdaptiveCard + { + String Template { get; }; + String Data { get; }; + String State { get; }; + + void Update(String template, String data, String state); + }; +} \ No newline at end of file diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.def b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.def new file mode 100644 index 0000000000..047a2f099f --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.def @@ -0,0 +1 @@ +EXPORTS diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl new file mode 100644 index 0000000000..e9a9977fc6 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl @@ -0,0 +1,97 @@ +import "Microsoft.Windows.DevHome.SDK.PluginUI.idl"; + +namespace Microsoft.Windows.DevHome.SDK +{ + [version(1.0)] + enum ProviderType + { + Repository = 0, + DevId = 1, + Settings = 2, + SetupFlow = 3, + Widget = 4, + DevDoctor = 5, + Notifications = 6 + }; + + [version(1.0)] + interface IPlugin + { + IInspectable GetProvider(ProviderType providerType); + void Dispose(); + } + + [version(1.0)] + interface IRepositoryProvider + { + String GetDisplayName(); + + Windows.Foundation.IAsyncOperation > GetRepositoriesAsync(IDeveloperId developerId); + }; + + [version(1.0)] + interface IRepository + { + String DisplayName(); + + Windows.Foundation.IAsyncAction CloneRepositoryAsync(String cloneDestination, IDeveloperId developerId); + + Windows.Foundation.IAsyncAction CloneRepositoryAsync(String cloneDestination); + } + + [version(1.0)] + interface ISettingsProvider + { + String GetName(); + }; + + [version(1.0)] + interface IDevIdProvider + { + String GetName(); + + IIterable GetLoggedInDeveloperIds(); + + void LoginNewDeveloperId(); + + void LogoutDeveloperId(IDeveloperId developerId); + + IPluginAdaptiveCardController GetAdaptiveCardController(String[] args); + + String LogoutUI(); + }; + + [version(1.0)] + interface ISetupFlowProvider + { + String GetName(); + }; + + [version(1.0)] + interface IWidgetProvider + { + String GetName(); + }; + + [version(1.0)] + interface IDevDoctorProvider + { + String GetName(); + }; + + [version(1.0)] + interface INotificationsProvider + { + String GetName(); + }; + + [version(1.0)] + // IDeveloperId is the basic interface for DeveloperId corresponding to each logged in user, used by the Dev Home Core app + interface IDeveloperId + { + String LoginId(); + String DisplayName(); + String Email(); + String Url(); + }; +} diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj new file mode 100644 index 0000000000..2460bd3d40 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj @@ -0,0 +1,176 @@ + + + + + true + true + true + true + {295dd37e-c85d-4b08-aafe-7381fa890463} + Microsoft.Windows.DevHome.SDK + Microsoft.Windows.DevHome.SDK + en-US + 14.0 + true + Windows Store + 10.0 + 10.0.19041.0 + 10.0.17763.0 + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + DynamicLibrary + v143 + v142 + v141 + v140 + Unicode + false + + + true + true + + + false + true + false + + + + + + + + + + + + bin\x86\$(Configuration)\ + obj\x86\$(Configuration)\ + + + bin\x86\$(Configuration)\ + obj\x86\$(Configuration)\ + + + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + + + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + + + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + + + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + + + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + + + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /bigobj + _WINRT_DLL;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + + + Console + false + Microsoft.Windows.DevHome.SDK.def + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + Create + + + + + + + + + + + + + false + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/packages.config b/pluginsdk/Microsoft.Windows.DevHome.SDK/packages.config new file mode 100644 index 0000000000..cbf6205e62 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/pch.cpp b/pluginsdk/Microsoft.Windows.DevHome.SDK/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/pch.h b/pluginsdk/Microsoft.Windows.DevHome.SDK/pch.h new file mode 100644 index 0000000000..21199686d5 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/pch.h @@ -0,0 +1,4 @@ +#pragma once +#include +#include +#include diff --git a/pluginsdk/Microsoft.Windows.DevHome.SDK/readme.txt b/pluginsdk/Microsoft.Windows.DevHome.SDK/readme.txt new file mode 100644 index 0000000000..49d8e990b3 --- /dev/null +++ b/pluginsdk/Microsoft.Windows.DevHome.SDK/readme.txt @@ -0,0 +1,2 @@ +The DLL that this project builds exists only to satisfy the build system, and is otherwise unused. +This Project provides winmd to the SDK project for building projections. \ No newline at end of file diff --git a/pluginsdk/_build/.gitkeep b/pluginsdk/_build/.gitkeep new file mode 100644 index 0000000000..9514cec208 --- /dev/null +++ b/pluginsdk/_build/.gitkeep @@ -0,0 +1,3 @@ +Do not delete this folder. +This folder is where the locally built SDK Nuget packages end up to get ingested into Dev Home. +The build system is always expecting it. diff --git a/pluginsdk/azure-pipelines.yml b/pluginsdk/azure-pipelines.yml new file mode 100644 index 0000000000..a36a582130 --- /dev/null +++ b/pluginsdk/azure-pipelines.yml @@ -0,0 +1,4 @@ +pool: + vmImage: 'windows-latest' + + \ No newline at end of file diff --git a/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.nuspec b/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.nuspec new file mode 100644 index 0000000000..19af23af10 --- /dev/null +++ b/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.nuspec @@ -0,0 +1,36 @@ + + + + Microsoft.Windows.DevHome.SDK + 1.0.0 + Dev Home SDK + Microsoft + Microsoft + false + Dev Home SDK provides support for creating Dev Home plugins on Windows. + Release notes are available on the Dev Home repository. + Dev Home Windows App Plugin + © Microsoft Corporation. All rights reserved. + + https://github.com/microsoft/devhome + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.props b/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.props new file mode 100644 index 0000000000..e8d0a99897 --- /dev/null +++ b/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.props @@ -0,0 +1,5 @@ + + + $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', '..')) + + \ No newline at end of file diff --git a/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.targets b/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.targets new file mode 100644 index 0000000000..a0015c77b4 --- /dev/null +++ b/pluginsdk/nuget/Microsoft.Windows.DevHome.SDK.targets @@ -0,0 +1,7 @@ + + + + Always + + + \ No newline at end of file diff --git a/src/Activation/ActivationHandler.cs b/src/Activation/ActivationHandler.cs new file mode 100644 index 0000000000..b607360b9c --- /dev/null +++ b/src/Activation/ActivationHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Activation; + +// Extend this class to implement new ActivationHandlers. See DefaultActivationHandler for an example. +// https://github.com/microsoft/TemplateStudio/blob/main/docs/WinUI/activation.md +public abstract class ActivationHandler : IActivationHandler + where T : class +{ + // Override this method to add the logic for whether to handle the activation. + protected virtual bool CanHandleInternal(T args) => true; + + // Override this method to add the logic for your activation handler. + protected abstract Task HandleInternalAsync(T args); + + public bool CanHandle(object args) => args is T && CanHandleInternal((args as T)!); + + public async Task HandleAsync(object args) => await HandleInternalAsync((args as T)!); +} diff --git a/src/Activation/DefaultActivationHandler.cs b/src/Activation/DefaultActivationHandler.cs new file mode 100644 index 0000000000..faea75ef4b --- /dev/null +++ b/src/Activation/DefaultActivationHandler.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.ViewModels; +using Microsoft.UI.Xaml; + +namespace DevHome.Activation; + +public class DefaultActivationHandler : ActivationHandler +{ + private readonly INavigationService _navigationService; + + public DefaultActivationHandler(INavigationService navigationService) + { + _navigationService = navigationService; + } + + protected override bool CanHandleInternal(LaunchActivatedEventArgs args) + { + // None of the ActivationHandlers has handled the activation. + return _navigationService.Frame?.Content == null; + } + + protected async override Task HandleInternalAsync(LaunchActivatedEventArgs args) + { + _navigationService.NavigateTo(App.NavConfig.NavMenu.Groups[0].Tools[0].ViewModelFullName, args.Arguments); + await Task.CompletedTask; + } +} diff --git a/src/Activation/IActivationHandler.cs b/src/Activation/IActivationHandler.cs new file mode 100644 index 0000000000..e0ade69991 --- /dev/null +++ b/src/Activation/IActivationHandler.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Activation; + +public interface IActivationHandler +{ + bool CanHandle(object args); + + Task HandleAsync(object args); +} diff --git a/src/App.xaml b/src/App.xaml new file mode 100644 index 0000000000..60d5b178c0 --- /dev/null +++ b/src/App.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/src/App.xaml.cs b/src/App.xaml.cs new file mode 100644 index 0000000000..90558170da --- /dev/null +++ b/src/App.xaml.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Activation; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.Core.Contracts.Services; +using DevHome.Core.Services; +using DevHome.Helpers; +using DevHome.Models; +using DevHome.Services; +using DevHome.ViewModels; +using DevHome.Views; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; +using Microsoft.Windows.DevHome.SDK; +using Newtonsoft.Json; +using WinRT; + +namespace DevHome; + +// To learn more about WinUI 3, see https://docs.microsoft.com/windows/apps/winui/winui3/. +public partial class App : Application, IApp +{ + // The .NET Generic Host provides dependency injection, configuration, logging, and other services. + // https://docs.microsoft.com/dotnet/core/extensions/generic-host + // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection + // https://docs.microsoft.com/dotnet/core/extensions/configuration + // https://docs.microsoft.com/dotnet/core/extensions/logging + public IHost Host + { + get; + } + + public T GetService() + where T : class => Host.GetService(); + + public static WindowEx MainWindow { get; } = new MainWindow(); + + internal static NavConfig NavConfig { get; } = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "navConfig.json")))!; + + public App() + { + InitializeComponent(); + + Host = Microsoft.Extensions.Hosting.Host. + CreateDefaultBuilder(). + UseContentRoot(AppContext.BaseDirectory). + ConfigureServices((context, services) => + { + // Default Activation Handler + services.AddTransient, DefaultActivationHandler>(); + + // Other Activation Handlers + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Core Services + services.AddSingleton(); + + // Main window: Allow access to the main window + // from anywhere in the application. + services.AddSingleton(_ => MainWindow); + + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configuration + services.Configure(context.Configuration.GetSection(nameof(LocalSettingsOptions))); + }). + Build(); + + UnhandledException += App_UnhandledException; + } + + private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + // TODO: Log and handle exceptions as appropriate. + // https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception. + } + + protected async override void OnLaunched(LaunchActivatedEventArgs args) + { + base.OnLaunched(args); + + await GetService().ActivateAsync(args); + } +} diff --git a/src/Assets/Fonts/CascadiaMono.ttf b/src/Assets/Fonts/CascadiaMono.ttf new file mode 100644 index 0000000000..d15637efe0 Binary files /dev/null and b/src/Assets/Fonts/CascadiaMono.ttf differ diff --git a/src/Assets/Fonts/README.md b/src/Assets/Fonts/README.md new file mode 100644 index 0000000000..25ae0969c1 --- /dev/null +++ b/src/Assets/Fonts/README.md @@ -0,0 +1,3 @@ +# Included fonts +- Cascadia Mono (v2111.01) + - https://github.com/microsoft/cascadia-code/releases/tag/v2111.01 \ No newline at end of file diff --git a/src/Assets/LockScreenLogo.scale-200.png b/src/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..735f57adb5 Binary files /dev/null and b/src/Assets/LockScreenLogo.scale-200.png differ diff --git a/src/Assets/SplashScreen.scale-200.png b/src/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..023e7f1fed Binary files /dev/null and b/src/Assets/SplashScreen.scale-200.png differ diff --git a/src/Assets/Square150x150Logo.scale-200.png b/src/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..af49fec1a5 Binary files /dev/null and b/src/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/Assets/Square44x44Logo.scale-200.png b/src/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..ce342a2ec8 Binary files /dev/null and b/src/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..f6c02ce97e Binary files /dev/null and b/src/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/Assets/StoreLogo.png b/src/Assets/StoreLogo.png new file mode 100644 index 0000000000..7385b56c0e Binary files /dev/null and b/src/Assets/StoreLogo.png differ diff --git a/src/Assets/Wide310x150Logo.scale-200.png b/src/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..288995b397 Binary files /dev/null and b/src/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/Assets/WindowIcon.ico b/src/Assets/WindowIcon.ico new file mode 100644 index 0000000000..5bbd48e61d Binary files /dev/null and b/src/Assets/WindowIcon.ico differ diff --git a/src/Contracts/Services/IActivationService.cs b/src/Contracts/Services/IActivationService.cs new file mode 100644 index 0000000000..716077d626 --- /dev/null +++ b/src/Contracts/Services/IActivationService.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Contracts.Services; + +public interface IActivationService +{ + Task ActivateAsync(object activationArgs); +} diff --git a/src/Contracts/Services/IFileService.cs b/src/Contracts/Services/IFileService.cs new file mode 100644 index 0000000000..2345fa5585 --- /dev/null +++ b/src/Contracts/Services/IFileService.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Core.Contracts.Services; + +public interface IFileService +{ + T Read(string folderPath, string fileName); + + void Save(string folderPath, string fileName, T content); + + void Delete(string folderPath, string fileName); +} diff --git a/src/Contracts/Services/IGitWatcher.cs b/src/Contracts/Services/IGitWatcher.cs new file mode 100644 index 0000000000..1b6fee640e --- /dev/null +++ b/src/Contracts/Services/IGitWatcher.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.ObjectModel; + +namespace DevHome.Contracts.Services; + +public enum GitRepositoryChangeType +{ + Created, + Deleted, +} + +public class GitRepositoryChangedEventArgs : EventArgs +{ + public GitRepositoryChangedEventArgs(GitRepositoryChangeType changeType, string repositoryPath) + { + RepositoryPath = repositoryPath; + ChangeType = changeType; + } + + public string RepositoryPath { get; } + + public GitRepositoryChangeType ChangeType { get; } +} + +public interface IGitWatcher +{ + /// + /// Provides the GitWatcher with a list of repositories to monitor. + /// + /// + /// A collection of strings representing repository paths to monitor. Each should point to the root + /// directory of the repository. + /// + /// These can be either absolute paths, or paths prefaced with a WSL identifier (format TBD). + /// + public void AddTrackedRepositories(Collection repositoryPaths); + + /// + /// Removes a set of repositories from monitoring. + /// + /// + /// A collection of strings representing repository paths to monitor. Each should point to the root + /// directory of the repository. + /// + /// These can be either absolute paths, or paths prefaced with a WSL identifier (format TBD). + /// + /// Paths that are not already being tracked will be ignored. + /// + public void RemoveTrackedRepositories(Collection repositoryPaths); + + /// + /// Get the list of repositories being monitored. + /// + /// A list of absolute paths, or paths prefaced with WSL identifiers (format TBD), + /// that are currently being monitored by this watcher. + public List GetTrackedRepositories(); + + /// + /// Provides a list of sources to monitor for new Git repositories. + /// + /// To stop monitoring, use SetMonitoredSources(null, false);. + /// + /// + /// A collection of strings representing sources to monitor. + /// + /// Each string can be either an absolute path, or a WSL identifier (format TBD). + /// + /// true to append new sources to the monitoring list, false to overwrite + public void SetMonitoredSources(Collection? sources, bool append = false); + + /// + /// Remove a source from monitoring for new Git repositories. + /// + /// + /// A string representing a source being monitored. + /// + /// Can be either an absolute path, or a WSL identifier (format TBD). + /// + /// true if removed; false if the source specified was not already being monitored + public bool RemoveMonitoredSource(string source); + + /// + /// Gets the list of sources being monitored for new repositories. + /// + /// A list of strings representing either absolute paths or WSL identifiers of monitored sources + public List GetMonitoredSources(); + + public event EventHandler GitRepositoryCreated; + + public event EventHandler GitRepositoryDeleted; + + /// + /// Subscribes to callbacks for changes to files matching a particular pattern within all known repositories. + /// + /// + /// The pattern to match. Supports asterisk and question mark wildcards; see + /// FileSystemWatcher.Filter's documentation for details: + /// https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.filter + /// + /// An IGitFileWatcher watching for changes to the specified file(s) + public IGitFileWatcher CreateFileWatcher(string filePattern); +} + +public enum GitFileChangeType +{ + Created, + Modified, + Deleted, +} + +public class GitFileChangedEventArgs : EventArgs +{ + public GitFileChangedEventArgs(GitFileChangeType changeType, string repositoryPath, string filePath) + { + RepositoryPath = repositoryPath; + FilePath = filePath; + ChangeType = changeType; + } + + public string RepositoryPath { get; } = string.Empty; + + public string FilePath { get; } = string.Empty; + + public GitFileChangeType ChangeType { get; } +} + +public interface IGitFileWatcher +{ + public string Filter { get; } + + public event EventHandler? FileCreated; + + public event EventHandler? FileModified; + + public event EventHandler? FileDeleted; + + public void Close(); +} diff --git a/src/Contracts/Services/ILocalSettingsService.cs b/src/Contracts/Services/ILocalSettingsService.cs new file mode 100644 index 0000000000..783122f58e --- /dev/null +++ b/src/Contracts/Services/ILocalSettingsService.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Contracts.Services; + +public interface ILocalSettingsService +{ + Task ReadSettingAsync(string key); + + Task SaveSettingAsync(string key, T value); +} diff --git a/src/Contracts/Services/INavigationViewService.cs b/src/Contracts/Services/INavigationViewService.cs new file mode 100644 index 0000000000..a1cbf89e3d --- /dev/null +++ b/src/Contracts/Services/INavigationViewService.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Contracts.Services; + +public interface INavigationViewService +{ + IList? MenuItems + { + get; + } + + IList? FooterMenuItems + { + get; + } + + object? SettingsItem + { + get; + } + + void Initialize(NavigationView navigationView); + + void UnregisterEvents(); + + NavigationViewItem? GetSelectedItem(Type pageType); +} diff --git a/src/Contracts/Services/IPageService.cs b/src/Contracts/Services/IPageService.cs new file mode 100644 index 0000000000..45114b284c --- /dev/null +++ b/src/Contracts/Services/IPageService.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Contracts.Services; + +public interface IPageService +{ + Type GetPageType(string key); +} diff --git a/src/Contracts/ViewModels/INavigationAware.cs b/src/Contracts/ViewModels/INavigationAware.cs new file mode 100644 index 0000000000..e87a707fca --- /dev/null +++ b/src/Contracts/ViewModels/INavigationAware.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Contracts.ViewModels; + +public interface INavigationAware +{ + void OnNavigatedTo(object parameter); + + void OnNavigatedFrom(); +} diff --git a/src/DevHome.csproj b/src/DevHome.csproj new file mode 100644 index 0000000000..cd4cbd1df0 --- /dev/null +++ b/src/DevHome.csproj @@ -0,0 +1,66 @@ + + + WinExe + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome + Assets/WindowIcon.ico + app.manifest + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + Properties\PublishProfiles\win10-$(Platform).pubxml + enable + enable + true + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + Always + + + Always + + + MSBuild:Compile + + + + + + + + + + + + + true + + diff --git a/src/Helpers/EnumToBooleanConverter.cs b/src/Helpers/EnumToBooleanConverter.cs new file mode 100644 index 0000000000..be900bcb18 --- /dev/null +++ b/src/Helpers/EnumToBooleanConverter.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace DevHome.Helpers; + +public class EnumToBooleanConverter : IValueConverter +{ + public EnumToBooleanConverter() + { + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + if (parameter is string enumString) + { + if (!Enum.IsDefined(typeof(ElementTheme), value)) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterValueMustBeAnEnum"); + } + + var enumValue = Enum.Parse(typeof(ElementTheme), enumString); + + return enumValue.Equals(value); + } + + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (parameter is string enumString) + { + return Enum.Parse(typeof(ElementTheme), enumString); + } + + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } +} diff --git a/src/Helpers/FrameExtensions.cs b/src/Helpers/FrameExtensions.cs new file mode 100644 index 0000000000..9da1520a16 --- /dev/null +++ b/src/Helpers/FrameExtensions.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Helpers; + +public static class FrameExtensions +{ + public static object? GetPageViewModel(this Frame frame) => frame?.Content?.GetType().GetProperty("ViewModel")?.GetValue(frame.Content, null); +} diff --git a/src/Helpers/Json.cs b/src/Helpers/Json.cs new file mode 100644 index 0000000000..67047d10e6 --- /dev/null +++ b/src/Helpers/Json.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Newtonsoft.Json; + +namespace DevHome.Core.Helpers; + +public static class Json +{ + public static async Task ToObjectAsync(string value) + { + return await Task.Run(() => + { + return JsonConvert.DeserializeObject(value)!; + }); + } + + public static async Task StringifyAsync(object value) + { + return await Task.Run(() => + { + return JsonConvert.SerializeObject(value); + }); + } +} diff --git a/src/Helpers/NavConfigHelper.cs b/src/Helpers/NavConfigHelper.cs new file mode 100644 index 0000000000..0bcd305956 --- /dev/null +++ b/src/Helpers/NavConfigHelper.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Newtonsoft.Json; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +namespace DevHome.Helpers; + +internal class NavConfig +{ + [JsonProperty("navMenu")] + public NavMenu NavMenu { get; set; } +} + +internal class NavMenu +{ + [JsonProperty("groups")] + public Group[] Groups { get; set; } +} + +internal class Group +{ + [JsonProperty("identity")] + public string Identity { get; set; } + + [JsonProperty("tools")] + public Tool[] Tools { get; set; } +} + +internal class Tool +{ + [JsonProperty("identity")] + public string Identity { get; set; } + + [JsonProperty("assembly")] + public string Assembly { get; set; } + + [JsonProperty("viewFullName")] + public string ViewFullName { get; set; } + + [JsonProperty("viewModelFullName")] + public string ViewModelFullName { get; set; } +} + +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/src/Helpers/NavigationHelper.cs b/src/Helpers/NavigationHelper.cs new file mode 100644 index 0000000000..7ff03845a9 --- /dev/null +++ b/src/Helpers/NavigationHelper.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Helpers; + +// Helper class to set the navigation target for a NavigationViewItem. +// +// Usage in XAML: +// +// +// Usage in code: +// NavigationHelper.SetNavigateTo(navigationViewItem, typeof(MainViewModel).FullName); +public class NavigationHelper +{ + public static string GetNavigateTo(NavigationViewItem item) => (string)item.GetValue(NavigateToProperty); + + public static void SetNavigateTo(NavigationViewItem item, string value) => item.SetValue(NavigateToProperty, value); + + public static readonly DependencyProperty NavigateToProperty = + DependencyProperty.RegisterAttached("NavigateTo", typeof(string), typeof(NavigationHelper), new PropertyMetadata(null)); +} diff --git a/src/Helpers/ResourceExtensions.cs b/src/Helpers/ResourceExtensions.cs new file mode 100644 index 0000000000..a044645444 --- /dev/null +++ b/src/Helpers/ResourceExtensions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.Windows.ApplicationModel.Resources; + +namespace DevHome.Helpers; + +public static class ResourceExtensions +{ + private static readonly ResourceLoader _resourceLoader = new (); + + public static string GetLocalized(this string resourceKey) => _resourceLoader.GetString(resourceKey); +} diff --git a/src/Helpers/RuntimeHelper.cs b/src/Helpers/RuntimeHelper.cs new file mode 100644 index 0000000000..a1d858eb9b --- /dev/null +++ b/src/Helpers/RuntimeHelper.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Runtime.InteropServices; +using System.Text; + +namespace DevHome.Helpers; + +public class RuntimeHelper +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder? packageFullName); + + public static bool IsMSIX + { + get + { + var length = 0; + + return GetCurrentPackageFullName(ref length, null) != 15700L; + } + } +} diff --git a/src/Helpers/SettingsStorageExtensions.cs b/src/Helpers/SettingsStorageExtensions.cs new file mode 100644 index 0000000000..32a970e164 --- /dev/null +++ b/src/Helpers/SettingsStorageExtensions.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Core.Helpers; + +using Windows.Storage; +using Windows.Storage.Streams; + +namespace DevHome.Helpers; + +// Use these extension methods to store and retrieve local and roaming app data +// More details regarding storing and retrieving app data at https://docs.microsoft.com/windows/apps/design/app-settings/store-and-retrieve-app-data +public static class SettingsStorageExtensions +{ + private const string FileExtension = ".json"; + + public static bool IsRoamingStorageAvailable(this ApplicationData appData) + { + return appData.RoamingStorageQuota == 0; + } + + public static async Task SaveAsync(this StorageFolder folder, string name, T content) + { + var file = await folder.CreateFileAsync(GetFileName(name), CreationCollisionOption.ReplaceExisting); + var fileContent = await Json.StringifyAsync(content!); + + await FileIO.WriteTextAsync(file, fileContent); + } + + public static async Task ReadAsync(this StorageFolder folder, string name) + { + if (!File.Exists(Path.Combine(folder.Path, GetFileName(name)))) + { + return default; + } + + var file = await folder.GetFileAsync($"{name}.json"); + var fileContent = await FileIO.ReadTextAsync(file); + + return await Json.ToObjectAsync(fileContent); + } + + public static async Task SaveAsync(this ApplicationDataContainer settings, string key, T value) + { + settings.SaveString(key, await Json.StringifyAsync(value!)); + } + + public static void SaveString(this ApplicationDataContainer settings, string key, string value) + { + settings.Values[key] = value; + } + + public static async Task ReadAsync(this ApplicationDataContainer settings, string key) + { + object? obj; + + if (settings.Values.TryGetValue(key, out obj)) + { + return await Json.ToObjectAsync((string)obj); + } + + return default; + } + + public static async Task SaveFileAsync(this StorageFolder folder, byte[] content, string fileName, CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentException("File name is null or empty. Specify a valid file name", nameof(fileName)); + } + + var storageFile = await folder.CreateFileAsync(fileName, options); + await FileIO.WriteBytesAsync(storageFile, content); + return storageFile; + } + + public static async Task ReadFileAsync(this StorageFolder folder, string fileName) + { + var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); + + if ((item != null) && item.IsOfType(StorageItemTypes.File)) + { + var storageFile = await folder.GetFileAsync(fileName); + var content = await storageFile.ReadBytesAsync(); + return content; + } + + return null; + } + + public static async Task ReadBytesAsync(this StorageFile file) + { + if (file != null) + { + using IRandomAccessStream stream = await file.OpenReadAsync(); + using var reader = new DataReader(stream.GetInputStreamAt(0)); + await reader.LoadAsync((uint)stream.Size); + var bytes = new byte[stream.Size]; + reader.ReadBytes(bytes); + return bytes; + } + + return null; + } + + private static string GetFileName(string name) + { + return string.Concat(name, FileExtension); + } +} diff --git a/src/Helpers/TitleBarHelper.cs b/src/Helpers/TitleBarHelper.cs new file mode 100644 index 0000000000..410274aaa2 --- /dev/null +++ b/src/Helpers/TitleBarHelper.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Runtime.InteropServices; + +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; + +using Windows.UI; + +namespace DevHome.Helpers; + +// Helper class to workaround custom title bar bugs. +// DISCLAIMER: The resource key names and color values used below are subject to change. Do not depend on them. +// https://github.com/microsoft/TemplateStudio/issues/4516 +internal class TitleBarHelper +{ + private const int WAINACTIVE = 0x00; + private const int WAACTIVE = 0x01; + private const int WMACTIVATE = 0x0006; + + [DllImport("user32.dll")] + private static extern IntPtr GetActiveWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); + + public static void UpdateTitleBar(ElementTheme theme) + { + if (App.MainWindow.ExtendsContentIntoTitleBar) + { + if (theme != ElementTheme.Default) + { + Application.Current.Resources["WindowCaptionForeground"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Colors.White), + ElementTheme.Light => new SolidColorBrush(Colors.Black), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionForegroundDisabled"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF)), + ElementTheme.Light => new SolidColorBrush(Color.FromArgb(0x66, 0x00, 0x00, 0x00)), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonBackgroundPointerOver"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF)), + ElementTheme.Light => new SolidColorBrush(Color.FromArgb(0x33, 0x00, 0x00, 0x00)), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonBackgroundPressed"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF)), + ElementTheme.Light => new SolidColorBrush(Color.FromArgb(0x66, 0x00, 0x00, 0x00)), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonStrokePointerOver"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Colors.White), + ElementTheme.Light => new SolidColorBrush(Colors.Black), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonStrokePressed"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Colors.White), + ElementTheme.Light => new SolidColorBrush(Colors.Black), + _ => new SolidColorBrush(Colors.Transparent) + }; + } + + Application.Current.Resources["WindowCaptionBackground"] = new SolidColorBrush(Colors.Transparent); + Application.Current.Resources["WindowCaptionBackgroundDisabled"] = new SolidColorBrush(Colors.Transparent); + + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow); + if (hwnd == GetActiveWindow()) + { + SendMessage(hwnd, WMACTIVATE, WAINACTIVE, IntPtr.Zero); + SendMessage(hwnd, WMACTIVATE, WAACTIVE, IntPtr.Zero); + } + else + { + SendMessage(hwnd, WMACTIVATE, WAACTIVE, IntPtr.Zero); + SendMessage(hwnd, WMACTIVATE, WAINACTIVE, IntPtr.Zero); + } + } + } +} diff --git a/src/MainWindow.xaml b/src/MainWindow.xaml new file mode 100644 index 0000000000..ea63d45ecb --- /dev/null +++ b/src/MainWindow.xaml @@ -0,0 +1,16 @@ + + + + + diff --git a/src/MainWindow.xaml.cs b/src/MainWindow.xaml.cs new file mode 100644 index 0000000000..b3b52d6da8 --- /dev/null +++ b/src/MainWindow.xaml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Helpers; + +namespace DevHome; + +public sealed partial class MainWindow : WindowEx +{ + public MainWindow() + { + InitializeComponent(); + + AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/WindowIcon.ico")); + Content = null; + Title = "AppDisplayName".GetLocalized(); + } +} diff --git a/src/Models/LocalSettingsOptions.cs b/src/Models/LocalSettingsOptions.cs new file mode 100644 index 0000000000..f5fd390ebd --- /dev/null +++ b/src/Models/LocalSettingsOptions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.Models; + +public class LocalSettingsOptions +{ + public string? ApplicationDataFolder + { + get; set; + } + + public string? LocalSettingsFile + { + get; set; + } +} diff --git a/src/Models/PluginWrapper.cs b/src/Models/PluginWrapper.cs new file mode 100644 index 0000000000..80d52c8c72 --- /dev/null +++ b/src/Models/PluginWrapper.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Runtime.InteropServices; +using DevHome.Common.Services; +using DevHome.Services; +using Microsoft.Windows.DevHome.SDK; +using WinRT; + +namespace DevHome.Models; + +public class PluginWrapper : IPluginWrapper +{ + private readonly string _classId; + private readonly object _lock = new (); + private readonly List _providerTypes = new (); + private IPlugin? _pluginObject; + + public PluginWrapper(string name, string classId) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + _classId = classId ?? throw new ArgumentNullException(nameof(classId)); + } + + public string Name + { + get; + } + + public bool IsRunning() + { + // TODO : We also need to check if the underlying ptr is still alive + // to make sure the other process is still running + return _pluginObject is not null; + } + + public async Task StartPlugin() + { + await Task.Run(() => + { + lock (_lock) + { + if (!IsRunning()) + { + IntPtr pluginPtr = IntPtr.Zero; + try + { + var hr = Ole32.CoCreateInstance(Guid.Parse(_classId), IntPtr.Zero, Ole32.CLSCTXLOCALSERVER, typeof(IPlugin).GUID, out pluginPtr); + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + _pluginObject = MarshalInterface.FromAbi(pluginPtr); + } + finally + { + if (pluginPtr != IntPtr.Zero) + { + Marshal.Release(pluginPtr); + } + } + } + } + }); + } + + public void Kill() + { + lock (_lock) + { + if (IsRunning()) + { + // TODO : Should we kill the process as well? + _pluginObject = null; + } + } + } + + public IPlugin? GetPluginObject() + { + lock (_lock) + { + if (IsRunning()) + { + return _pluginObject; + } + else + { + return null; + } + } + } + + public void AddProviderType(ProviderType providerType) + { + _providerTypes.Add(providerType); + } + + public bool HasProviderType(ProviderType providerType) + { + return _providerTypes.Contains(providerType); + } +} diff --git a/src/NativeMethods.txt b/src/NativeMethods.txt new file mode 100644 index 0000000000..2bd230b733 --- /dev/null +++ b/src/NativeMethods.txt @@ -0,0 +1,3 @@ +GetPhysicallyInstalledSystemMemory +GlobalMemoryStatusEx +GetSystemInfo \ No newline at end of file diff --git a/src/NavConfig.json b/src/NavConfig.json new file mode 100644 index 0000000000..8393552466 --- /dev/null +++ b/src/NavConfig.json @@ -0,0 +1,23 @@ +{ + "navMenu": { + "groups": [ + { + "identity": "main", + "tools": [ + { + "identity": "DevHome.Dashboard", + "assembly": "DevHome.Dashboard", + "viewFullName": "DevHome.Dashboard.DashboardView", + "viewModelFullName": "DevHome.Dashboard.DashboardViewModel" + }, + { + "identity": "DevHome.SetupFlow", + "assembly": "DevHome.SetupFlow", + "viewFullName": "DevHome.SetupFlow.Views.SetupFlowPage", + "viewModelFullName": "DevHome.SetupFlow.ViewModels.SetupFlowViewModel" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/Package.appinstaller b/src/Package.appinstaller new file mode 100644 index 0000000000..53ec182694 --- /dev/null +++ b/src/Package.appinstaller @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest new file mode 100644 index 0000000000..46b000169c --- /dev/null +++ b/src/Package.appxmanifest @@ -0,0 +1,42 @@ + + + + + Dev Home + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + com.microsoft.devhome + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Properties/launchsettings.json b/src/Properties/launchsettings.json new file mode 100644 index 0000000000..9d40062577 --- /dev/null +++ b/src/Properties/launchsettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "DevHome (Package)": { + "commandName": "MsixPackage" + }, + "DevHome (Unpackaged)": { + "commandName": "Project" + } + } +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000000..723a8306c5 --- /dev/null +++ b/src/README.md @@ -0,0 +1,27 @@ +*Recommended Markdown Viewer: [Markdown Editor](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor2)* + +## Getting Started + +Browse and address `TODO:` comments in `View -> Task List` to learn the codebase and understand next steps for turning the generated code into production code. + +Explore the [WinUI Gallery](https://www.microsoft.com/store/productId/9P3JFPWWDZRC) to learn about available controls and design patterns. + +Relaunch Template Studio to modify the project by right-clicking on the project in `View -> Solution Explorer` then selecting `Add -> New Item (Template Studio)`. + +## Publishing + +For projects with MSIX packaging, right-click on the application project and select `Package and Publish -> Create App Packages...` to create an MSIX package. + +For projects without MSIX packaging, follow the [deployment guide](https://docs.microsoft.com/windows/apps/windows-app-sdk/deploy-unpackaged-apps) or add the `Self-Contained` Feature to enable xcopy deployment. + +## CI Pipelines + +See [README.md](https://github.com/microsoft/TemplateStudio/blob/main/docs/WinUI/pipelines/README.md) for guidance on building and testing projects in CI pipelines. + +## Changelog + +See [releases](https://github.com/microsoft/TemplateStudio/releases) and [milestones](https://github.com/microsoft/TemplateStudio/milestones). + +## Feedback + +Bugs and feature requests should be filed at https://aka.ms/templatestudio. diff --git a/src/Services/ActivationService.cs b/src/Services/ActivationService.cs new file mode 100644 index 0000000000..2fcbb0b5d6 --- /dev/null +++ b/src/Services/ActivationService.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Activation; +using DevHome.Common.Extensions; +using DevHome.Contracts.Services; +using DevHome.Views; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Services; + +public class ActivationService : IActivationService +{ + private readonly ActivationHandler _defaultHandler; + private readonly IEnumerable _activationHandlers; + private readonly IThemeSelectorService _themeSelectorService; + private UIElement? _shell; + + public ActivationService(ActivationHandler defaultHandler, IEnumerable activationHandlers, IThemeSelectorService themeSelectorService) + { + _defaultHandler = defaultHandler; + _activationHandlers = activationHandlers; + _themeSelectorService = themeSelectorService; + } + + public async Task ActivateAsync(object activationArgs) + { + // Execute tasks before activation. + await InitializeAsync(); + + // Set the MainWindow Content. + if (App.MainWindow.Content == null) + { + _shell = Application.Current.GetService(); + App.MainWindow.Content = _shell ?? new Frame(); + } + + // Handle activation via ActivationHandlers. + await HandleActivationAsync(activationArgs); + + // Activate the MainWindow. + App.MainWindow.Activate(); + + // Execute tasks after activation. + await StartupAsync(); + } + + private async Task HandleActivationAsync(object activationArgs) + { + var activationHandler = _activationHandlers.FirstOrDefault(h => h.CanHandle(activationArgs)); + + if (activationHandler != null) + { + await activationHandler.HandleAsync(activationArgs); + } + + if (_defaultHandler.CanHandle(activationArgs)) + { + await _defaultHandler.HandleAsync(activationArgs); + } + } + + private async Task InitializeAsync() + { + await _themeSelectorService.InitializeAsync().ConfigureAwait(false); + await Task.CompletedTask; + } + + private async Task StartupAsync() + { + await _themeSelectorService.SetRequestedThemeAsync(); + await Task.CompletedTask; + } +} diff --git a/src/Services/FileService.cs b/src/Services/FileService.cs new file mode 100644 index 0000000000..7d7e82f41f --- /dev/null +++ b/src/Services/FileService.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text; +using DevHome.Core.Contracts.Services; +using Newtonsoft.Json; + +namespace DevHome.Core.Services; + +public class FileService : IFileService +{ +#pragma warning disable CS8603 // Possible null reference return. + public T Read(string folderPath, string fileName) + { + var path = Path.Combine(folderPath, fileName); + if (File.Exists(path)) + { + var json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } + + return default; + } +#pragma warning restore CS8603 // Possible null reference return. + + public void Save(string folderPath, string fileName, T content) + { + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + var fileContent = JsonConvert.SerializeObject(content); + File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, Encoding.UTF8); + } + + public void Delete(string folderPath, string fileName) + { + if (fileName != null && File.Exists(Path.Combine(folderPath, fileName))) + { + File.Delete(Path.Combine(folderPath, fileName)); + } + } +} diff --git a/src/Services/GitWatcher.cs b/src/Services/GitWatcher.cs new file mode 100644 index 0000000000..911943b557 --- /dev/null +++ b/src/Services/GitWatcher.cs @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text.RegularExpressions; +using DevHome.Contracts.Services; + +namespace DevHome.Services; + +public class GitWatcher : IGitWatcher +{ + private static readonly Lazy LazyInstance = new (() => new ()); + + public static GitWatcher Instance => LazyInstance.Value; + + public event EventHandler? GitRepositoryCreated; + + public event EventHandler? GitRepositoryDeleted; + + private readonly Dictionary newRepoWatchers; + private readonly Dictionary existingRepoWatchers; + + // Used to protect two dictionaries above from simultaneous modification + private readonly object modificationLock = new (); + + // Checks for one of the following formats of drive roots (must be a full string match): + // c: + // D:\ + // \\?\X: + private static readonly Regex RootIsDrive = new (@"^(([a-z]:\\?)|(\\\\\?\\[a-z]:))$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private GitWatcher() + { + // TODO: Add rehydration logic either here or in core app initialization code + newRepoWatchers = new (); + existingRepoWatchers = new (); + } + + public void AddTrackedRepositories(Collection repositoryPaths) + { + lock (modificationLock) + { + repositoryPaths.ToList().ForEach( + (repositoryPath) => + { + // Path must be fully qualified to a root drive, i.e. not a UNC path + if (!Path.IsPathFullyQualified(repositoryPath) || + (repositoryPath.IndexOfAny(Path.GetInvalidPathChars()) != -1) || + !RootIsDrive.IsMatch(Path.GetPathRoot(repositoryPath)!)) + { + throw new ArgumentException("Path is not fully qualified or is a UNC path."); + } + + var path = repositoryPath.ToLower(CultureInfo.InvariantCulture); + AddNewRepoWatcher(path); + }); + } + } + + public void RemoveTrackedRepositories(Collection repositoryPaths) + { + lock (modificationLock) + { + repositoryPaths.ToList().ForEach( + (repositoryPath) => + { + var path = repositoryPath.ToLower(CultureInfo.InvariantCulture); + existingRepoWatchers.Remove(path); + }); + } + } + + public List GetTrackedRepositories() => existingRepoWatchers.Keys.ToList(); + + public void SetMonitoredSources(Collection? sources, bool append = false) + { + lock (modificationLock) + { + if (!append) + { + newRepoWatchers.Clear(); + } + + sources?.ToList().ForEach( + (source) => + { + // TODO: handle WSL paths + FileSystemWatcher watcher = new (source) + { + NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.CreationTime + | NotifyFilters.FileName + | NotifyFilters.Size + | NotifyFilters.LastAccess, + + IncludeSubdirectories = true, + Filter = "description", + }; + + watcher.Created += RepoSentinelFileCreated; + + watcher.EnableRaisingEvents = true; + + newRepoWatchers.Add(source, watcher); + }); + } + } + + public bool RemoveMonitoredSource(string source) + { + lock (modificationLock) + { + return newRepoWatchers.Remove(source); + } + } + + public List GetMonitoredSources() => newRepoWatchers.Keys.ToList(); + + public IGitFileWatcher CreateFileWatcher(string filePattern) + { + return new GitFileWatcher(this, filePattern); + } + + private void AddNewRepoWatcher(string path) + { + lock (modificationLock) + { + FileSystemWatcher deletionWatcher = new (path + @"\.git") + { + NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.CreationTime + | NotifyFilters.FileName + | NotifyFilters.Size + | NotifyFilters.LastAccess, + + Filter = @"HEAD", + }; + + deletionWatcher.Deleted += RepoSentinelFileDeleted; + + deletionWatcher.EnableRaisingEvents = true; + + existingRepoWatchers.Add(path, deletionWatcher); + } + } + + // Gets the root of the repository from a path like C:\...\rootOfRepo\.git\description + // Essentially just goes up two levels, with associated validation + private static string GetRepoRootFromFileInGitFolder(string path) + { + var result = Path.GetDirectoryName(path); + if (result == null) + { + throw new FileNotFoundException(); + } + + result = Path.GetDirectoryName(result); + if (result == null) + { + throw new FileNotFoundException(); + } + + return result; + } + + private void RepoSentinelFileCreated(object sender, FileSystemEventArgs e) + { + lock (modificationLock) + { + if (!e.FullPath.Contains(@"\.git\")) + { + return; + } + + var path = GetRepoRootFromFileInGitFolder(e.FullPath); + + // TODO: Linux filesystems may recognize upper- and lowercase paths as distinct + path = path.ToLower(CultureInfo.InvariantCulture); + if (!existingRepoWatchers.ContainsKey(path)) + { + AddNewRepoWatcher(path); + GitRepositoryCreated?.Invoke(this, new GitRepositoryChangedEventArgs(GitRepositoryChangeType.Created, path)); + } + } + } + + private void RepoSentinelFileDeleted(object sender, FileSystemEventArgs e) + { + lock (modificationLock) + { + var path = GetRepoRootFromFileInGitFolder(e.FullPath); + + path = path.ToLower(CultureInfo.InvariantCulture); + if (existingRepoWatchers.Remove(path)) + { + GitRepositoryDeleted?.Invoke(this, new GitRepositoryChangedEventArgs(GitRepositoryChangeType.Deleted, path)); + } + } + } +} + +public class GitFileWatcher : IGitFileWatcher +{ + private readonly Dictionary watchers; + private readonly GitWatcher owner; + + // Used to protect "watchers" from simultaneous modification + private readonly object modificationLock = new (); + + public bool IsOpen { get; private set; } + + public string Filter { get; } + + public event EventHandler? FileCreated; + + public event EventHandler? FileModified; + + public event EventHandler? FileDeleted; + + public GitFileWatcher(GitWatcher owner, string filePattern) + { + this.owner = owner; + Filter = filePattern; + + watchers = new (); + + owner.GitRepositoryCreated += OnRepoCreated; + owner.GitRepositoryDeleted += OnRepoDeleted; + + owner.GetTrackedRepositories().ForEach((repository) => CreateWatcher(filePattern, repository)); + } + + private void CreateWatcher(string filePattern, string repository) + { + FileSystemWatcher watcher = new (repository) + { + NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.CreationTime + | NotifyFilters.FileName + | NotifyFilters.Size + | NotifyFilters.LastAccess, + + IncludeSubdirectories = true, + Filter = filePattern, + }; + + watcher.Created += WatchedFileCreated; + watcher.Changed += WatchedFileChanged; + watcher.Deleted += WatchedFileDeleted; + + lock (modificationLock) + { + var key = repository.ToLower(CultureInfo.InvariantCulture); + if (!watchers.ContainsKey(key)) + { + watcher.EnableRaisingEvents = true; + watchers.Add(key, watcher); + } + } + } + + private void WatchedFileChanged(object sender, FileSystemEventArgs e) + { + foreach (var watcher in watchers) + { + if (e.FullPath.StartsWith(watcher.Key, StringComparison.InvariantCultureIgnoreCase)) + { + FileModified?.Invoke(this, new GitFileChangedEventArgs(GitFileChangeType.Modified, watcher.Key, e.FullPath)); + return; + } + } + + throw new DirectoryNotFoundException(); + } + + private void WatchedFileCreated(object sender, FileSystemEventArgs e) + { + foreach (var watcher in watchers) + { + if (e.FullPath.StartsWith(watcher.Key, StringComparison.InvariantCultureIgnoreCase)) + { + FileCreated?.Invoke(this, new GitFileChangedEventArgs(GitFileChangeType.Created, watcher.Key, e.FullPath)); + return; + } + } + + throw new DirectoryNotFoundException(); + } + + private void WatchedFileDeleted(object sender, FileSystemEventArgs e) + { + foreach (var watcher in watchers) + { + if (e.FullPath.StartsWith(watcher.Key, StringComparison.InvariantCultureIgnoreCase)) + { + FileDeleted?.Invoke(this, new GitFileChangedEventArgs(GitFileChangeType.Deleted, watcher.Key, e.FullPath)); + return; + } + } + + throw new DirectoryNotFoundException(); + } + + public void Close() + { + IsOpen = false; + + owner.GitRepositoryCreated -= OnRepoCreated; + owner.GitRepositoryDeleted -= OnRepoDeleted; + + watchers.Clear(); + } + + private void OnRepoCreated(object? sender, GitRepositoryChangedEventArgs e) + { + CreateWatcher(Filter, e.RepositoryPath); + } + + private void OnRepoDeleted(object? sender, GitRepositoryChangedEventArgs e) + { + lock (modificationLock) + { + watchers.Remove(Filter); + } + } + + ~GitFileWatcher() + { + if (IsOpen) + { + owner.GitRepositoryCreated -= OnRepoCreated; + owner.GitRepositoryDeleted -= OnRepoDeleted; + } + } +} diff --git a/src/Services/LocalSettingsService.cs b/src/Services/LocalSettingsService.cs new file mode 100644 index 0000000000..380055eb58 --- /dev/null +++ b/src/Services/LocalSettingsService.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Contracts.Services; +using DevHome.Core.Contracts.Services; +using DevHome.Core.Helpers; +using DevHome.Helpers; +using DevHome.Models; +using Microsoft.Extensions.Options; +using Windows.ApplicationModel; +using Windows.Storage; + +namespace DevHome.Services; + +public class LocalSettingsService : ILocalSettingsService +{ + private const string _defaultApplicationDataFolder = "DevHome/ApplicationData"; + private const string _defaultLocalSettingsFile = "LocalSettings.json"; + + private readonly IFileService _fileService; + private readonly LocalSettingsOptions _options; + + private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + private readonly string _applicationDataFolder; + private readonly string _localsettingsFile; + + private IDictionary _settings; + + private bool _isInitialized; + + public LocalSettingsService(IFileService fileService, IOptions options) + { + _fileService = fileService; + _options = options.Value; + + _applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? _defaultApplicationDataFolder); + _localsettingsFile = _options.LocalSettingsFile ?? _defaultLocalSettingsFile; + + _settings = new Dictionary(); + } + + private async Task InitializeAsync() + { + if (!_isInitialized) + { + _settings = await Task.Run(() => _fileService.Read>(_applicationDataFolder, _localsettingsFile)) ?? new Dictionary(); + + _isInitialized = true; + } + } + + public async Task ReadSettingAsync(string key) + { + if (RuntimeHelper.IsMSIX) + { + if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj)) + { + return await Json.ToObjectAsync((string)obj); + } + } + else + { + await InitializeAsync(); + + if (_settings != null && _settings.TryGetValue(key, out var obj)) + { + return await Json.ToObjectAsync((string)obj); + } + } + + return default; + } + + public async Task SaveSettingAsync(string key, T value) + { + if (RuntimeHelper.IsMSIX) + { + ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!); + } + else + { + await InitializeAsync(); + + _settings[key] = await Json.StringifyAsync(value!); + + await Task.Run(() => _fileService.Save(_applicationDataFolder, _localsettingsFile, _settings)); + } + } +} diff --git a/src/Services/NavigationService.cs b/src/Services/NavigationService.cs new file mode 100644 index 0000000000..f4cd46b8f3 --- /dev/null +++ b/src/Services/NavigationService.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.Contracts.ViewModels; +using DevHome.Helpers; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.Services; + +// For more information on navigation between pages see +// https://github.com/microsoft/TemplateStudio/blob/main/docs/WinUI/navigation.md +public class NavigationService : INavigationService +{ + private readonly IPageService _pageService; + private object? _lastParameterUsed; + private Frame? _frame; + + public event NavigatedEventHandler? Navigated; + + public Frame? Frame + { + get + { + if (_frame == null) + { + _frame = App.MainWindow.Content as Frame; + RegisterFrameEvents(); + } + + return _frame; + } + + set + { + UnregisterFrameEvents(); + _frame = value; + RegisterFrameEvents(); + } + } + + [MemberNotNullWhen(true, nameof(Frame), nameof(_frame))] + public bool CanGoBack => Frame != null && Frame.CanGoBack; + + public NavigationService(IPageService pageService) + { + _pageService = pageService; + } + + private void RegisterFrameEvents() + { + if (_frame != null) + { + _frame.Navigated += OnNavigated; + } + } + + private void UnregisterFrameEvents() + { + if (_frame != null) + { + _frame.Navigated -= OnNavigated; + } + } + + public bool GoBack() + { + if (CanGoBack) + { + var vmBeforeNavigation = _frame.GetPageViewModel(); + _frame.GoBack(); + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + + return true; + } + + return false; + } + + public bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false) + { + var pageType = _pageService.GetPageType(pageKey); + + if (_frame != null && (_frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(_lastParameterUsed)))) + { + _frame.Tag = clearNavigation; + var vmBeforeNavigation = _frame.GetPageViewModel(); + var navigated = _frame.Navigate(pageType, parameter); + if (navigated) + { + _lastParameterUsed = parameter; + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + } + + return navigated; + } + + return false; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame) + { + var clearNavigation = (bool)frame.Tag; + if (clearNavigation) + { + frame.BackStack.Clear(); + } + + if (frame.GetPageViewModel() is INavigationAware navigationAware) + { + navigationAware.OnNavigatedTo(e.Parameter); + } + + Navigated?.Invoke(sender, e); + } + } +} diff --git a/src/Services/NavigationViewService.cs b/src/Services/NavigationViewService.cs new file mode 100644 index 0000000000..4ba09b445a --- /dev/null +++ b/src/Services/NavigationViewService.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.Helpers; +using DevHome.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Services; + +public class NavigationViewService : INavigationViewService +{ + private readonly INavigationService _navigationService; + + private readonly IPageService _pageService; + + private NavigationView? _navigationView; + + public IList? MenuItems => _navigationView?.MenuItems; + + public IList? FooterMenuItems => _navigationView?.FooterMenuItems; + + public object? SettingsItem => _navigationView?.SettingsItem; + + public NavigationViewService(INavigationService navigationService, IPageService pageService) + { + _navigationService = navigationService; + _pageService = pageService; + } + + [MemberNotNull(nameof(_navigationView))] + public void Initialize(NavigationView navigationView) + { + _navigationView = navigationView; + _navigationView.BackRequested += OnBackRequested; + _navigationView.ItemInvoked += OnItemInvoked; + } + + public void UnregisterEvents() + { + if (_navigationView != null) + { + _navigationView.BackRequested -= OnBackRequested; + _navigationView.ItemInvoked -= OnItemInvoked; + } + } + + public NavigationViewItem? GetSelectedItem(Type pageType) + { + if (_navigationView != null) + { + return GetSelectedItem(_navigationView.MenuItems, pageType) ?? GetSelectedItem(_navigationView.FooterMenuItems, pageType); + } + + return null; + } + + private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args) => _navigationService.GoBack(); + + private void OnItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + if (args.IsSettingsInvoked) + { + _navigationService.NavigateTo(typeof(SettingsViewModel).FullName!); + } + else + { + var selectedItem = args.InvokedItemContainer as NavigationViewItem; + + if (selectedItem?.GetValue(NavigationHelper.NavigateToProperty) is string pageKey) + { + _navigationService.NavigateTo(pageKey); + } + } + } + + private NavigationViewItem? GetSelectedItem(IEnumerable menuItems, Type pageType) + { + foreach (var item in menuItems.OfType()) + { + if (IsMenuItemForPageType(item, pageType)) + { + return item; + } + + var selectedChild = GetSelectedItem(item.MenuItems, pageType); + if (selectedChild != null) + { + return selectedChild; + } + } + + return null; + } + + private bool IsMenuItemForPageType(NavigationViewItem menuItem, Type sourcePageType) + { + if (menuItem.GetValue(NavigationHelper.NavigateToProperty) is string pageKey) + { + return _pageService.GetPageType(pageKey) == sourcePageType; + } + + return false; + } +} diff --git a/src/Services/PageService.cs b/src/Services/PageService.cs new file mode 100644 index 0000000000..070c60845a --- /dev/null +++ b/src/Services/PageService.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Reflection; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common; +using DevHome.Contracts.Services; +using DevHome.Helpers; +using DevHome.ViewModels; +using DevHome.Views; +using Microsoft.UI.Xaml.Controls; +using Newtonsoft.Json; +using WinRT; + +namespace DevHome.Services; + +public class PageService : IPageService +{ + private readonly Dictionary _pages = new (); + + public PageService() + { + Configure(); + + Configure(); + + foreach (var group in App.NavConfig.NavMenu.Groups) + { + foreach (var tool in group.Tools) + { + var toolType = from assembly in AppDomain.CurrentDomain.GetAssemblies() + where assembly.GetName().Name == tool.Assembly + select assembly.GetType(tool.ViewFullName); + + Configure(tool.ViewModelFullName, toolType.First()); + } + } + } + + public Type GetPageType(string key) + { + Type? pageType; + lock (_pages) + { + if (!_pages.TryGetValue(key, out pageType)) + { + throw new ArgumentException($"Page not found: {key}. Did you forget to call PageService.Configure?"); + } + } + + return pageType; + } + + private void Configure() + where T_VM : ObservableObject + where T_V : Page + { + lock (_pages) + { + var key = typeof(T_VM).FullName!; + if (_pages.ContainsKey(key)) + { + throw new ArgumentException($"The key {key} is already configured in PageService"); + } + + var type = typeof(T_V); + if (_pages.Any(p => p.Value == type)) + { + throw new ArgumentException($"This type is already configured with key {_pages.First(p => p.Value == type).Key}"); + } + + _pages.Add(key, type); + } + } + + private void Configure(string t_vm, Type t_v) + { + lock (_pages) + { + if (_pages.ContainsKey(t_vm)) + { + throw new ArgumentException($"The key {t_vm} is already configured in PageService"); + } + + if (_pages.Any(p => p.Value == t_v)) + { + throw new ArgumentException($"This type is already configured with key {_pages.First(p => p.Value == t_v).Key}"); + } + + _pages.Add(t_vm, t_v); + } + } +} diff --git a/src/Services/PluginService.cs b/src/Services/PluginService.cs new file mode 100644 index 0000000000..79d5fca784 --- /dev/null +++ b/src/Services/PluginService.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common; +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.Helpers; +using DevHome.Models; +using DevHome.SetupFlow.RepoConfig; +using DevHome.ViewModels; +using DevHome.Views; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; +using Newtonsoft.Json; +using Windows.ApplicationModel.AppExtensions; +using Windows.ApplicationModel.Background; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage; +using WinRT; + +namespace DevHome.Services; + +public class PluginService : IPluginService +{ +#pragma warning disable IDE0044 // Add readonly modifier + private static List installedPlugins = new (); +#pragma warning restore IDE0044 // Add readonly modifier + + public async Task> GetInstalledPluginsAsync() + { + if (installedPlugins.Count == 0) + { + var extensions = await AppExtensionCatalog.Open("com.microsoft.devhome").FindAllAsync(); + foreach (var extension in extensions) + { + var properties = await extension.GetExtensionPropertiesAsync(); + var devHomeProvider = GetSubPropertySet(properties, "DevHomeProvider"); + if (devHomeProvider is null) + { + continue; + } + + var activation = GetSubPropertySet(devHomeProvider, "Activation"); + if (activation is null) + { + continue; + } + + var comActivation = GetSubPropertySet(activation, "CreateInstance"); + if (comActivation is null) + { + continue; + } + + var classId = GetProperty(comActivation, "@ClassId"); + if (classId is null) + { + continue; + } + + var name = extension.DisplayName; + var pluginWrapper = new PluginWrapper(name, classId); + + var supportedInterfaces = GetSubPropertySet(devHomeProvider, "SupportedInterfaces"); + if (supportedInterfaces is not null) + { + foreach (var supportedInterface in supportedInterfaces) + { + ProviderType pt; + if (Enum.TryParse(supportedInterface.Key, out pt)) + { + pluginWrapper.AddProviderType(pt); + } + else + { + // TODO: throw warning or fire notification that plugin declared unsupported plugin interface + } + } + } + + installedPlugins.Add(pluginWrapper); + } + } + + return installedPlugins; + } + + public async Task> GetInstalledPluginsAsync(ProviderType providerType) + { + var installedPlugins = await GetInstalledPluginsAsync(); + + List filteredPlugins = new (); + foreach (var installedPlugin in installedPlugins) + { + if (installedPlugin.HasProviderType(providerType)) + { + filteredPlugins.Add(installedPlugin); + } + } + + return filteredPlugins; + } + + private IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) + { + return propSet[name] as IPropertySet; + } + + private string? GetProperty(IPropertySet propSet, string name) + { + return propSet[name] as string; + } +} + +public class Ole32 +{ + // https://docs.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx + public const int CLSCTXLOCALSERVER = 0x4; + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance + [DllImport(nameof(Ole32))] + +#pragma warning disable CA1401 // P/Invokes should not be visible + public static extern int CoCreateInstance( + [In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, + IntPtr pUnkOuter, + uint dwClsContext, + [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out IntPtr ppv); +#pragma warning restore CA1401 // P/Invokes should not be visible +} diff --git a/src/Services/ThemeSelectorService.cs b/src/Services/ThemeSelectorService.cs new file mode 100644 index 0000000000..9bf05acf95 --- /dev/null +++ b/src/Services/ThemeSelectorService.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Contracts.Services; +using DevHome.Helpers; +using Microsoft.UI.Xaml; + +namespace DevHome.Services; + +public class ThemeSelectorService : IThemeSelectorService +{ + private const string SettingsKey = "AppBackgroundRequestedTheme"; + + public ElementTheme Theme { get; set; } = ElementTheme.Default; + + private readonly ILocalSettingsService _localSettingsService; + + public ThemeSelectorService(ILocalSettingsService localSettingsService) + { + _localSettingsService = localSettingsService; + } + + public async Task InitializeAsync() + { + Theme = await LoadThemeFromSettingsAsync(); + await Task.CompletedTask; + } + + public async Task SetThemeAsync(ElementTheme theme) + { + Theme = theme; + + await SetRequestedThemeAsync(); + await SaveThemeInSettingsAsync(Theme); + } + + public async Task SetRequestedThemeAsync() + { + if (App.MainWindow.Content is FrameworkElement rootElement) + { + rootElement.RequestedTheme = Theme; + + TitleBarHelper.UpdateTitleBar(Theme); + } + + await Task.CompletedTask; + } + + private async Task LoadThemeFromSettingsAsync() + { + var themeName = await _localSettingsService.ReadSettingAsync(SettingsKey); + + if (Enum.TryParse(themeName, out ElementTheme cacheTheme)) + { + return cacheTheme; + } + + return ElementTheme.Default; + } + + private async Task SaveThemeInSettingsAsync(ElementTheme theme) + { + await _localSettingsService.SaveSettingAsync(SettingsKey, theme.ToString()); + } +} diff --git a/src/Strings/en-us/Resources.resw b/src/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..7e7fbc5082 --- /dev/null +++ b/src/Strings/en-us/Resources.resw @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Dev Home + The name of our application + + + Dev Home + The name of our application + + + Personalization + + + Theme + + + Light + + + Dark + + + Default + + + About this application + + + TODO: Replace with your app description. + + + Privacy Statement + + + https://YourPrivacyUrlGoesHere/ + + + Dev Home follows the + Dev Home abides by Microsoft Open Source guidance + + + Propose something new + Suggest a new feature that we don't currently have + + + Suggest a Feature/Improvement + FeatureImprovement is to suggest a useful feature that the app does not currently have + + + Get Started + Begin to fill out the feature request content dialog + + + Report errors or unexpected behavior + Tell us the errors that were happening + + + Report a Bug + Tell us about an error that is happening + + + Get Started + Begin to fill out the report bug content dialog + + + Cancel + Close out of the content dialog + + + Preview Issue on GitHub + Navigate to the GitHub issue to see the issue form + + + Report a Bug + Tell us about an error + + + Steps to recreate + What to do to reproduce the error + + + Detailed steps help us reproduce the bug. + Specific details on how to recreate the error + + + Actual behavior + What is currently happening + + + What happened instead? + The incorrect behavior that was happening instead of the correct behavior + + + Expected behavior + The correct behavior that was supposed to have + + + What were you expecting? + What was supposed to happen + + + Include A/B experiment info + Opt into including experimentation information into your issue template + + + Include installed plugins + Opt into including information on plugins into your issue template + + + Include my system information + Opt into including the computer information into your issue template + + + Subject/Title + SubjectTitle is the topic of the issue that the user wants to report + + + Title + The topic of the issue that the user wants to report + + + Create a custom extension with our step-by-step guide + Documentation on how to create an extension + + + Build an Extension + Creating a new extension + + + Documentation + Description or instruction on building an extension + + + Report incorrect translations + Tell us about a translation that is not correct + + + Localization/Translation Issue + LocalizationTranslation means that there are words and language being translated that are incorrect + + + Get Started + Begin to fill out the translation issue content dialog + + + Cancel + Close out of the content dialog + + + Finish Report on GitHub + Continue the report on the GitHub template + + + Report a Localization/Translation Issue + LocalizationTranslation means that there are words and language being translated that are incorrect + + + Subject/Title + SubjectTitle is the topic of the issue that the user wants to report. + + + Title + The topic of the issue that the user wants to report + + + Please review our security policy for more details + Look at the guidance on security + + + Report a Security Vulnerability + Tell us about potential security concerns + + + View Policy + Click on this link if you want to learn more and read the security policy. View is an action. + + + Language Affected + The language that is being incorrectly translated + + + E.g. "German" + Example of a language is German + + + Description of the new feature / improvement + More details on the feature that is suggested + + + What is the expected behavior of the proposed feature/improvement? + What is the feature supposed to do and how is it important + + + Cancel + Exit out of the content dialog + + + Preview Issue on GitHub + Go to the issue on the GitHub template + + + Suggest a Feature/Improvement + Tell us about a feature that you would like to see. FeatureImprovement is a feature that would be useful for the app to have that it doesn't currently have already. + + + Subject/Title + SubjectTitle topic of the feature that the user wants to reccomend creating + + + Title + The topic of the feature that the user wants to reccomend creating + + + Scenario when this would be used + When you would apply the feature that you are suggesting + + + What is the scenario this would be used in? Why is this important to your + workflow? + Explaining why we would need this feature + + + Supporting Information + More information you can give on suggesting a feature + + + Any additional evidence, tweets, posts, etc. that provide more context. + What are some additional resoures about this feature + + + to learn more. + Go to this link to understand more about documentation + + + See + Look at this link for more information + + + Documentation for Dev Home + Guidance on the project + + + Microsoft Open Source of Conduct + Open source documentaiton guidance + + + Before you report an issue, you may want to review + View the guidance on reporting an issue + + + Before you make a suggestion, you may want to review + Prior to making a suggestion, look at our guidance + + + the guidance we provide. + This is the link to the guidance we have + + + https://yourissueguidancelinkgoeshere/ + httpsyourissueguidancelinkgoeshere is a dummy issue guidance link that goes here. Will be replaced with an aka.ms link later. + + + https://YourDocumentationGuidanceLinkGoesHere/ + httpsYourDocumentationGuidanceLinkGoesHere is is a dummy documentation link that goes here. Will be replaced with an aka.ms link later. + + + https://YourBuildExtensionDocGoesHere/ + httpsYourBuildExtensionDocGoesHere is a dummy buliding extension documentation link that goes here. Will be replaced with an aka.ms link later. + + + https://YourOpenSourceLinkGoesHere/ + httpsYourOpenSourceLinkGoesHere is a dummy open source guidance link that goes here. Will be replaced with an aka.ms link later. + + + https://YourSecurityLinkGoesHere/ + httpsYourSecurityLinkGoesHere is a dummy security guidance link that goes here. Will be replaced with an aka.ms link later. + + \ No newline at end of file diff --git a/src/Styles/Feedback_ThemeResources.xaml b/src/Styles/Feedback_ThemeResources.xaml new file mode 100644 index 0000000000..db226ccce5 --- /dev/null +++ b/src/Styles/Feedback_ThemeResources.xaml @@ -0,0 +1,18 @@ + + + + + + 0,40,-25,0 + + 0,0,30,0 + + 0,-20,0,0 + + 100 + + 430 + + diff --git a/src/Styles/FontFamilies.xaml b/src/Styles/FontFamilies.xaml new file mode 100644 index 0000000000..8c058508be --- /dev/null +++ b/src/Styles/FontFamilies.xaml @@ -0,0 +1,7 @@ + + + ms-appx:///Assets/Fonts/CascadiaMono.ttf#Cascadia Mono,MS Gothic, NSimSun + diff --git a/src/Styles/FontSizes.xaml b/src/Styles/FontSizes.xaml new file mode 100644 index 0000000000..eb9f54a0d2 --- /dev/null +++ b/src/Styles/FontSizes.xaml @@ -0,0 +1,9 @@ + + + 24 + + 16 + + diff --git a/src/Styles/TextBlock.xaml b/src/Styles/TextBlock.xaml new file mode 100644 index 0000000000..552d15380f --- /dev/null +++ b/src/Styles/TextBlock.xaml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/src/Styles/Thickness.xaml b/src/Styles/Thickness.xaml new file mode 100644 index 0000000000..281f42c7fb --- /dev/null +++ b/src/Styles/Thickness.xaml @@ -0,0 +1,36 @@ + + + 0,36,0,0 + 0,36,0,36 + + 0,24,0,0 + 0,24,0,24 + 24,0,24,0 + 0,0,0,24 + + 12,0,0,0 + 12,0,12,0 + 0,12,0,0 + 0,0,12,0 + 0,12,0,12 + + 8,0,0,0 + 0,8,0,0 + 8,8,8,8 + + 0,4,0,0 + 4,4,4,4 + + 1,1,0,0 + 8,0,0,0 + 0,48,0,0 + 56,34,0,0 + 56,24,56,0 + + 36,24,36,0 + + -12,4,0,0 + + diff --git a/src/TemplateStudio.xml b/src/TemplateStudio.xml new file mode 100644 index 0000000000..11390d46f3 --- /dev/null +++ b/src/TemplateStudio.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/Usings.cs b/src/Usings.cs new file mode 100644 index 0000000000..31bf8ecd0d --- /dev/null +++ b/src/Usings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +global using WinUIEx; diff --git a/src/ViewModels/FeedbackViewModel.cs b/src/ViewModels/FeedbackViewModel.cs new file mode 100644 index 0000000000..35ec10513a --- /dev/null +++ b/src/ViewModels/FeedbackViewModel.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Reflection; +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Contracts.Services; +using DevHome.Helpers; +using Microsoft.UI.Xaml; +using Windows.ApplicationModel; + +namespace DevHome.ViewModels; + +public class FeedbackViewModel : ObservableRecipient +{ + private readonly IThemeSelectorService _themeSelectorService; + private ElementTheme _elementTheme; + private string _versionDescription; + + public ElementTheme ElementTheme + { + get => _elementTheme; + set => SetProperty(ref _elementTheme, value); + } + + public string VersionDescription + { + get => _versionDescription; + set => SetProperty(ref _versionDescription, value); + } + + public ICommand SwitchThemeCommand + { + get; + } + + public FeedbackViewModel(IThemeSelectorService themeSelectorService) + { + _themeSelectorService = themeSelectorService; + _elementTheme = _themeSelectorService.Theme; + _versionDescription = GetVersionDescription(); + + SwitchThemeCommand = new RelayCommand( + async (param) => + { + if (ElementTheme != param) + { + ElementTheme = param; + await _themeSelectorService.SetThemeAsync(param); + } + }); + } + + private static string GetVersionDescription() + { + Version version; + + if (RuntimeHelper.IsMSIX) + { + var packageVersion = Package.Current.Id.Version; + + version = new (packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision); + } + else + { + version = Assembly.GetExecutingAssembly().GetName().Version!; + } + + return $"{"AppDisplayName".GetLocalized()} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } +} diff --git a/src/ViewModels/SettingsViewModel.cs b/src/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000000..92dcf14bb6 --- /dev/null +++ b/src/ViewModels/SettingsViewModel.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Reflection; +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Contracts.Services; +using DevHome.Helpers; +using Microsoft.UI.Xaml; +using Windows.ApplicationModel; + +namespace DevHome.ViewModels; + +public class SettingsViewModel : ObservableRecipient +{ + private readonly IThemeSelectorService _themeSelectorService; + private ElementTheme _elementTheme; + private string _versionDescription; + + public ElementTheme ElementTheme + { + get => _elementTheme; + set => SetProperty(ref _elementTheme, value); + } + + public string VersionDescription + { + get => _versionDescription; + set => SetProperty(ref _versionDescription, value); + } + + public ICommand SwitchThemeCommand + { + get; + } + + public SettingsViewModel(IThemeSelectorService themeSelectorService) + { + _themeSelectorService = themeSelectorService; + _elementTheme = _themeSelectorService.Theme; + _versionDescription = GetVersionDescription(); + + SwitchThemeCommand = new RelayCommand( + async (param) => + { + if (ElementTheme != param) + { + ElementTheme = param; + await _themeSelectorService.SetThemeAsync(param); + } + }); + } + + private static string GetVersionDescription() + { + Version version; + + if (RuntimeHelper.IsMSIX) + { + var packageVersion = Package.Current.Id.Version; + + version = new (packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision); + } + else + { + version = Assembly.GetExecutingAssembly().GetName().Version!; + } + + return $"{"AppDisplayName".GetLocalized()} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } +} diff --git a/src/ViewModels/ShellViewModel.cs b/src/ViewModels/ShellViewModel.cs new file mode 100644 index 0000000000..404cb88dd1 --- /dev/null +++ b/src/ViewModels/ShellViewModel.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.Views; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.ViewModels; + +public class ShellViewModel : ObservableRecipient +{ + private object? _selected; + + public INavigationService NavigationService + { + get; + } + + public INavigationViewService NavigationViewService + { + get; + } + + public object? Selected + { + get => _selected; + set => SetProperty(ref _selected, value); + } + + public ShellViewModel(INavigationService navigationService, INavigationViewService navigationViewService) + { + NavigationService = navigationService; + NavigationService.Navigated += OnNavigated; + NavigationViewService = navigationViewService; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (e.SourcePageType == typeof(SettingsPage)) + { + Selected = NavigationViewService.SettingsItem; + return; + } + + var selectedItem = NavigationViewService.GetSelectedItem(e.SourcePageType); + if (selectedItem != null) + { + Selected = selectedItem; + } + } +} diff --git a/src/Views/FeedbackPage.xaml b/src/Views/FeedbackPage.xaml new file mode 100644 index 0000000000..2f7170584a --- /dev/null +++ b/src/Views/FeedbackPage.xaml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs new file mode 100644 index 0000000000..2395f637ec --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Dashboard.Views; +public sealed partial class AddWidgetDialog : ContentDialog +{ + public AddWidgetDialog() + { + this.InitializeComponent(); + configurationContentFrame.Content = new WidgetConfigurationContent(); + } + + private void WidgetConfigurationNavigationView_SelectionChanged( + NavigationView sender, + NavigationViewSelectionChangedEventArgs args) + { + // Load correct adaptive card + } + + private void CancelButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + this.Hide(); + } +} diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml new file mode 100644 index 0000000000..df1a1e6b57 --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs new file mode 100644 index 0000000000..83e317cc53 --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System; +using DevHome.Common; +using DevHome.Dashboard.ViewModels; +using DevHome.Dashboard.Views; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Dashboard; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public partial class DashboardView : ToolPage +{ + public override string ShortName => "Dashboard"; + + public DashboardViewModel ViewModel { get; } + + public DashboardView() + { + this.InitializeComponent(); + } + + private async void AddWidgetButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var dialog = new AddWidgetDialog + { + // XamlRoot must be set in the case of a ContentDialog running in a Desktop app + XamlRoot = this.XamlRoot, + }; + _ = await dialog.ShowAsync(); + } +} diff --git a/tools/Dashboard/DevHome.Dashboard/Views/WidgetConfigurationContent.xaml b/tools/Dashboard/DevHome.Dashboard/Views/WidgetConfigurationContent.xaml new file mode 100644 index 0000000000..dc0de7bf2a --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/Views/WidgetConfigurationContent.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + Pull Requests + GitHub + + + + URL + + + + + + + diff --git a/tools/Dashboard/DevHome.Dashboard/Views/WidgetConfigurationContent.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/WidgetConfigurationContent.xaml.cs new file mode 100644 index 0000000000..1a6a5607cc --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/Views/WidgetConfigurationContent.xaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Dashboard.Views; + +public sealed partial class WidgetConfigurationContent : Page +{ + public WidgetConfigurationContent() + { + this.InitializeComponent(); + } +} diff --git a/tools/SampleTool/src/Class1.cs b/tools/SampleTool/src/Class1.cs new file mode 100644 index 0000000000..903718a757 --- /dev/null +++ b/tools/SampleTool/src/Class1.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SampleTool; + +public class Class1 +{ +} diff --git a/tools/SampleTool/src/SampleTool.csproj b/tools/SampleTool/src/SampleTool.csproj new file mode 100644 index 0000000000..6594fc2292 --- /dev/null +++ b/tools/SampleTool/src/SampleTool.csproj @@ -0,0 +1,29 @@ + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + SampleTool + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + diff --git a/tools/SampleTool/src/Strings/en-us/Resources.resw b/tools/SampleTool/src/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..f064e8206c --- /dev/null +++ b/tools/SampleTool/src/Strings/en-us/Resources.resw @@ -0,0 +1,9 @@ + + + + This is SampleTool + + + Sample Tool + + diff --git a/tools/SampleTool/src/ViewModels/SampleToolViewModel.cs b/tools/SampleTool/src/ViewModels/SampleToolViewModel.cs new file mode 100644 index 0000000000..d78221ef27 --- /dev/null +++ b/tools/SampleTool/src/ViewModels/SampleToolViewModel.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Tools.SampleTool.ViewModels; + +public class SampleToolViewModel : ObservableRecipient +{ + public SampleToolViewModel() + { + } +} diff --git a/tools/SampleTool/src/Views/SampleToolPage.xaml b/tools/SampleTool/src/Views/SampleToolPage.xaml new file mode 100644 index 0000000000..c2e0ccbba8 --- /dev/null +++ b/tools/SampleTool/src/Views/SampleToolPage.xaml @@ -0,0 +1,11 @@ + + + + diff --git a/tools/SampleTool/src/Views/SampleToolPage.xaml.cs b/tools/SampleTool/src/Views/SampleToolPage.xaml.cs new file mode 100644 index 0000000000..139be9b5cd --- /dev/null +++ b/tools/SampleTool/src/Views/SampleToolPage.xaml.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using DevHome.Common; +using DevHome.Telemetry; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Tools.SampleTool.ViewModels; + +namespace Tools.SampleTool.Views; + +public partial class SampleToolPage : ToolPage +{ + public override string ShortName => "SampleTool"; + + private readonly ILogger logger; + + public SampleToolViewModel ViewModel + { + get; + } + + public SampleToolPage() + { + ViewModel = new SampleToolViewModel(); + InitializeComponent(); + this.logger = LoggerFactory.Get(); + + this.logger.Log("PageLoad", LogLevel.Local, nameof(SampleToolPage)); + } +} diff --git a/tools/SampleTool/uitest/SampleTool.UITest.csproj b/tools/SampleTool/uitest/SampleTool.UITest.csproj new file mode 100644 index 0000000000..d4251916d6 --- /dev/null +++ b/tools/SampleTool/uitest/SampleTool.UITest.csproj @@ -0,0 +1,19 @@ + + + net6.0-windows10.0.19041.0 + SampleTool.UITest + x86;x64;arm64 + false + enable + true + true + resources.pri + + + + + + + + + \ No newline at end of file diff --git a/tools/SampleTool/uitest/SampleToolScenarioStandard.cs b/tools/SampleTool/uitest/SampleToolScenarioStandard.cs new file mode 100644 index 0000000000..7de3ba37a1 --- /dev/null +++ b/tools/SampleTool/uitest/SampleToolScenarioStandard.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Remote; +using static System.Collections.Specialized.BitVector32; + +namespace DevHome.Tests.UITest +{ + [TestClass] + public class SampleToolScenarioStandard : SampleToolSession + { + // [TestMethod] // Test is just a sample and should not run + public void SampleToolTest1() + { + WindowsElement title = session.FindElementByName("Sample Tool"); + Assert.AreEqual("Sample Tool", title.Text); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + Setup(context); + } + + [ClassCleanup] + public static void ClassCleanup() + { + TearDown(); + } + } +} diff --git a/tools/SampleTool/uitest/SampleToolSession.cs b/tools/SampleTool/uitest/SampleToolSession.cs new file mode 100644 index 0000000000..fd662b9f70 --- /dev/null +++ b/tools/SampleTool/uitest/SampleToolSession.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; + +namespace DevHome.Tests.UITest; + +public class SampleToolSession +{ + private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723"; + private const string DevHomeAppId = "Microsoft.DevHome_8wekyb3d8bbwe!App"; + +#pragma warning disable SA1401 // Fields should be private +#pragma warning disable CA2211 // Non-constant fields should not be visible + protected static WindowsDriver session; +#pragma warning restore CA2211 // Non-constant fields should not be visible +#pragma warning restore SA1401 // Fields should be private + + public static void Setup(TestContext context) + { + if (session == null) + { + // Create a new session to bring up an instance of the Dev Home application + // Note: Multiple calculator windows (instances) share the same process Id + AppiumOptions options = new AppiumOptions(); + options.AddAdditionalCapability("deviceName", "WindowsPC"); + options.AddAdditionalCapability("platformName", "Windows"); + options.AddAdditionalCapability("app", DevHomeAppId); + + session = new WindowsDriver(new Uri(WindowsApplicationDriverUrl), options); + Assert.IsNotNull(session); + + // Set implicit timeout to 1.5 seconds to make element search to retry every 500 ms for at most three times + session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1.5); + } + } + + public static void TearDown() + { + // Close the application and delete the session + if (session != null) + { + session.Quit(); + session = null; + } + } +} diff --git a/tools/SampleTool/unittest/Initialize.cs b/tools/SampleTool/unittest/Initialize.cs new file mode 100644 index 0000000000..b3ad24bfb0 --- /dev/null +++ b/tools/SampleTool/unittest/Initialize.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.Windows.ApplicationModel.DynamicDependency; + +[assembly: WinUITestTarget(typeof(DevHome.App))] + +namespace SampleTool.Test; + +[TestClass] +public class Initialize +{ + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + // TODO: Initialize the appropriate version of the Windows App SDK. + // This is required when testing MSIX apps that are framework-dependent on the Windows App SDK. + Bootstrap.TryInitialize(0x00010001, out var _); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + Bootstrap.Shutdown(); + } +} diff --git a/tools/SampleTool/unittest/SampleTool.UnitTest.csproj b/tools/SampleTool/unittest/SampleTool.UnitTest.csproj new file mode 100644 index 0000000000..04dfd41dae --- /dev/null +++ b/tools/SampleTool/unittest/SampleTool.UnitTest.csproj @@ -0,0 +1,24 @@ + + + net6.0-windows10.0.19041.0 + SampleTool.Test + x86;x64;arm64 + false + enable + enable + true + true + resources.pri + + + + + + + + + + + + + diff --git a/tools/SampleTool/unittest/TestClass.cs b/tools/SampleTool/unittest/TestClass.cs new file mode 100644 index 0000000000..bf8d11f874 --- /dev/null +++ b/tools/SampleTool/unittest/TestClass.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Diagnostics; + +using Microsoft.UI.Xaml.Controls; + +namespace SampleTool.Test; + +[TestClass] +public class TestClass +{ + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + Debug.WriteLine("ClassInitialize"); + } + + [ClassCleanup] + public static void ClassCleanup() + { + Debug.WriteLine("ClassCleanup"); + } + + [TestInitialize] + public void TestInitialize() + { + Debug.WriteLine("TestInitialize"); + } + + [TestCleanup] + public void TestCleanup() + { + Debug.WriteLine("TestCleanup"); + } + + [TestMethod] + public void TestMethod() + { + Assert.IsTrue(true); + } +} diff --git a/tools/SampleTool/unittest/Usings.cs b/tools/SampleTool/unittest/Usings.cs new file mode 100644 index 0000000000..037943bebe --- /dev/null +++ b/tools/SampleTool/unittest/Usings.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/AppManagementTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/AppManagementTaskGroup.cs new file mode 100644 index 0000000000..4005004977 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/AppManagementTaskGroup.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.AppManagement.Models; +using DevHome.SetupFlow.AppManagement.ViewModels; +using DevHome.SetupFlow.Common.Models; +using DevHome.SetupFlow.Common.ViewModels; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.AppManagement; + +public class AppManagementTaskGroup : ISetupTaskGroup +{ + private readonly IHost _host; + + public AppManagementTaskGroup(IHost host) + { + _host = host; + } + + private readonly IList _installTasks = new List(); + + public IEnumerable SetupTasks => _installTasks; + + public SetupPageViewModelBase GetSetupPageViewModel() => _host.CreateInstance(this); + + public ReviewTabViewModelBase GetReviewTabViewModel() => _host.CreateInstance(this); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/DevHome.SetupFlow.AppManagement.csproj b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/DevHome.SetupFlow.AppManagement.csproj new file mode 100644 index 0000000000..4425db94fd --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/DevHome.SetupFlow.AppManagement.csproj @@ -0,0 +1,71 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.AppManagement + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + + + $(DefaultXamlRuntime) + + + + + + MSBuild:Compile + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/CatalogConnectionException.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/CatalogConnectionException.cs new file mode 100644 index 0000000000..b62f698586 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/CatalogConnectionException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.AppManagement.Exceptions; + +/// +/// Exception thrown if a catalog connection failed +/// +public class CatalogConnectionException : Exception +{ + private readonly ConnectResultStatus _status; + + public CatalogConnectionException(ConnectResultStatus status) + { + _status = status; + } + + public ConnectResultStatus Status => _status; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/FindPackagesException.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/FindPackagesException.cs new file mode 100644 index 0000000000..7fdc76ce7f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/FindPackagesException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.AppManagement.Exceptions; + +/// +/// Exception thrown if a find package operation failed +/// +public class FindPackagesException : Exception +{ + private readonly FindPackagesResultStatus _status; + + public FindPackagesException(FindPackagesResultStatus status) + { + _status = status; + } + + public FindPackagesResultStatus Status => _status; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/PackageInstallException.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/PackageInstallException.cs new file mode 100644 index 0000000000..2ba03514d0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Exceptions/PackageInstallException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.AppManagement.Exceptions; + +/// +/// Exception thrown if package installation failed +/// +public class InstallPackageException : Exception +{ + private readonly InstallResultStatus _status; + + public InstallPackageException(InstallResultStatus status) + { + _status = status; + } + + public InstallResultStatus Status => _status; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/IWinGetCatalog.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/IWinGetCatalog.cs new file mode 100644 index 0000000000..71d3ab81b9 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/IWinGetCatalog.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using DevHome.SetupFlow.AppManagement.Exceptions; + +namespace DevHome.SetupFlow.AppManagement.Models; + +/// +/// Interface for a winget catalog +/// +public interface IWinGetCatalog +{ + /// + /// Gets a value indicating whether a catalog was opened. + /// + /// + public bool IsConnected + { + get; + } + + /// + /// Opens a catalog before searching. + /// This method calls ConnectAsync() from PackageManager.idl + /// + /// Exception thrown if a catalog connection failed + public Task ConnectAsync(); + + /// + /// Search for packages in this catalog. + /// Equivalent to "winget search --query {query} --source {this}" + /// + /// Search query + /// List of winget package matches + /// Exception thrown if the search packages operation failed + public Task> SearchAsync(string query); + + /// + /// Get packages by id from this catalog. + /// Equivalent to "winget search --id {packageId} --exact --source {this}" + /// + /// Set of package id + /// List of winget package matches + /// Exception thrown if the get packages operation failed + public Task> GetPackagesAsync(ISet packageIdSet); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/IWinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/IWinGetPackage.cs new file mode 100644 index 0000000000..ef7f7af3f0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/IWinGetPackage.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Threading.Tasks; +using DevHome.SetupFlow.AppManagement.Services; + +namespace DevHome.SetupFlow.AppManagement.Models; + +/// +/// Interface for a winget package. +/// +public interface IWinGetPackage +{ + /// + /// Gets the package Id + /// + public string Id + { + get; + } + + /// + /// Gets the package display name + /// + public string Name + { + get; + } + + /// + /// Gets the version of the package which could be of any format supported + /// by WinGet package manager (e.g. alpha-numeric, 'Unknown', '1-preview, etc...). + /// + /// + public string Version + { + get; + } + + /// + /// Gets the package image uri + /// + public Uri ImageUri + { + get; + } + + /// + /// Gets the package "learn more" uri + /// + public Uri PackageUri + { + get; + } + + /// + /// Install this package + /// + /// Windows package manager service + Task InstallAsync(IWindowsPackageManager wpm); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/InstallPackageTask.cs new file mode 100644 index 0000000000..b77e7f762f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/InstallPackageTask.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using DevHome.SetupFlow.Common.Models; +using Windows.Foundation; + +namespace DevHome.SetupFlow.AppManagement.Models; + +internal class InstallPackageTask : ISetupTask +{ + public bool RequiresAdmin => throw new NotImplementedException(); + + // As we don't have this information available for each package in the WinGet COM API, + // simply assume that any package installation may need a reboot. + public bool RequiresReboot => true; + + public LoadingMessages GetLoadingMessages() => throw new NotImplementedException(); + + IAsyncOperation ISetupTask.Execute() => throw new NotImplementedException(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/PackageCatalog.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/PackageCatalog.cs new file mode 100644 index 0000000000..0fc0f4eebe --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/PackageCatalog.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace DevHome.SetupFlow.AppManagement.Models; + +// TODO Rename this class to PackageCollection to avoid confusion with the COM PackageCatalog class + +/// +/// Model class for a package catalog. A package catalog contains a list of +/// packages provided from the same source +/// +public class PackageCatalog +{ + public string Name + { + init; get; + } + + public string Description + { + init; get; + } + + public IReadOnlyCollection Packages + { + init; get; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/WinGetCompositeCatalog.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/WinGetCompositeCatalog.cs new file mode 100644 index 0000000000..8ec9a59492 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/WinGetCompositeCatalog.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DevHome.SetupFlow.AppManagement.Exceptions; +using DevHome.SetupFlow.ComInterop.Projection.WindowsPackageManager; +using DevHome.Telemetry; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.AppManagement.Models; + +/// +/// Model class for a composite catalog from remote and/or local packages +/// +public class WinGetCompositeCatalog : IWinGetCatalog +{ + private readonly ILogger _logger; + private readonly WindowsPackageManagerFactory _wingetFactory; + private readonly CreateCompositePackageCatalogOptions _compositeCatalogOptions; + private Microsoft.Management.Deployment.PackageCatalog _catalog; + + public bool IsConnected => _catalog != null; + + public WinGetCompositeCatalog(ILogger logger, WindowsPackageManagerFactory wingetFactory) + { + _logger = logger; + _wingetFactory = wingetFactory; + _compositeCatalogOptions = _wingetFactory.CreateCreateCompositePackageCatalogOptions(); + } + + public void AddPackageCatalog(PackageCatalogReference catalog) + { + _compositeCatalogOptions.Catalogs.Add(catalog); + } + + public CompositeSearchBehavior CompositeSearchBehavior + { + get => _compositeCatalogOptions.CompositeSearchBehavior; + set => _compositeCatalogOptions.CompositeSearchBehavior = value; + } + + public async Task ConnectAsync() + { + // Skip if already connected + if (IsConnected) + { + return; + } + + try + { + var packageManager = _wingetFactory.CreatePackageManager(); + var compositeCatalog = packageManager.CreateCompositePackageCatalog(_compositeCatalogOptions); + var connection = await compositeCatalog.ConnectAsync(); + if (connection.Status != ConnectResultStatus.Ok) + { + _logger.LogError(nameof(CatalogConnectionException), LogLevel.Info, $"Failed to connect to catalog with status {connection.Status}"); + throw new CatalogConnectionException(connection.Status); + } + + _catalog = connection.PackageCatalog; + } + catch (Exception e) + { + _logger.LogError(nameof(WinGetCompositeCatalog), LogLevel.Info, $"Error connecting to catalog reference: {e.Message}"); + throw; + } + } + + public async Task> SearchAsync(string query) + { + try + { + // Use default filter criteria for searching + var options = _wingetFactory.CreateFindPackagesOptions(); + var filter = _wingetFactory.CreatePackageMatchFilter(); + filter.Field = PackageMatchField.CatalogDefault; + filter.Option = PackageFieldMatchOption.ContainsCaseInsensitive; + filter.Value = query; + options.Selectors.Add(filter); + + return await FindPackagesAsync(options); + } + catch (Exception e) + { + _logger.LogError(nameof(WinGetCompositeCatalog), LogLevel.Info, $"Error searching for packages: {e.Message}"); + throw; + } + } + + public async Task> GetPackagesAsync(ISet packageIdSet) + { + try + { + var options = _wingetFactory.CreateFindPackagesOptions(); + foreach (var packageId in packageIdSet) + { + var filter = _wingetFactory.CreatePackageMatchFilter(); + filter.Field = PackageMatchField.Id; + filter.Option = PackageFieldMatchOption.Equals; + filter.Value = packageId; + options.Selectors.Add(filter); + } + + return await FindPackagesAsync(options); + } + catch (Exception e) + { + _logger.LogError(nameof(WinGetCompositeCatalog), LogLevel.Info, $"Error searching for packages: {e.Message}"); + throw; + } + } + + /// + /// Core method for finding packages based on the provided options + /// + /// Find packages options + /// List of winget package matches + /// Exception thrown if the catalog is not connected before attempting to find packages + /// Exception thrown if the find packages operation failed + private async Task> FindPackagesAsync(FindPackagesOptions options) + { + if (!IsConnected) + { + throw new InvalidOperationException($"Cannot perform {FindPackagesAsync} operation because the catalog reference is not connected"); + } + + var result = new List(); + var findResult = await _catalog.FindPackagesAsync(options); + if (findResult.Status != FindPackagesResultStatus.Ok) + { + _logger.LogError(nameof(FindPackagesException), LogLevel.Info, $"Failed to find packages with status {findResult.Status}"); + throw new FindPackagesException(findResult.Status); + } + + // Cannot use foreach or Linq for out-of-process IVector + // Bug: https://github.com/microsoft/CsWinRT/issues/1205 + for (var i = 0; i < findResult.Matches.Count; ++i) + { + var catalogPackage = findResult.Matches[i].CatalogPackage; + result.Add(new WinGetPackage(catalogPackage)); + } + + return result; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/WinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/WinGetPackage.cs new file mode 100644 index 0000000000..ce3395a33a --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Models/WinGetPackage.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Threading.Tasks; +using DevHome.SetupFlow.AppManagement.Services; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.AppManagement.Models; + +/// +/// Model class for a Windows Package Manager package. +/// +public class WinGetPackage : IWinGetPackage +{ + private readonly CatalogPackage _package; + + public WinGetPackage(CatalogPackage package) + { + _package = package; + } + + public CatalogPackage CatalogPackage => _package; + + public string Id => _package.Id; + + public string Name => _package.Name; + + public string Version => _package.DefaultInstallVersion.Version; + + public Uri ImageUri + { + get; set; + } + + public Uri PackageUri + { + get; set; + } + + public async Task InstallAsync(IWindowsPackageManager wpm) + { + await wpm.InstallPackageAsync(this); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Selectors/AppManagementViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Selectors/AppManagementViewSelector.cs new file mode 100644 index 0000000000..cc88834de3 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Selectors/AppManagementViewSelector.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Selectors; + +/// +/// Data template class for selecting the application management page's current +/// view template. +/// +public class AppManagementViewSelector : DataTemplateSelector +{ + /// + /// Gets or sets the default main template + /// + public DataTemplate MainTemplate { get; set; } + + /// + /// Gets or sets the search template displayed during a search operation + /// + public DataTemplate SearchTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item) + { + return ResolveDataTemplate(item); + } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + return ResolveDataTemplate(item); + } + + private DataTemplate ResolveDataTemplate(object item) + { + return item switch + { + SearchViewModel => SearchTemplate, + _ => MainTemplate, + }; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Services/IWindowsPackageManager.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Services/IWindowsPackageManager.cs new file mode 100644 index 0000000000..4390fa3d76 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Services/IWindowsPackageManager.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Threading.Tasks; +using DevHome.SetupFlow.AppManagement.Models; + +namespace DevHome.SetupFlow.AppManagement.Services; + +/// +/// Interface for interacting with the WinGet package manager. +/// More details: https://github.com/microsoft/winget-cli/blob/master/src/Microsoft.Management.Deployment/PackageManager.idl +/// +public interface IWindowsPackageManager +{ + /// + /// Gets a composite catalog for all remote and local catalogs. + /// + public IWinGetCatalog AllCatalogs + { + get; + } + + /// + /// Gets a composite catalog for the predefined winget and local catalogs. + /// + public IWinGetCatalog WinGetCatalog + { + get; + } + + /// + /// Gets a composite catalog for the predefined msstore and local catalogs. + /// + public IWinGetCatalog MsStoreCatalog + { + get; + } + + /// + /// Install a winget package + /// + /// Package to install + public Task InstallPackageAsync(WinGetPackage package); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Services/WindowsPackageManager.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Services/WindowsPackageManager.cs new file mode 100644 index 0000000000..f241ac2356 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Services/WindowsPackageManager.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.AppManagement.Models; +using DevHome.SetupFlow.ComInterop.Projection.WindowsPackageManager; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.AppManagement.Services; + +/// +/// Windows package manager class is an entrypoint for using the WinGet COM API. +/// +public class WindowsPackageManager : IWindowsPackageManager +{ + private readonly ILogger _logger; + private readonly IHost _host; + private readonly WindowsPackageManagerFactory _wingetFactory; + + // Predefined catalogs + private readonly Lazy _allCatalogs; + private readonly Lazy _wingetCatalog; + private readonly Lazy _msStoreCatalog; + + public WindowsPackageManager(IHost host, ILogger logger, WindowsPackageManagerFactory wingetFactory) + { + _host = host; + _logger = logger; + _wingetFactory = wingetFactory; + + // Lazy-initialize catalogs + _allCatalogs = new (CreateAllCatalogs); + _wingetCatalog = new (CreateWinGetCatalog); + _msStoreCatalog = new (CreateMsStoreCatalog); + } + + public IWinGetCatalog AllCatalogs => _allCatalogs.Value; + + public IWinGetCatalog WinGetCatalog => _wingetCatalog.Value; + + public IWinGetCatalog MsStoreCatalog => _msStoreCatalog.Value; + + public async Task InstallPackageAsync(WinGetPackage package) + { + await Task.CompletedTask; + throw new NotImplementedException(); + } + + /// + /// Create a composite catalog that can be used for finding packages in all + /// remote and local packages + /// + /// Catalog composed of all remote and local catalogs + private WinGetCompositeCatalog CreateAllCatalogs() + { + var compositeCatalog = new WinGetCompositeCatalog(_logger, _wingetFactory); + compositeCatalog.CompositeSearchBehavior = CompositeSearchBehavior.RemotePackagesFromAllCatalogs; + var packageManager = _wingetFactory.CreatePackageManager(); + var catalogs = packageManager.GetPackageCatalogs(); + + // Cannot use foreach or Linq for out-of-process IVector + // Bug: https://github.com/microsoft/CsWinRT/issues/1205 + for (var i = 0; i < catalogs.Count; ++i) + { + compositeCatalog.AddPackageCatalog(catalogs[i]); + } + + return compositeCatalog; + } + + /// + /// Create a composite catalog that can be used for finding packages in + /// winget and local catalogs + /// + /// Catalog composed of winget and local catalogs + private WinGetCompositeCatalog CreateWinGetCatalog() + { + return CreatePredefinedCatalog(PredefinedPackageCatalog.OpenWindowsCatalog); + } + + /// + /// Create a composite catalog that can be used for finding packages in + /// msstore and local catalogs + /// + /// Catalog composed of msstore and local catalogs + private WinGetCompositeCatalog CreateMsStoreCatalog() + { + return CreatePredefinedCatalog(PredefinedPackageCatalog.MicrosoftStore); + } + + /// + /// Create a composite catalog that can be used for finding packages in + /// a predefined and local catalogs + /// + /// Predefined package catalog + /// Catalog composed of the provided and local catalogs + private WinGetCompositeCatalog CreatePredefinedCatalog(PredefinedPackageCatalog predefinedPackageCatalog) + { + var compositeCatalog = new WinGetCompositeCatalog(_logger, _wingetFactory); + compositeCatalog.CompositeSearchBehavior = CompositeSearchBehavior.RemotePackagesFromAllCatalogs; + var packageManager = _wingetFactory.CreatePackageManager(); + var catalog = packageManager.GetPredefinedPackageCatalog(predefinedPackageCatalog); + compositeCatalog.AddPackageCatalog(catalog); + return compositeCatalog; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/AppManagementReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/AppManagementReviewViewModel.cs new file mode 100644 index 0000000000..702795a119 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/AppManagementReviewViewModel.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.ObjectModel; +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.Telemetry; + +namespace DevHome.SetupFlow.AppManagement.ViewModels; + +public partial class AppManagementReviewViewModel : ReviewTabViewModelBase +{ + private readonly ILogger _logger; + private readonly IStringResource _stringResource; + private readonly AppManagementTaskGroup _taskGroup; + + // TODO Use the selected packages for the review list once available. + public ObservableCollection ReviewPackages { get; } = new (); + + public AppManagementReviewViewModel(ILogger logger, IStringResource stringResource, AppManagementTaskGroup taskGroup) + { + _logger = logger; + _stringResource = stringResource; + _taskGroup = taskGroup; + + TabTitle = stringResource.GetLocalized(StringResourceKey.Applications); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/AppManagementViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/AppManagementViewModel.cs new file mode 100644 index 0000000000..8f7e2f92e7 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/AppManagementViewModel.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.SetupFlow.AppManagement.Services; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.AppManagement.ViewModels; + +public partial class AppManagementViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + private readonly SearchViewModel _searchViewModel; + private readonly PackageCatalogListViewModel _packageCatalogListViewModel; + private readonly AppManagementTaskGroup _taskGroup; + private readonly IWindowsPackageManager _wpm; + + /// + /// Current view to display in the main content control + /// + [ObservableProperty] + private ObservableObject _currentView; + + public ObservableCollection SelectedPackages { get; } = new (); + + /// + /// Gets the localized string for + /// + public string ApplicationsSelectedCountText => StringResource.GetLocalized(StringResourceKey.ApplicationsSelectedCount, SelectedPackages.Count); + + public AppManagementViewModel(ILogger logger, IStringResource stringResource, IHost host, IWindowsPackageManager wpm, AppManagementTaskGroup taskGroup) + : base(stringResource) + { + _logger = logger; + _taskGroup = taskGroup; + _wpm = wpm; + + _searchViewModel = host.GetService(); + + _packageCatalogListViewModel = host.GetService(); + _packageCatalogListViewModel.CatalogLoaded += OnCatalogLoaded; + + // By default, show the package catalogs + CurrentView = _packageCatalogListViewModel; + } + + public async override void OnNavigateToPageAsync() + { + // Connect to catalogs on a separate (non-UI) thread to prevent lagging the UI. + await Task.Run(async () => + { + // Connect composite catalog for all local and remote catalogs to + // enable searching for pacakges from any source + await _wpm.AllCatalogs.ConnectAsync(); + + // Connect predefined (winget and msstore) catalogs to enable loading + // package with a known source (e.g. for restoring packages) + await _wpm.WinGetCatalog.ConnectAsync(); + await _wpm.MsStoreCatalog.ConnectAsync(); + }); + } + + [RelayCommand(AllowConcurrentExecutions = true)] + private async Task SearchTextChangedAsync(string text, CancellationToken cancellationToken) + { + var (searchResultStatus, packages) = await _searchViewModel.SearchAsync(text, cancellationToken); + switch (searchResultStatus) + { + case SearchViewModel.SearchResultStatus.Ok: + CurrentView = _searchViewModel; + SetPackageSelectionChangedHandler(packages); + break; + case SearchViewModel.SearchResultStatus.EmptySearchQuery: + CurrentView = _packageCatalogListViewModel; + break; + case SearchViewModel.SearchResultStatus.CatalogNotConnect: + case SearchViewModel.SearchResultStatus.ExceptionThrown: + _logger.LogError(nameof(AppManagementViewModel), LogLevel.Local, $"Search failed with status: {searchResultStatus}"); + CurrentView = _packageCatalogListViewModel; + break; + case SearchViewModel.SearchResultStatus.Canceled: + default: + // noop + break; + } + } + + private void SetPackageSelectionChangedHandler(List packages) + { + foreach (var package in packages) + { + package.SelectionChanged += OnPackageSelectionChanged; + } + } + + private void OnCatalogLoaded(object sender, PackageCatalogViewModel packageCatalog) + { + packageCatalog.PackageSelectionChanged += OnPackageSelectionChanged; + } + + private void OnPackageSelectionChanged(object sender, PackageViewModel package) + { + if (package.IsSelected) + { + SelectedPackages.Add(package); + } + else + { + SelectedPackages.Remove(package); + } + + OnPropertyChanged(nameof(ApplicationsSelectedCountText)); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageCatalogListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageCatalogListViewModel.cs new file mode 100644 index 0000000000..1cbf3b3089 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageCatalogListViewModel.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.SetupFlow.AppManagement.ViewModels; +public partial class PackageCatalogListViewModel : ObservableObject +{ + /// + /// List of package catalogs to display + /// + [ObservableProperty] + private IList _packageCatalogs; + + /// + /// Occurrs when a package catalog is loaded + /// + public event EventHandler CatalogLoaded; + + /// + /// Load the package catalogs to display + /// + public async Task LoadCatalogsAsync() + { + var catalogs = new List(); + + // TODO Load recommended packages + // TODO Load restore packages + foreach (var catalog in catalogs) + { + CatalogLoaded?.Invoke(null, catalog); + } + + PackageCatalogs = catalogs; + await Task.CompletedTask; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageCatalogViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageCatalogViewModel.cs new file mode 100644 index 0000000000..241d078a31 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageCatalogViewModel.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.AppManagement.Models; + +namespace DevHome.SetupFlow.AppManagement.ViewModels; + +/// +/// ViewModel class for a model. +/// +public partial class PackageCatalogViewModel : ObservableObject +{ + private readonly PackageCatalog _packageCatalog; + + [ObservableProperty] + private bool _canAddAllPackages; + + public string Name => _packageCatalog.Name; + + public string Description => _packageCatalog.Description; + + public IReadOnlyCollection Packages { get; private set; } + + /// + /// Occurs when one of the packages in this catalog has its IsSelected state changed + /// + public event EventHandler PackageSelectionChanged; + + public PackageCatalogViewModel(PackageCatalog packageCatalog) + { + _packageCatalog = packageCatalog; + Packages = packageCatalog.Packages.Select(p => + { + var packageViewModel = new PackageViewModel(p); + packageViewModel.SelectionChanged += (sender, eventArg) => PackageSelectionChanged?.Invoke(sender, eventArg); + return packageViewModel; + }).ToReadOnlyCollection(); + } + + [RelayCommand] + private void AddAllPackages() + { + foreach (var package in Packages) + { + package.IsSelected = true; + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageViewModel.cs new file mode 100644 index 0000000000..116ad57910 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/PackageViewModel.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.SetupFlow.AppManagement.Models; + +namespace DevHome.SetupFlow.AppManagement.ViewModels; + +/// +/// ViewModel class for the model. +/// +public partial class PackageViewModel : ObservableObject +{ + private static readonly Uri DefaultPackageIconSource = new ("ms-appx:///DevHome.SetupFlow/Assets/DefaultPackageIcon.png"); + private readonly IWinGetPackage _package; + + /// + /// Occurrs after the package selection changes + /// + public event EventHandler SelectionChanged; + + /// + /// Indicates if a package is selected + /// + [ObservableProperty] + private bool _isSelected; + + public PackageViewModel(IWinGetPackage package) + { + _package = package; + } + + public string Name => _package.Name; + + public Uri ImageUri => _package.ImageUri ?? DefaultPackageIconSource; + + public string Version => _package.Version; + + public Uri PackageUri => _package.PackageUri; + + partial void OnIsSelectedChanged(bool value) => SelectionChanged?.Invoke(null, this); + + /// + /// Toggle package selection + /// + [RelayCommand] + private void ToggleSelection() => IsSelected = !IsSelected; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/SearchViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/SearchViewModel.cs new file mode 100644 index 0000000000..03b6e109fa --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/ViewModels/SearchViewModel.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Services; +using DevHome.SetupFlow.AppManagement.Services; +using DevHome.SetupFlow.Common.Services; +using DevHome.Telemetry; + +namespace DevHome.SetupFlow.AppManagement.ViewModels; +public partial class SearchViewModel : ObservableObject +{ + public enum SearchResultStatus + { + // Search was successful + Ok, + + // Search was performed on a null, empty or whitespace string + EmptySearchQuery, + + // Search canceled + Canceled, + + // Search aborted because catalog is not connected yet + CatalogNotConnect, + + // Exception thrown during search + ExceptionThrown, + } + + private readonly ILogger _logger; + private readonly IWindowsPackageManager _wpm; + private readonly IStringResource _stringResource; + + /// + /// Search query text + /// + [ObservableProperty] + private string _searchText; + + /// + /// List of search results + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SearchCountText))] + [NotifyPropertyChangedFor(nameof(NoSearchResultsText))] + private List _resultPackages = new (); + + /// + /// Gets the localized string for + /// + public string SearchCountText => _stringResource.GetLocalized(StringResourceKey.ResultCount, ResultPackages.Count); + + /// + /// Gets the localized string for + /// + public string NoSearchResultsText => _stringResource.GetLocalized(StringResourceKey.NoSearchResultsFoundTitle, SearchText); + + public SearchViewModel(ILogger logger, IWindowsPackageManager wpm, IStringResource stringResource) + { + _wpm = wpm; + _logger = logger; + _stringResource = stringResource; + } + + /// + /// Search for packages in all remote and local catalogs + /// + /// Text search query + /// Cancellation token + /// Search status and result + public async Task<(SearchResultStatus, List)> SearchAsync(string text, CancellationToken cancellationToken) + { + // Skip search if text is empty + if (string.IsNullOrWhiteSpace(text)) + { + return (SearchResultStatus.EmptySearchQuery, null); + } + + // Connect is required before searching + if (!_wpm.AllCatalogs.IsConnected) + { + return (SearchResultStatus.CatalogNotConnect, null); + } + + try + { + // Run the search on a separate (non-UI) thread to prevent lagging the UI. + var matches = await Task.Run(async () => await _wpm.AllCatalogs.SearchAsync(text), cancellationToken); + + // Don't update the UI if the operation was canceled + if (cancellationToken.IsCancellationRequested) + { + return (SearchResultStatus.Canceled, null); + } + + // Update the UI only if the operation was successful + SearchText = text; + ResultPackages = matches.Select(m => new PackageViewModel(m)).ToList(); + return (SearchResultStatus.Ok, ResultPackages); + } + catch (OperationCanceledException) + { + return (SearchResultStatus.Canceled, null); + } + catch (Exception e) + { + _logger.LogError(nameof(SearchViewModel), LogLevel.Info, $"Search error: {e.Message}"); + return (SearchResultStatus.ExceptionThrown, null); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementReviewView.xaml new file mode 100644 index 0000000000..5f722744ff --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementReviewView.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + 24 + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementReviewView.xaml.cs new file mode 100644 index 0000000000..b18513256b --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementReviewView.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Views; +public sealed partial class AppManagementReviewView : UserControl +{ + public AppManagementReviewViewModel ViewModel => (AppManagementReviewViewModel)DataContext; + + public AppManagementReviewView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementView.xaml b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementView.xaml new file mode 100644 index 0000000000..47664f2846 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementView.xaml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + 24 + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementView.xaml.cs new file mode 100644 index 0000000000..e93836ae33 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/AppManagementView.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Views; + +public sealed partial class AppManagementView : UserControl +{ + public AppManagementView() + { + this.InitializeComponent(); + } + + public AppManagementViewModel ViewModel => (AppManagementViewModel)this.DataContext; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogListView.xaml b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogListView.xaml new file mode 100644 index 0000000000..29fb71f7f7 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogListView.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogListView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogListView.xaml.cs new file mode 100644 index 0000000000..0f1dd3295f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogListView.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Views; +public sealed partial class PackageCatalogListView : UserControl +{ + public PackageCatalogListViewModel ViewModel => (PackageCatalogListViewModel)DataContext; + + public PackageCatalogListView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogView.xaml b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogView.xaml new file mode 100644 index 0000000000..5c4f93ed59 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogView.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + 0 + 0,0,0,15 + 0 + 0 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogView.xaml.cs new file mode 100644 index 0000000000..46801f0b2a --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageCatalogView.xaml.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Views; + +[INotifyPropertyChanged] +public sealed partial class PackageCatalogView : UserControl +{ + /// + /// List of package groups of size + /// + private List _packageGroupsCache = new (); + + /// + /// Gets the list of package groups of size + /// + public List PackageGroups => _packageGroupsCache; + + /// + /// Gets a value indicating whether the pager should be visible. + /// Show the pager if there's more than one group + /// + public Visibility PagerVisibility => PackageGroups.Count > 1 ? Visibility.Visible : Visibility.Collapsed; + + /// + /// Store the set of flip view panels + /// + private readonly HashSet _panels = new (); + + /// + /// Gets or sets the package catalog to display + /// + public PackageCatalogViewModel Catalog + { + get => (PackageCatalogViewModel)GetValue(CatalogProperty); + set => SetValue(CatalogProperty, value); + } + + /// + /// Gets or sets the max size of each package group. If the total number of + /// packages is not divisible by the group size, then the lsat group will + /// have less packages. + /// + public int GroupSize + { + get => (int)GetValue(GroupSizeProperty); + set => SetValue(GroupSizeProperty, value); + } + + public PackageCatalogView() + { + this.InitializeComponent(); + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + UpdateFlipViewSize(); + } + + private void OnItemsWrapGridLoaded(object sender, RoutedEventArgs e) + { + var panel = (ItemsWrapGrid)sender; + _panels.Add(panel); + UpdateFlipViewSize(); + } + + private void OnItemsWrapGridUnloaded(object sender, RoutedEventArgs e) + { + var panel = (ItemsWrapGrid)sender; + _panels.Remove(panel); + UpdateFlipViewSize(); + } + + /// + /// Update the flip view size to match the max content height at all time. + /// This method covers the following scenarios: + /// - Maintain a consistent (max) height throughout the flip view rotation + /// - When the window width changes and the grid content wraps to a new + /// row, update the flip view height accordingly to fit content + /// + private void UpdateFlipViewSize() + { + if (_panels.Count > 0) + { + PackagesFlipView.Height = _panels.Max(p => p.ActualHeight); + } + } + + /// + /// Update the list of package group cache + /// + private void UpdatePackageGroups() + { + if (Catalog != null) + { + _packageGroupsCache = Catalog.Packages.Chunk(GroupSize).ToList(); + OnPropertyChanged(nameof(PackageGroups)); + OnPropertyChanged(nameof(PagerVisibility)); + } + } + + /// + /// Perform all UI updates + /// + private void UpdateAll() + { + UpdatePackageGroups(); + UpdateFlipViewSize(); + } + + public static readonly DependencyProperty CatalogProperty = DependencyProperty.Register(nameof(Catalog), typeof(PackageCatalogViewModel), typeof(PackageCatalogView), new PropertyMetadata(null, (c, _) => ((PackageCatalogView)c).UpdateAll())); + public static readonly DependencyProperty GroupSizeProperty = DependencyProperty.Register(nameof(GroupSize), typeof(int), typeof(PackageCatalogView), new PropertyMetadata(4, (c, _) => ((PackageCatalogView)c).UpdateAll())); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageView.xaml b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageView.xaml new file mode 100644 index 0000000000..4aae6fd786 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageView.xaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageView.xaml.cs new file mode 100644 index 0000000000..134cdcb614 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/PackageView.xaml.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Views; +public sealed partial class PackageView : UserControl +{ + public PackageViewModel Package + { + get => (PackageViewModel)GetValue(PackageProperty); + set => SetValue(PackageProperty, value); + } + + public PackageView() + { + this.InitializeComponent(); + } + + public static readonly DependencyProperty PackageProperty = DependencyProperty.Register(nameof(Package), typeof(PackageViewModel), typeof(PackageView), new PropertyMetadata(null)); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/SearchView.xaml b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/SearchView.xaml new file mode 100644 index 0000000000..c46abbab37 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/SearchView.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/SearchView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/SearchView.xaml.cs new file mode 100644 index 0000000000..c3ffa7fa59 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.AppManagement/Views/SearchView.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.AppManagement.Views; +public sealed partial class SearchView : UserControl +{ + public SearchViewModel ViewModel => (SearchViewModel)DataContext; + + public SearchView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/DevHome.SetupFlow.ComInterop.Projection.csproj b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/DevHome.SetupFlow.ComInterop.Projection.csproj new file mode 100644 index 0000000000..eaee45c065 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/DevHome.SetupFlow.ComInterop.Projection.csproj @@ -0,0 +1,53 @@ + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.ComInterop.Projection + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + + + + + 10.0.19041.0 + Microsoft.Management.Deployment + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + NU1701 + true + none + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/NativeMethods.txt b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/NativeMethods.txt new file mode 100644 index 0000000000..10afab81ef --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/NativeMethods.txt @@ -0,0 +1 @@ +CoCreateInstance \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClassModel.cs b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClassModel.cs new file mode 100644 index 0000000000..314e0df1e0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClassModel.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; + +namespace DevHome.SetupFlow.ComInterop.Projection.WindowsPackageManager; + +internal class ClassModel +{ + /// + /// Gets the interface for the projected class type generated by CsWinRT + /// + public Type InterfaceType { init; get; } + + /// + /// Gets the projected class type generated by CsWinRT + /// + public Type ProjectedClassType { init; get; } + + /// + /// Gets the clsids for each context (e.g. OutOfProcProd, OutOfProcDev) + /// + public IReadOnlyDictionary Clsids { init; get; } + + /// + /// Get CLSID based on the provided context + /// + /// Context + /// CLSID for the provided context. + /// Throw an exception if the clsid context is not available for the current instance. + public Guid GetClsid(ClsidContext context) + { + if (!Clsids.TryGetValue(context, out var clsid)) + { + throw new InvalidOperationException($"{ProjectedClassType.FullName} is not implemented in context {context}"); + } + + return clsid; + } + + /// + /// Get IID corresponding to the COM object + /// + /// IID. + public Guid GetIid() + { + return InterfaceType.GUID; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClassesDefinition.cs b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClassesDefinition.cs new file mode 100644 index 0000000000..231d83a00e --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClassesDefinition.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Management.Deployment; + +namespace DevHome.SetupFlow.ComInterop.Projection.WindowsPackageManager; + +internal static class ClassesDefinition +{ + private static Dictionary Classes { get; } = new () + { + [typeof(PackageManager)] = new () + { + ProjectedClassType = typeof(PackageManager), + InterfaceType = typeof(IPackageManager), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("C53A4F16-787E-42A4-B304-29EFFB4BF597"), + [ClsidContext.Dev] = new Guid("74CB3139-B7C5-4B9E-9388-E6616DEA288C"), + }, + }, + + [typeof(FindPackagesOptions)] = new () + { + ProjectedClassType = typeof(FindPackagesOptions), + InterfaceType = typeof(IFindPackagesOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("572DED96-9C60-4526-8F92-EE7D91D38C1A"), + [ClsidContext.Dev] = new Guid("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"), + }, + }, + + [typeof(CreateCompositePackageCatalogOptions)] = new () + { + ProjectedClassType = typeof(CreateCompositePackageCatalogOptions), + InterfaceType = typeof(ICreateCompositePackageCatalogOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("526534B8-7E46-47C8-8416-B1685C327D37"), + [ClsidContext.Dev] = new Guid("EE160901-B317-4EA7-9CC6-5355C6D7D8A7"), + }, + }, + + [typeof(InstallOptions)] = new () + { + ProjectedClassType = typeof(InstallOptions), + InterfaceType = typeof(IInstallOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("1095F097-EB96-453B-B4E6-1613637F3B14"), + [ClsidContext.Dev] = new Guid("44FE0580-62F7-44D4-9E91-AA9614AB3E86"), + }, + }, + + [typeof(UninstallOptions)] = new () + { + ProjectedClassType = typeof(UninstallOptions), + InterfaceType = typeof(IUninstallOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("E1D9A11E-9F85-4D87-9C17-2B93143ADB8D"), + [ClsidContext.Dev] = new Guid("AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C"), + }, + }, + + [typeof(PackageMatchFilter)] = new () + { + ProjectedClassType = typeof(PackageMatchFilter), + InterfaceType = typeof(IPackageMatchFilter), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("D02C9DAF-99DC-429C-B503-4E504E4AB000"), + [ClsidContext.Dev] = new Guid("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"), + }, + }, + }; + + /// + /// Get CLSID based on the provided context for the specified type + /// + /// Projected class type + /// Context + /// CLSID for the provided context and type, or throw an exception if not found. + /// Throws an exception if type is not a project class. + public static Guid GetClsid(ClsidContext context) + { + ValidateType(); + return Classes[typeof(T)].GetClsid(context); + } + + /// + /// Get IID corresponding to the COM object + /// + /// Projected class type + /// IID or throw an exception if not found. + /// Throws an exception if type is not a project class. + public static Guid GetIid() + { + ValidateType(); + return Classes[typeof(T)].GetIid(); + } + + /// + /// Validate that the provided type is defined. + /// + /// Projected class type + /// Throws an exception if type is not a project class. + private static void ValidateType() + { + if (!Classes.ContainsKey(typeof(TType))) + { + throw new InvalidOperationException($"{typeof(TType).Name} is not a projected class type."); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClsidContext.cs b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClsidContext.cs new file mode 100644 index 0000000000..34ae4880d1 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/ClsidContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.SetupFlow.ComInterop.Projection.WindowsPackageManager; + +public enum ClsidContext +{ + // Production CLSID Guids + Prod, + + // Development CLSID Guids + Dev, +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/WindowsPackageManagerFactory.cs b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/WindowsPackageManagerFactory.cs new file mode 100644 index 0000000000..7098ad3b4e --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ComInterop.Projection/WindowsPackageManager/WindowsPackageManagerFactory.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Management.Deployment; +using Windows.Win32; +using Windows.Win32.System.Com; +using WinRT; + +namespace DevHome.SetupFlow.ComInterop.Projection.WindowsPackageManager; + +/// +/// Factory class for creating WinGet COM objects. +/// Details about each method can be found in the source IDL: +/// https://github.com/microsoft/winget-cli/blob/master/src/Microsoft.Management.Deployment/PackageManager.idl +/// +public class WindowsPackageManagerFactory +{ + private readonly ClsidContext _clsidContext; + + public WindowsPackageManagerFactory(ClsidContext clsidContext = ClsidContext.Prod) + { + _clsidContext = clsidContext; + } + + public PackageManager CreatePackageManager() => CreateInstance(); + + public FindPackagesOptions CreateFindPackagesOptions() => CreateInstance(); + + public CreateCompositePackageCatalogOptions CreateCreateCompositePackageCatalogOptions() => CreateInstance(); + + public InstallOptions CreateInstallOptions() => CreateInstance(); + + public UninstallOptions CreateUninstallOptions() => CreateInstance(); + + public PackageMatchFilter CreatePackageMatchFilter() => CreateInstance(); + + private TClass CreateInstance() + { + var clsid = ClassesDefinition.GetClsid(_clsidContext); + var iid = ClassesDefinition.GetIid(); + PInvoke.CoCreateInstance(clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, iid, out var result); + return MarshalGeneric.FromAbi(Marshal.GetIUnknownForObject(result)); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Behaviors/SetupFlowNavigationContentBehavior.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Behaviors/SetupFlowNavigationContentBehavior.cs new file mode 100644 index 0000000000..65c8d6374c --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Behaviors/SetupFlowNavigationContentBehavior.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Xaml.Interactivity; + +namespace DevHome.SetupFlow.Common.Behaviors; + +/// +/// Behavior class for customizing the setup flow navigation content +/// +public class SetupFlowNavigationContentBehavior : Behavior +{ + /// + /// Singleton instance of this behavior class implemented by the setup flow + /// root page content control. + /// + private static SetupFlowNavigationContentBehavior _instance; + + protected override void OnAttached() + { + base.OnAttached(); + _instance = this; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + _instance = null; + } + + /// + /// Sets the content of the associated in the setup flow navigation + /// + /// Customized content + public static void SetNavigationContent(object content) + { + if (_instance != null) + { + _instance.AssociatedObject.Content = content; + } + } + + /// + /// Getter for the attached property + /// + /// Target control + /// Content object + public static object GetContent(UserControl control) => control.GetValue(ContentProperty); + + /// + /// Setter for the attached property + /// + /// Target control + /// Content object + public static void SetContent(UserControl control, object content) + { + control.SetValue(ContentProperty, content); + + // Remove navigation content before navigating to a new page + control.Unloaded += (_, _) => SetNavigationContent(null); + } + + public static readonly DependencyProperty ContentProperty = DependencyProperty.RegisterAttached("Content", typeof(ControlTemplate), typeof(SetupFlowNavigationContentBehavior), new PropertyMetadata(null, (_, e) => SetNavigationContent(e.NewValue))); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Controls/SetupShell.xaml b/tools/SetupFlow/DevHome.SetupFlow.Common/Controls/SetupShell.xaml new file mode 100644 index 0000000000..1ffbacceb5 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Controls/SetupShell.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Controls/SetupShell.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Controls/SetupShell.xaml.cs new file mode 100644 index 0000000000..8ff844012a --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Controls/SetupShell.xaml.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Markup; + +namespace DevHome.SetupFlow.Common.Controls; + +/// +/// Setup shell class used by the setup flow pages to ensure a consistent +/// end-to-end page layout. +/// +[ContentProperty(Name = nameof(SetupShellContent))] +public sealed partial class SetupShell : UserControl +{ + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public string Description + { + get => (string)GetValue(DescriptionProperty); + set => SetValue(DescriptionProperty, value); + } + + public string HeaderText + { + get => (string)GetValue(HeaderTextProperty); + set => SetValue(HeaderTextProperty, value); + } + + public ControlTemplate HeaderTemplate + { + get => (ControlTemplate)GetValue(HeaderTemplateProperty) ?? defaultHeaderTemplate; + set => SetValue(HeaderTemplateProperty, value); + } + + public object SetupShellContent + { + get => (object)GetValue(SetupShellContentProperty); + set => SetValue(SetupShellContentProperty, value); + } + + public SetupShell() + { + this.InitializeComponent(); + } + + public static readonly DependencyProperty TitleProperty = DependencyProperty.RegisterAttached(nameof(Title), typeof(string), typeof(SetupShell), new PropertyMetadata(string.Empty)); + public static readonly DependencyProperty DescriptionProperty = DependencyProperty.RegisterAttached(nameof(Description), typeof(string), typeof(SetupShell), new PropertyMetadata(string.Empty)); + public static readonly DependencyProperty SetupShellContentProperty = DependencyProperty.RegisterAttached(nameof(SetupShellContent), typeof(object), typeof(SetupShell), new PropertyMetadata(null)); + public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.RegisterAttached(nameof(HeaderText), typeof(string), typeof(SetupShell), new PropertyMetadata(string.Empty)); + public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.RegisterAttached(nameof(HeaderTemplate), typeof(ControlTemplate), typeof(SetupShell), new PropertyMetadata(null)); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj new file mode 100644 index 0000000000..5aafd0189a --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj @@ -0,0 +1,39 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.Common + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + + + + + + $(DefaultXamlRuntime) + Designer + + + MSBuild:Compile + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Models/ISetupTask.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/ISetupTask.cs new file mode 100644 index 0000000000..cbe69de908 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/ISetupTask.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Windows.Foundation; + +namespace DevHome.SetupFlow.Common.Models; + +/// +/// A single atomic task to perform during the setup flow. +/// +public interface ISetupTask +{ + /// + /// Gets a value indicating whether this task requires admin privileges to be executed. + /// + public abstract bool RequiresAdmin + { + get; + } + + /// + /// Gets a value indicating whether this task requires to reboot the computer to be completed. + /// + /// + /// This will be used to guide whether we show a warning to the user about possible reboots + /// before beginning the setup. + /// TODO: We need to figure a story around how to handle reboots and the different cases. + /// Setting up WSL (future) will require us to reboot the machine to finish, but other + /// tasks like installing an app may trigger a reboot out of our control. + /// + public abstract bool RequiresReboot + { + get; + } + + /// + /// Executes this setup task. + /// + /// + /// The task must work correctly in a background thread (not the UI thread). + /// TODO: Define return type to report status in Loading page (success, failure) + /// TODO: Define progress type to report status in Loading page + /// TODO: We will have a background process to run tasks that require elevated permissions. + /// We will have to figure a way to communicate those tasks to the background process. + /// + /// + /// The async operation that executes this task. The value returned indicates whether the task completed successfully. + /// + public abstract IAsyncOperation Execute(); + + /// + /// Gets a string to show in the loading page while executing this task. + /// + /// A localized string indicating that this task is being executed. + public abstract LoadingMessages GetLoadingMessages(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Models/ISetupTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/ISetupTaskGroup.cs new file mode 100644 index 0000000000..3e857e9a80 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/ISetupTaskGroup.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using DevHome.SetupFlow.Common.ViewModels; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.Common.Models; + +/// +/// A container for a group of related setup tasks that are always handled together. +/// +/// +/// For now, each group contains a single type of tasks. For example, only app installation +/// or only repo cloning. In the future, we expect to have groups with different tasks, +/// like dev volume and WSL. That may require some re-work depending on the requirements, +/// but should work fine if a whole group is always shown together and not differently +/// depending on the context. +/// +public interface ISetupTaskGroup +{ + /// + /// Gets the view model for the setup page containing all the options for this setup task group. + /// + public SetupPageViewModelBase GetSetupPageViewModel(); + + /// + /// Gets the view model for the contents of the tab shown in the review page for this setup task group. + /// + public ReviewTabViewModelBase GetReviewTabViewModel(); + + /// + /// Gets all the individual setup tasks that make up this group + /// + public IEnumerable SetupTasks + { + get; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Models/LoadingMessages.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/LoadingMessages.cs new file mode 100644 index 0000000000..244116ea79 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/LoadingMessages.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevHome.SetupFlow.Common.Models; + +/// +/// Messages to display in the loading screen. +/// +public class LoadingMessages +{ + /// + /// Gets or sets the message to display when the task is executing. + /// + public string Executing + { + get; set; + } + + /// + /// Gets or sets the message to display when the task finished successfully. + /// + public string Finished + { + get; set; + } + + /// + /// Gets or sets the message to display when the task has a non-recoverable error. + /// + public string Error + { + get; set; + } + + /// + /// Gets or sets the message to display when the task finished, but needs attention. + /// + public string NeedsAttention + { + get; set; + } + + public LoadingMessages() + { + } + + public LoadingMessages(string executingMessage, string finishedMessage, string errorMessage, string needsAttention) + { + Executing = executingMessage; + Finished = finishedMessage; + Error = errorMessage; + NeedsAttention = needsAttention; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Models/TaskFinishedState.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/TaskFinishedState.cs new file mode 100644 index 0000000000..840d6dcbbb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Models/TaskFinishedState.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevHome.SetupFlow.Common.Models; + +/// +/// Enum to tell Dev Home the status of a task. +/// +public enum TaskFinishedState +{ + Success, + Failure, + NeedsAttention, +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Services/SetupFlowOrchestrator.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Services/SetupFlowOrchestrator.cs new file mode 100644 index 0000000000..0e3dfed7be --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Services/SetupFlowOrchestrator.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using CommunityToolkit.Mvvm.Input; +using DevHome.SetupFlow.Common.Models; + +namespace DevHome.SetupFlow.Common.Services; + +/// +/// Orchestrator for the Setup Flow, in charge functionality across multiple pages. +/// +public class SetupFlowOrchestrator +{ + /// + /// Relay commands associated with the navigation buttons in the UI. + /// + private readonly List _navigationButtonsCommands = new (); + + /// + /// Gets or sets the task groups that are to be executed on the flow. + /// + public IList TaskGroups + { + get; set; + } + + /// + /// Sets the list of commands associated with the navigation buttons. + /// + public void SetNavigationButtonsCommands(IList navigationButtonsCommands) + { + _navigationButtonsCommands.Clear(); + _navigationButtonsCommands.AddRange(navigationButtonsCommands); + } + + /// + /// Notify all the navigation buttons that the CanExecute property has changed. + /// + /// + /// This is used so that the individual pages can notify the navigation container + /// about changes in state without having to reach into the navigation container. + /// We could notify each button specifically, but this is simpler and not too bad. + /// + public void NotifyNavigationCanExecuteChanged() + { + foreach (var command in _navigationButtonsCommands) + { + command.NotifyCanExecuteChanged(); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Services/StringResourceKey.cs new file mode 100644 index 0000000000..cebe789345 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Services/StringResourceKey.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace DevHome.SetupFlow.Common.Services; + +/// +/// Static class for storing the keys of the string resources that are accessed +/// from C# such as string resources that have placeholders or data that is +/// defined in the code and will surface on the UI. +/// +public static class StringResourceKey +{ + // Keys in this file should be a subset of the ones found in the .resw file. + public static readonly string ApplicationsSelectedCount = nameof(ApplicationsSelectedCount); + public static readonly string Applications = nameof(Applications); + public static readonly string Basics = nameof(Basics); + public static readonly string Close = nameof(Close); + public static readonly string ConfigurationFileTypeNotSupported = nameof(ConfigurationFileTypeNotSupported); + public static readonly string FileTypeNotSupported = nameof(FileTypeNotSupported); + public static readonly string Next = nameof(Next); + public static readonly string NoSearchResultsFoundTitle = nameof(NoSearchResultsFoundTitle); + public static readonly string PackagesCount = nameof(PackagesCount); + public static readonly string ResultCount = nameof(ResultCount); + public static readonly string Repository = nameof(Repository); + public static readonly string SelectedPackagesCount = nameof(SelectedPackagesCount); + public static readonly string SetUpButton = nameof(SetUpButton); + public static readonly string ViewConfiguration = nameof(ViewConfiguration); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Styles/SetupToolStyles.xaml b/tools/SetupFlow/DevHome.SetupFlow.Common/Styles/SetupToolStyles.xaml new file mode 100644 index 0000000000..c6987146d1 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Styles/SetupToolStyles.xaml @@ -0,0 +1,127 @@ + + + + + + + + + Segoe MDL2 Assets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/ViewModels/ReviewTabViewModelBase.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/ViewModels/ReviewTabViewModelBase.cs new file mode 100644 index 0000000000..b0457a4f90 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/ViewModels/ReviewTabViewModelBase.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.SetupFlow.Common.ViewModels; + +/// +/// Base view model class for all the tabs we show in the Review page of the Setup flow. +/// +public partial class ReviewTabViewModelBase : ObservableObject +{ + /// + /// Title shown on the tabs bar. + /// + [ObservableProperty] + private string _tabTitle = string.Empty; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/ViewModels/SetupPageViewModelBase.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/ViewModels/SetupPageViewModelBase.cs new file mode 100644 index 0000000000..f56d51d500 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/ViewModels/SetupPageViewModelBase.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.Services; + +namespace DevHome.SetupFlow.Common.ViewModels; + +/// +/// Base view model class for all the pages in the Setup flow. +/// +public partial class SetupPageViewModelBase : ObservableObject +{ + //// TODO: Figure out how to notify change of state to the Can* properties to the RelayCommands controlling the buttons + //// TODO: Figure out how the navigation in the setup flow should interact with navigation in the whole app (INavigationAware) + + /// + /// Indicates whether to show the navigation bar at the bottom of this page. + /// + [ObservableProperty] + private bool _isNavigationBarVisible = true; + + /// + /// Indicates whether the "Previous page" button should be enabled on this page. + /// + /// + /// This needs only be aware of reasons to disable the action that are local to the page. + /// The containing orchestrator handles other conditions, like being the first page. + /// + [ObservableProperty] + private bool _canGoToPreviousPage = true; + + /// + /// Indicates whether the "Next page" button should be enabled on this page. + /// + /// + /// This needs only be aware of reasons to disable the action that are local to the page. + /// The containing orchestrator handles other conditions, like being the last page. + /// + [ObservableProperty] + private bool _canGoToNextPage = true; + + /// + /// Text shown on the button that goes to the next page. + /// By default, this will be "Next". + /// + [ObservableProperty] + private string _nextPageButtonText; + + /// + /// Indicates whether the "Cancel" button should be enabled on this page. + /// + /// + /// This needs only be aware of reasons to disable the action that are local to the page. + /// The containing orchestrator handles other conditions. + /// + [ObservableProperty] + private bool _canCancel = true; + + /// + /// Indicates whether this page is one of the steps the user will need to complete before starting the setup. + /// + /// + /// This will allow us to add the "Step x of y" at the top of these pages. + /// + //// TODO: Integrate this with Amir's SetupShell + [ObservableProperty] + private bool _isStepPage = true; + + /// + /// Gets an object used to retrieve localized strings. + /// + protected IStringResource StringResource + { + get; init; + } + + public SetupPageViewModelBase(IStringResource stringResource) + { + StringResource = stringResource; + _nextPageButtonText = StringResource.GetLocalized(StringResourceKey.Next); + } + + /// + /// Hook for actions to execute the first time the page is loaded. + /// + /// + /// The orchestrator takes care of calling this when appropriate. + /// This runs on the UI thread, any time-consuming task should be non-blocking. + /// Examples of uses include loading content to display on the page, or start + /// background processing. + /// + public async virtual void OnNavigateToPageAsync() + { + // Do nothing + await Task.CompletedTask; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/ConfigurationFileTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/ConfigurationFileTaskGroup.cs new file mode 100644 index 0000000000..60c4a3ca10 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/ConfigurationFileTaskGroup.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.Common.Models; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.ConfigurationFile.Models; +using DevHome.SetupFlow.ConfigurationFile.ViewModels; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.AppManagement; + +public class ConfigurationFileTaskGroup : ISetupTaskGroup +{ + private readonly IList _configurationTasks = new List(); + private readonly IHost _host; + private readonly ConfigurationFileViewModel _viewModel; + + public ConfigurationFileTaskGroup(IHost host) + { + _host = host; + _viewModel = _host.GetService(); + } + + public async Task PickConfigurationFileAsync() => await _viewModel.PickConfigurationFileAsync(); + + public IEnumerable SetupTasks => _configurationTasks; + + public SetupPageViewModelBase GetSetupPageViewModel() => _viewModel; + + public ReviewTabViewModelBase GetReviewTabViewModel() => throw new System.NotImplementedException(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/DevHome.SetupFlow.ConfigurationFile.csproj b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/DevHome.SetupFlow.ConfigurationFile.csproj new file mode 100644 index 0000000000..de8d16f3dc --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/DevHome.SetupFlow.ConfigurationFile.csproj @@ -0,0 +1,34 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.ConfigurationFile + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + + + + $(DefaultXamlRuntime) + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Models/Configuration.cs b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Models/Configuration.cs new file mode 100644 index 0000000000..7b3014406b --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Models/Configuration.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.IO; + +namespace DevHome.SetupFlow.ConfigurationFile.Models; + +/// +/// Model class for a YAML configuration file +/// +public class Configuration +{ + private readonly FileInfo _fileInfo; + private readonly Lazy _lazyContent; + + public Configuration(string filePath) + { + _fileInfo = new FileInfo(filePath); + _lazyContent = new (LoadContent); + } + + /// + /// Gets the configuration file name + /// + public string Name => _fileInfo.Name; + + /// + /// Gets the file content + /// + public string Content => _lazyContent.Value; + + /// + /// Load configuration file content + /// + /// Configuration file content + private string LoadContent() + { + using var text = _fileInfo.OpenText(); + return text.ReadToEnd(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Models/ConfigureTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Models/ConfigureTask.cs new file mode 100644 index 0000000000..f4ba7a6e56 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Models/ConfigureTask.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using DevHome.SetupFlow.Common.Models; +using Windows.Foundation; + +namespace DevHome.SetupFlow.ConfigurationFile.Models; + +internal class ConfigureTask : ISetupTask +{ + public bool RequiresAdmin => throw new NotImplementedException(); + + public bool RequiresReboot => throw new NotImplementedException(); + + public LoadingMessages GetLoadingMessages() => throw new NotImplementedException(); + + IAsyncOperation ISetupTask.Execute() => throw new NotImplementedException(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/ViewModels/ConfigurationFileViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/ViewModels/ConfigurationFileViewModel.cs new file mode 100644 index 0000000000..147dc4061c --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/ViewModels/ConfigurationFileViewModel.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.ConfigurationFile.Models; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; +using WinUIEx; + +namespace DevHome.SetupFlow.ConfigurationFile.ViewModels; + +public partial class ConfigurationFileViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + private readonly IHost _host; + private readonly SetupFlowOrchestrator _orchestrator; + + public ConfigurationFileViewModel(ILogger logger, IStringResource stringResource, IHost host, SetupFlowOrchestrator orchestrator) + : base(stringResource) + { + _logger = logger; + _host = host; + _orchestrator = orchestrator; + + // Configure navigation bar + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton); + CanGoToNextPage = false; + } + + /// + /// Configuration file + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(TitleText))] + [NotifyPropertyChangedFor(nameof(Content))] + private Configuration _configuration; + + /// + /// Store the value for whether the agreements are read. + /// + [ObservableProperty] + private bool _readAndAgree; + + partial void OnReadAndAgreeChanged(bool value) + { + CanGoToNextPage = value; + _orchestrator.NotifyNavigationCanExecuteChanged(); + } + + /// + /// Gets the title for the configuration page + /// + public string TitleText => StringResource.GetLocalized(StringResourceKey.ViewConfiguration, _configuration.Name); + + /// + /// Gets the configuration file content + /// + public string Content => _configuration.Content; + + /// + /// Open file picker to select a YAML configuration file. + /// + /// True if a YAML configuration file was selected, false otherwise + public async Task PickConfigurationFileAsync() + { + // Get the application root window. + var mainWindow = Application.Current.GetService(); + + // Create and configure file picker + var filePicker = mainWindow.CreateOpenFilePicker(); + filePicker.FileTypeFilter.Add(".yaml"); + filePicker.FileTypeFilter.Add(".yml"); + var file = await filePicker.PickSingleFileAsync(); + + // Check if a file was selected + if (file != null) + { + try + { + Configuration = new (file.Path); + + // TODO Call Configuration COM API once implemented to validate + // the input file. + return true; + } + catch + { + await mainWindow.ShowErrorMessageDialogAsync( + StringResource.GetLocalized(StringResourceKey.FileTypeNotSupported), + StringResource.GetLocalized(StringResourceKey.ConfigurationFileTypeNotSupported), + StringResource.GetLocalized(StringResourceKey.Close)); + } + } + + return false; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Views/ConfigurationFileView.xaml b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Views/ConfigurationFileView.xaml new file mode 100644 index 0000000000..a43a11479c --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Views/ConfigurationFileView.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Views/ConfigurationFileView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Views/ConfigurationFileView.xaml.cs new file mode 100644 index 0000000000..064d0a7784 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.ConfigurationFlow/Views/ConfigurationFileView.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.ConfigurationFile.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.ConfigurationFile.Views; +public sealed partial class ConfigurationFileView : UserControl +{ + public ConfigurationFileViewModel ViewModel => (ConfigurationFileViewModel)DataContext; + + public ConfigurationFileView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/DevHome.SetupFlow.DevVolume.csproj b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/DevHome.SetupFlow.DevVolume.csproj new file mode 100644 index 0000000000..564f1c63d6 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/DevHome.SetupFlow.DevVolume.csproj @@ -0,0 +1,34 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.DevVolume + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/DevVolumeTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/DevVolumeTaskGroup.cs new file mode 100644 index 0000000000..dd1b2479ac --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/DevVolumeTaskGroup.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.Common.Models; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.DevVolume.Models; +using DevHome.SetupFlow.DevVolume.ViewModels; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.DevVolume; + +public class DevVolumeTaskGroup : ISetupTaskGroup +{ + private readonly IHost _host; + + public DevVolumeTaskGroup(IHost host) + { + _host = host; + } + + private readonly IList _devVolumeTasks = new List(); + + public IEnumerable SetupTasks => _devVolumeTasks; + + public SetupPageViewModelBase GetSetupPageViewModel() => _host.CreateInstance(this); + + public ReviewTabViewModelBase GetReviewTabViewModel() => _host.CreateInstance(this); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Models/CreateDevVolumeTask.cs b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Models/CreateDevVolumeTask.cs new file mode 100644 index 0000000000..a81252623f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Models/CreateDevVolumeTask.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using DevHome.SetupFlow.Common.Models; +using Windows.Foundation; + +namespace DevHome.SetupFlow.DevVolume.Models; + +internal class CreateDevVolumeTask : ISetupTask +{ + public bool RequiresAdmin => throw new NotImplementedException(); + + public bool RequiresReboot => false; + + public LoadingMessages GetLoadingMessages() => throw new NotImplementedException(); + + IAsyncOperation ISetupTask.Execute() => throw new NotImplementedException(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/ViewModels/DevVolumeReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/ViewModels/DevVolumeReviewViewModel.cs new file mode 100644 index 0000000000..e5c906a6ce --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/ViewModels/DevVolumeReviewViewModel.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.Telemetry; + +namespace DevHome.SetupFlow.DevVolume.ViewModels; + +public partial class DevVolumeReviewViewModel : ReviewTabViewModelBase +{ + private readonly ILogger _logger; + private readonly IStringResource _stringResource; + private readonly DevVolumeTaskGroup _taskGroup; + + public DevVolumeReviewViewModel(ILogger logger, IStringResource stringResource, DevVolumeTaskGroup taskGroup) + { + _logger = logger; + _stringResource = stringResource; + _taskGroup = taskGroup; + + TabTitle = stringResource.GetLocalized(StringResourceKey.Basics); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/ViewModels/DevVolumeViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/ViewModels/DevVolumeViewModel.cs new file mode 100644 index 0000000000..76cb622cab --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/ViewModels/DevVolumeViewModel.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.Telemetry; + +namespace DevHome.SetupFlow.DevVolume.ViewModels; + +public partial class DevVolumeViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + private readonly DevVolumeTaskGroup _taskGroup; + + public DevVolumeViewModel(ILogger logger, IStringResource stringResource, DevVolumeTaskGroup taskGroup) + : base(stringResource) + { + _logger = logger; + _taskGroup = taskGroup; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeReviewView.xaml new file mode 100644 index 0000000000..8c8ede52ec --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeReviewView.xaml @@ -0,0 +1,16 @@ + + + + + + + Dev volume review + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeReviewView.xaml.cs new file mode 100644 index 0000000000..33a81ee7cc --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeReviewView.xaml.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.DevVolume.Views; +public sealed partial class DevVolumeReviewView : UserControl +{ + public DevVolumeReviewView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeView.xaml b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeView.xaml new file mode 100644 index 0000000000..de43b790eb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeView.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + DevVolume: Content goes here + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeView.xaml.cs new file mode 100644 index 0000000000..88643382ed --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.DevVolume/Views/DevVolumeView.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.DevVolume.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.DevVolume.Views; + +public sealed partial class DevVolumeView : UserControl +{ + public DevVolumeView() + { + this.InitializeComponent(); + } + + public DevVolumeViewModel ViewModel => (DevVolumeViewModel)this.DataContext; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Loading/DevHome.SetupFlow.Loading.csproj b/tools/SetupFlow/DevHome.SetupFlow.Loading/DevHome.SetupFlow.Loading.csproj new file mode 100644 index 0000000000..c077ba4103 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Loading/DevHome.SetupFlow.Loading.csproj @@ -0,0 +1,24 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.Loading + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Loading/Models/TaskInformation.cs b/tools/SetupFlow/DevHome.SetupFlow.Loading/Models/TaskInformation.cs new file mode 100644 index 0000000000..5557122295 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Loading/Models/TaskInformation.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.SetupFlow.Common.Models; + +namespace DevHome.SetupFlow.Loading.Models; +public partial class TaskInformation : ObservableObject +{ + public int TaskIndex + { + get; set; + } + + public ISetupTask TaskToExecute + { + get; set; + } + + [ObservableProperty] + private string _messageToShow; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Loading/ViewModels/LoadingViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.Loading/ViewModels/LoadingViewModel.cs new file mode 100644 index 0000000000..30d2ecf554 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Loading/ViewModels/LoadingViewModel.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.Models; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.Loading.Models; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; +using Windows.UI.Core; +using WinUIEx; + +namespace DevHome.SetupFlow.Loading.ViewModels; + +public partial class LoadingViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + private readonly IHost _host; + + /// + /// Event raised when the execution of all tasks is completed. + /// + public event EventHandler ExecutionFinished; + + private readonly SetupFlowOrchestrator orchestrator; + + [ObservableProperty] + private ObservableCollection _setupTasks; + + [ObservableProperty] + private int _tasksCompleted; + + [ObservableProperty] + private int _tasksFinishedSuccessfully; + + [ObservableProperty] + private int _tasksFinishedUnSuccessfully; + + [ObservableProperty] + private int _tasksThatNeedAttention; + + [ObservableProperty] + private string _executingTasks; + + [ObservableProperty] + private string _actionCenterDisplay; + + public LoadingViewModel(ILogger logger, IStringResource stringResource, IHost host) + : base(stringResource) + { + _logger = logger; + _host = host; + _setupTasks = new (); + + IsNavigationBarVisible = false; + IsStepPage = false; + + orchestrator = _host.GetService(); + } + + private void FetchTaskInformation() + { + var taskIndex = 0; + foreach (var taskGroup in orchestrator.TaskGroups) + { + foreach (var task in taskGroup.SetupTasks) + { + _setupTasks.Add(new TaskInformation { TaskIndex = taskIndex++, TaskToExecute = task, MessageToShow = task.GetLoadingMessages().Executing }); + } + } + + /* + * These strings need to be localized. + */ + ExecutingTasks = $"Executing Step {TasksCompleted}/{_setupTasks.Count}"; + ActionCenterDisplay = "Succeeded: 0 - Failed: 0 - Needs Attention: 0"; + } + + public void ChangeMessage(int taskNumber, TaskFinishedState taskFinishedState) + { + var information = _setupTasks[taskNumber]; + var stringToReplace = string.Empty; + + if (taskFinishedState == TaskFinishedState.Success) + { + stringToReplace = information.TaskToExecute.GetLoadingMessages().Finished; + } + else if (taskFinishedState == TaskFinishedState.Failure) + { + stringToReplace = information.TaskToExecute.GetLoadingMessages().Error; + } + else if (taskFinishedState == TaskFinishedState.NeedsAttention) + { + stringToReplace = information.TaskToExecute.GetLoadingMessages().NeedsAttention; + } + + // change a value in the list to force UI to update + _setupTasks[taskNumber].MessageToShow = stringToReplace; + } + + public async override void OnNavigateToPageAsync() + { + FetchTaskInformation(); + + var window = Application.Current.GetService(); + await Task.Run(() => + { + Parallel.ForEach(_setupTasks, async task => + { + // Start the task and wait for it to complete. + try + { + var taskFinishedState = await task.TaskToExecute.Execute(); + window.DispatcherQueue.TryEnqueue(() => + { + ChangeMessage(task.TaskIndex, taskFinishedState); + TasksFinishedSuccessfully++; + TasksCompleted++; + ExecutingTasks = $"Executing Step {TasksCompleted}/{_setupTasks.Count}"; + ActionCenterDisplay = $"Succeeded: {TasksFinishedSuccessfully} - Failed: {TasksFinishedUnSuccessfully} - Needs Attention: {TasksThatNeedAttention}"; + }); + } + catch + { + // Don't let a single task break everything + // TODO: Show failed tasks on UI + } + }); + }); + + ExecutionFinished.Invoke(null, null); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Loading/Views/LoadingView.xaml b/tools/SetupFlow/DevHome.SetupFlow.Loading/Views/LoadingView.xaml new file mode 100644 index 0000000000..dda848b5c4 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Loading/Views/LoadingView.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Setup initiated, stay tuned for the magic... + + + + + Creating repositories + Logs + Action Center + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Loading/Views/LoadingView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.Loading/Views/LoadingView.xaml.cs new file mode 100644 index 0000000000..8440e2521f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Loading/Views/LoadingView.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.Loading.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Loading.Views; + +public sealed partial class LoadingView : UserControl +{ + public LoadingView() + { + this.InitializeComponent(); + } + + public LoadingViewModel ViewModel => (LoadingViewModel)this.DataContext; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.MainPage/DevHome.SetupFlow.MainPage.csproj b/tools/SetupFlow/DevHome.SetupFlow.MainPage/DevHome.SetupFlow.MainPage.csproj new file mode 100644 index 0000000000..544c65c307 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.MainPage/DevHome.SetupFlow.MainPage.csproj @@ -0,0 +1,46 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.MainPage + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.MainPage/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.MainPage/ViewModels/MainPageViewModel.cs new file mode 100644 index 0000000000..3c211a6499 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.MainPage/ViewModels/MainPageViewModel.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.SetupFlow.AppManagement; +using DevHome.SetupFlow.Common.Models; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.DevVolume; +using DevHome.SetupFlow.RepoConfig; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; +using Windows.System; + +namespace DevHome.SetupFlow.MainPage.ViewModels; + +/// +/// View model for the main page of the Setup Flow. +/// This page contains controls to start the setup flow with different +/// combinations of steps to perform. For example, only Configuration file, +/// or a full flow with Dev Volume, Clone Repos, and App Management. +/// +public partial class MainPageViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + private readonly IHost _host; + + [ObservableProperty] + private bool _showAppListBackupBanner; + + /// + /// Event raised when the user elects to start the setup flow. + /// The orchestrator for the whole flow subscribes to this event to handle + /// all the work needed at that point. + /// + public event EventHandler> StartSetupFlow; + + public MainPageViewModel(ILogger logger, IStringResource stringResource, IHost host) + : base(stringResource) + { + _logger = logger; + _host = host; + + IsNavigationBarVisible = false; + IsStepPage = false; + } + + /// + /// Starts the setup flow including the pages for the given task groups. + /// + /// The task groups that will be included in this setup flow. + private void StartSetupFlowForTaskGroups(params ISetupTaskGroup[] taskGroups) + { + StartSetupFlow.Invoke(null, taskGroups); + } + + /// + /// Starts a full setup flow, with all the possible task groups. + /// + [RelayCommand] + private void StartSetup() + { + StartSetupFlowForTaskGroups( + _host.GetService(), + _host.GetService(), + _host.GetService()); + } + + /// + /// Starts a setup flow that only includes repo config. + /// + [RelayCommand] + private void StartRepoConfig() + { + StartSetupFlowForTaskGroups(_host.GetService()); + } + + /// + /// Starts a setup flow that only includes app management. + /// + [RelayCommand] + private void StartAppManagement() + { + StartSetupFlowForTaskGroups(_host.GetService()); + } + + /// + /// Starts a setup flow that only includes dev volume. + /// + [RelayCommand] + private void StartDevVolume() + { + StartSetupFlowForTaskGroups(_host.GetService()); + } + + /// + /// Starts a setup flow that only includes configuration file. + /// + [RelayCommand] + private async Task StartConfigurationFileAsync() + { + var configFileSetupFlow = _host.GetService(); + if (await configFileSetupFlow.PickConfigurationFileAsync()) + { + StartSetupFlowForTaskGroups(configFileSetupFlow); + } + } + + [RelayCommand] + private async Task DefaultBannerButtonAsync() + { + // TODO Update code with the "Learn more" button behavior + await Launcher.LaunchUriAsync(new ("https://microsoft.com")); + } + + [RelayCommand] + private void AppListBackupBannerButton() + { + StartSetup(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.MainPage/Views/Banner.xaml b/tools/SetupFlow/DevHome.SetupFlow.MainPage/Views/Banner.xaml new file mode 100644 index 0000000000..17f2430e9c --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.MainPage/Views/Banner.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Clone To + + + Local Path + Developer volume + + + + + + + Clone Path + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/AddRepoDialog.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/AddRepoDialog.xaml.cs new file mode 100644 index 0000000000..2c39244dd3 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/AddRepoDialog.xaml.cs @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.SetupFlow.RepoConfig.Models; +using DevHome.SetupFlow.RepoConfig.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; +using Windows.Storage; +using Windows.Storage.Pickers; +using WinUIEx; +using static DevHome.SetupFlow.RepoConfig.Models.Common; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. +namespace DevHome.SetupFlow.RepoConfig.Views; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +internal sealed partial class AddRepoDialog +{ + // Used in a drop down for the user to select what account to log into. + // Currently 1 account per repository provider. + private readonly ObservableCollection repositoryProviderNamesToShow = new (); + + // Used to keep track of relevant information to make cloning possible. + private readonly CloningInformation cloningInformation; + + private readonly AddRepoViewModel addRepoViewModel = new (); + + // In the repositories page the user can change between repositories by + // changing the account they are looking at. + private ObservableCollection _loginIdsToShow = new (); + + // Holds all dev volume location. + private List devVolumeLocations; + + private List loggedInAccounts = new (); + + private List avalibleRepositoresToSelectFrom = new (); + + // Collection of UserName/RepositoryName + private ObservableCollection repositoriesToShow = new (); + + public AddRepoDialog(CloningInformation cloningInformation) + { + this.cloningInformation = cloningInformation; + + this.InitializeComponent(); + + cloningInformation.CurrentPage = CurrentPage.AddViaUrl; + cloningInformation.CloneLocationSelectionMethod = CloneLocationSelectionMethod.LocalPath; + } + + public async Task StartPlugins() + { + await addRepoViewModel.StartPlugins(); + + foreach (var providerName in addRepoViewModel.QueryForAllProviderNames()) + { + repositoryProviderNamesToShow.Add(providerName); + } + + devVolumeLocations = addRepoViewModel.QueryForNewAndExistingDevVolumes(); + + DevVolumeComboBox.IsEnabled = devVolumeLocations.Any(); + } + + /// + /// Clicking away from URL + /// Can't click on account now only URL. + /// Hide URL UI. + /// Show Account UI. + /// + private void AddViaAccountToggleButton_Click(object sender, RoutedEventArgs e) + { + AddViaUrlToggleButton.IsChecked = false; + TogglePageSwitch(CurrentPage.AddViaAccount); + ToggleCloneButton(); + } + + /// + /// Clicking away from Account + /// Can't click on URL. Only account + /// Show URL UI + /// Hide Account UI + /// + private void AddViaUrlToggleButton_Click(object sender, RoutedEventArgs e) + { + AddViaAccountToggleButton.IsChecked = false; + TogglePageSwitch(CurrentPage.AddViaUrl); + ToggleCloneButton(); + } + + /// + /// Opens the directory picker and saves the location if a location was chosen. + /// + private async void ChooseCloneLocationButton_Click(object sender, RoutedEventArgs e) + { + var maybeCloneLocation = await addRepoViewModel.PickCloneDirectoryAsync(); + + if (maybeCloneLocation != null) + { + // Save the location to both URL and Account text boxes so the user + // can easily switch betweeen them. + CloneLocationForUrlTextBox.Text = maybeCloneLocation.FullName; + CloneLocationForAccountTextBox.Text = maybeCloneLocation.FullName; + + cloningInformation.CloneLocation = new DirectoryInfo(maybeCloneLocation.FullName); + } + + ToggleCloneButton(); + } + + /// + /// Toggles the clone button. Different pages have different logic to figure this out. + /// + private void ToggleCloneButton() + { + // Different pages have different input fields and different validation. + // Different pages save different information. + if (cloningInformation.CurrentPage == CurrentPage.AddViaUrl) + { + // Check if the user entered a url or username/repository combination, and + // the user has selected a clone location. + IsPrimaryButtonEnabled = (cloningInformation.UrlOrUsernameRepo.Length > 0) && + (cloningInformation.CloneLocation != null && cloningInformation.CloneLocation.FullName.Length > 0); + } + else if (cloningInformation.CurrentPage == CurrentPage.AddViaAccount || cloningInformation.CurrentPage == CurrentPage.Repositories) + { + // User has to go through the account dialog before selecting repositories. + // They have the same validation because the repositories selected to clone are saved if the user + // clicks into the URL dialog. + IsPrimaryButtonEnabled = (cloningInformation.RepositoriesToClone.Count > 0) && + cloningInformation.CloneLocation != null && cloningInformation.CloneLocation.FullName.Length > 0; + } + } + + /// + /// switches between the three grids. + /// + /// The page the user is navigating to. Allows us to figure out what to collapse + private void TogglePageSwitch(CurrentPage pageNavigatingTo) + { + // Clicking away from Account or Repositories + if (pageNavigatingTo == CurrentPage.AddViaUrl) + { + AddUrlGrid.Visibility = Visibility.Visible; + AddAccountGrid.Visibility = Visibility.Collapsed; + SelectRepositoriesGrid.Visibility = Visibility.Collapsed; + } + else if (pageNavigatingTo == CurrentPage.AddViaAccount) + { + // Clicking away from URL + AddUrlGrid.Visibility = Visibility.Collapsed; + AddAccountGrid.Visibility = Visibility.Visible; + } + else if (pageNavigatingTo == CurrentPage.Repositories) + { + // User has switched to an account. Show the information. + // Only way to get to this page is via accounts. + AddAccountGrid.Visibility = Visibility.Collapsed; + SelectRepositoriesGrid.Visibility = Visibility.Visible; + ClonePathComboBox.SelectedIndex = 0; + } + + cloningInformation.CurrentPage = pageNavigatingTo; + } + + /// + /// Saves the directory cloning location. + /// + private void RepoUrlOrNameTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + var repoUrlOrName = RepoUrlOrNameTextBox.Text; + if (!string.IsNullOrEmpty(repoUrlOrName)) + { + cloningInformation.UrlOrUsernameRepo = RepoUrlOrNameTextBox.Text; + } + + ToggleCloneButton(); + } + + /// + /// Logs the user into the provider if they aren't already. Then set up the display names for the repositories grid. + /// + private void RepositoryProviderNamesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + var repositoryProviderName = (string)RepositoryProviderNamesComboBox.SelectedItem; + loggedInAccounts = addRepoViewModel.GetAllLoggedInAccounts(repositoryProviderName).ToList(); + + if (!loggedInAccounts.Any()) + { + addRepoViewModel.LogIntoProvider(repositoryProviderName); + + // Logging in opens a webpage. + // Wait a few seconds for authorization to finish. + Thread.Sleep(5000); + loggedInAccounts = addRepoViewModel.GetAllLoggedInAccounts(repositoryProviderName).ToList(); + } + + _loginIdsToShow = new ObservableCollection(loggedInAccounts.Select(developerId => developerId.LoginId())); + + LoginIdsComboBox.ItemsSource = _loginIdsToShow; + LoginIdsComboBox.SelectedIndex = 0; + + TogglePageSwitch(CurrentPage.Repositories); + } + + /// + /// Users have 2 options for choosing a location. Full path or dev volume. This switches between the two grids. + /// + private void ToggleCloneLocationGrids() + { + if (cloningInformation.CloneLocationSelectionMethod == CloneLocationSelectionMethod.LocalPath) + { + CloneLocationForAccountTextBox.Visibility = Visibility.Visible; + ChooseCloneLocationForAccountButton.Visibility = Visibility.Visible; + + DevVolumeComboBox.Visibility = Visibility.Collapsed; + } + else if (cloningInformation.CloneLocationSelectionMethod == CloneLocationSelectionMethod.DevVolume) + { + CloneLocationForAccountTextBox.Visibility = Visibility.Collapsed; + ChooseCloneLocationForAccountButton.Visibility = Visibility.Collapsed; + + DevVolumeComboBox.Visibility = Visibility.Visible; + } + } + + /// + /// Saves how the user wants to pick their cloning location. + /// + private void ClonePathComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + // User can choose between Local Path and dev volume. + var pathChoice = (string)e.AddedItems[0]; + if (pathChoice.Equals("Local Path", StringComparison.OrdinalIgnoreCase)) + { + cloningInformation.CloneLocationSelectionMethod = CloneLocationSelectionMethod.LocalPath; + } + else + { + cloningInformation.CloneLocationSelectionMethod = CloneLocationSelectionMethod.DevVolume; + } + + ToggleCloneLocationGrids(); + } + + /// + /// Removes all shows repositories from the list view and replaces them with a new set of repositories from a + /// diffrent account. + /// + private async void LoginIdsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + // Switching account. Change Repositories. + repositoriesToShow.Clear(); + avalibleRepositoresToSelectFrom.Clear(); + + var loginId = (string)LoginIdsComboBox.SelectedValue; + var developerId = loggedInAccounts.FirstOrDefault(x => x.LoginId().Equals(loginId, StringComparison.OrdinalIgnoreCase)); + + var repositoryProviderName = (string)RepositoryProviderNamesComboBox.SelectedItem; + var repositories = await addRepoViewModel.GetRepositoriesAsync(repositoryProviderName, developerId); + + if (repositories != null) + { + foreach (var repository in repositories) + { + repositoriesToShow.Add(loginId + "/" + repository.DisplayName()); + } + + avalibleRepositoresToSelectFrom = repositories.ToList(); + } + } + + /// + /// Gets the repository to be cloned and either adds or removes it from the list. + /// + private void RepositoriesListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + var loginId = (string)LoginIdsComboBox.SelectedValue; + + var idForRepos = loggedInAccounts.FirstOrDefault(x => x.LoginId() == loginId); + var selectedItems = new List(e.AddedItems); + + // removed and added items go through the same code path. + // Add all removed items to one list. + selectedItems.AddRange(new List(e.RemovedItems)); + + foreach (var selectedItem in selectedItems) + { + var repoInformation = (selectedItem as string).Split("/"); + var repositoryToAddOrRemove = avalibleRepositoresToSelectFrom.FirstOrDefault(x => x.DisplayName().Equals(repoInformation[1], StringComparison.OrdinalIgnoreCase)); + + if (repositoryToAddOrRemove != null) + { + cloningInformation.AddRepositoryOrRemoveIfExists(idForRepos, repositoryToAddOrRemove); + } + } + + ToggleCloneButton(); + } + + /// + /// Writs the cloning location to all text boxes that show a clone location. + /// + private void CloneLocationForUrlTextBox_LostFocus(object sender, RoutedEventArgs e) + { + var locationToCloneTo = string.Empty; + if (cloningInformation.CurrentPage == CurrentPage.AddViaUrl) + { + locationToCloneTo = CloneLocationForUrlTextBox.Text; + } + else if (cloningInformation.CurrentPage == CurrentPage.AddViaAccount || cloningInformation.CurrentPage == CurrentPage.Repositories) + { + locationToCloneTo = CloneLocationForAccountTextBox.Text; + } + + // The control could lose focus when nothing was typed into the text box. + if (string.IsNullOrEmpty(locationToCloneTo) || string.IsNullOrWhiteSpace(locationToCloneTo)) + { + return; + } + + // Did the user put in an absolute path? + if (Path.IsPathRooted(locationToCloneTo)) + { + cloningInformation.CloneLocation = new DirectoryInfo(locationToCloneTo); + } + else + { + // ApplicationData isn't the best option. This can be discussed. + var fullPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), locationToCloneTo); + cloningInformation.CloneLocation = new DirectoryInfo(fullPath); + } + + // Modify the path in both URL and account so the user does not have to re-enter details. + // If the user puts in a relative path, should we replace the text with the full path so the user can see where + // the repos will be cloned? + CloneLocationForUrlTextBox.Text = locationToCloneTo; + CloneLocationForAccountTextBox.Text = locationToCloneTo; + + ToggleCloneButton(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigReviewView.xaml new file mode 100644 index 0000000000..93d3955a6f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigReviewView.xaml @@ -0,0 +1,16 @@ + + + + + + + Repo config review + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigReviewView.xaml.cs new file mode 100644 index 0000000000..5119f9939d --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigReviewView.xaml.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.RepoConfig.Views; +public sealed partial class RepoConfigReviewView : UserControl +{ + public RepoConfigReviewView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigView.xaml b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigView.xaml new file mode 100644 index 0000000000..0748b57990 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigView.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + Repository Connection + This page is for connection a repo. + + + + No account is selected yet + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigView.xaml.cs new file mode 100644 index 0000000000..aa3d7b3d9e --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.RepoConfig/Views/RepoConfigView.xaml.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Extensions; +using DevHome.Contracts.Services; +using DevHome.SetupFlow.RepoConfig.Models; +using DevHome.SetupFlow.RepoConfig.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Markup; +using Microsoft.Windows.DevHome.SDK; + +namespace DevHome.SetupFlow.RepoConfig.Views; + +public sealed partial class RepoConfigView : UserControl +{ + public RepoConfigView() + { + this.InitializeComponent(); + } + + private readonly CloningInformation _cloningInformation = new (); + + public RepoConfigViewModel ViewModel => (RepoConfigViewModel)this.DataContext; + + private async void AddRepoButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var addRepoDialog = new AddRepoDialog(_cloningInformation); + await addRepoDialog.StartPlugins(); + var themeService = Application.Current.GetService(); + addRepoDialog.XamlRoot = RepoConfigRelativePanel.XamlRoot; + addRepoDialog.RequestedTheme = themeService.Theme; + var result = await addRepoDialog.ShowAsync(ContentDialogPlacement.InPlace); + + if (result == ContentDialogResult.Primary) + { + ViewModel.SaveRepoInformation(_cloningInformation); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Review/DevHome.SetupFlow.Review.csproj b/tools/SetupFlow/DevHome.SetupFlow.Review/DevHome.SetupFlow.Review.csproj new file mode 100644 index 0000000000..4e1ac48d41 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Review/DevHome.SetupFlow.Review.csproj @@ -0,0 +1,27 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.Review + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Review/Selectors/ReviewTabViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow.Review/Selectors/ReviewTabViewSelector.cs new file mode 100644 index 0000000000..6dc08674b0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Review/Selectors/ReviewTabViewSelector.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using DevHome.SetupFlow.AppManagement.ViewModels; +using DevHome.SetupFlow.DevVolume.ViewModels; +using DevHome.SetupFlow.RepoConfig.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Review.Selectors; + +/// +/// Data template selector class for rendering the current active tab in the review page. +/// For example, if the DevVolumeReviewViewModel is currently bound to the +/// content control, then the DevVolumeReviewView will render. +/// +public class ReviewTabViewSelector : DataTemplateSelector +{ + public DataTemplate DevVolumeTabTemplate + { + get; set; + } + + public DataTemplate RepoConfigTabTemplate + { + get; set; + } + + public DataTemplate AppManagementTabTemplate + { + get; set; + } + + protected override DataTemplate SelectTemplateCore(object item) + { + return ResolveDataTemplate(item, () => base.SelectTemplateCore(item)); + } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + return ResolveDataTemplate(item, () => base.SelectTemplateCore(item, container)); + } + + /// + /// Resolve the data template for the given object type. + /// + /// Selected item. + /// Default data template function. + /// Data template or default data template if no corresponding data template was found. + private DataTemplate ResolveDataTemplate(object item, Func defaultDataTemplate) + { + return item switch + { + DevVolumeReviewViewModel => DevVolumeTabTemplate, + RepoConfigReviewViewModel => RepoConfigTabTemplate, + AppManagementReviewViewModel => AppManagementTabTemplate, + _ => defaultDataTemplate(), + }; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Review/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.Review/ViewModels/ReviewViewModel.cs new file mode 100644 index 0000000000..16c5d3a797 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Review/ViewModels/ReviewViewModel.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.Review.ViewModels; + +public partial class ReviewViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + private readonly IHost _host; + private readonly SetupFlowOrchestrator _orchestrator; + + [ObservableProperty] + private bool _isEulaAccepted = false; + + [ObservableProperty] + private bool _isRebootRequired = false; + + [ObservableProperty] + private bool _isRebootAccepted = false; + + [ObservableProperty] + private IList _reviewTabs; + + [ObservableProperty] + private ReviewTabViewModelBase _selectedReviewTab; + + public ReviewViewModel(ILogger logger, IStringResource stringResource, IHost host, SetupFlowOrchestrator orchestrator) + : base(stringResource) + { + _logger = logger; + _host = host; + _orchestrator = orchestrator; + + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton); + CanGoToNextPage = false; + } + + public async override void OnNavigateToPageAsync() + { + IsRebootRequired = _orchestrator.TaskGroups.Any(taskGroup => taskGroup.SetupTasks.Any(task => task.RequiresReboot)); + ReviewTabs = _orchestrator.TaskGroups.Select(taskGroup => taskGroup.GetReviewTabViewModel()).ToList(); + SelectedReviewTab = ReviewTabs.FirstOrDefault(); + await Task.CompletedTask; + } + + partial void OnIsEulaAcceptedChanged(bool value) => UpdateCanGoToNextPage(); + + partial void OnIsRebootAcceptedChanged(bool value) => UpdateCanGoToNextPage(); + + public void UpdateCanGoToNextPage() + { + CanGoToNextPage = + IsEulaAccepted && + (!IsRebootRequired || IsRebootAccepted) && + _orchestrator.TaskGroups.Any(g => g.SetupTasks.Any()); + _orchestrator.NotifyNavigationCanExecuteChanged(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Review/Views/ReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow.Review/Views/ReviewView.xaml new file mode 100644 index 0000000000..dd69424545 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Review/Views/ReviewView.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Review/Views/ReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.Review/Views/ReviewView.xaml.cs new file mode 100644 index 0000000000..02f64d4848 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Review/Views/ReviewView.xaml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.Review.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Review.Views; + +public sealed partial class ReviewView : UserControl +{ + public ReviewView() + { + this.InitializeComponent(); + } + + public ReviewViewModel ViewModel => (ReviewViewModel)this.DataContext; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Summary/DevHome.SetupFlow.Summary.csproj b/tools/SetupFlow/DevHome.SetupFlow.Summary/DevHome.SetupFlow.Summary.csproj new file mode 100644 index 0000000000..cd4e812c29 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Summary/DevHome.SetupFlow.Summary.csproj @@ -0,0 +1,24 @@ + + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow.Summary + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Summary/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow.Summary/ViewModels/SummaryViewModel.cs new file mode 100644 index 0000000000..f25db69d8a --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Summary/ViewModels/SummaryViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.Telemetry; + +namespace DevHome.SetupFlow.Summary.ViewModels; + +public partial class SummaryViewModel : SetupPageViewModelBase +{ + private readonly ILogger _logger; + + public SummaryViewModel(ILogger logger, IStringResource stringResource) + : base(stringResource) + { + _logger = logger; + IsNavigationBarVisible = false; + IsStepPage = false; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Summary/Views/SummaryView.xaml b/tools/SetupFlow/DevHome.SetupFlow.Summary/Views/SummaryView.xaml new file mode 100644 index 0000000000..9a3e727f8f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Summary/Views/SummaryView.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + Summary: Content goes here + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Summary/Views/SummaryView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow.Summary/Views/SummaryView.xaml.cs new file mode 100644 index 0000000000..7c56d6efeb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.Summary/Views/SummaryView.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using DevHome.SetupFlow.Summary.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Summary.Views; + +public sealed partial class SummaryView : UserControl +{ + public SummaryView() + { + this.InitializeComponent(); + } + + public SummaryViewModel ViewModel => (SummaryViewModel)this.DataContext; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UITest/DevHome.SetupFlow.UITest.csproj b/tools/SetupFlow/DevHome.SetupFlow.UITest/DevHome.SetupFlow.UITest.csproj new file mode 100644 index 0000000000..c538255786 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UITest/DevHome.SetupFlow.UITest.csproj @@ -0,0 +1,19 @@ + + + net6.0-windows10.0.19041.0 + SetupFlow.UITest + x86;x64;arm64 + false + enable + true + true + resources.pri + + + + + + + + + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow.UITest/SetupFlowScenarioStandard.cs b/tools/SetupFlow/DevHome.SetupFlow.UITest/SetupFlowScenarioStandard.cs new file mode 100644 index 0000000000..33acd5e4b7 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UITest/SetupFlowScenarioStandard.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Remote; +using static System.Collections.Specialized.BitVector32; + +namespace DevHome.SetupFlow.UITest; + +[TestClass] +public class SetupFlowScenarioStandard : SetupFlowSession +{ + [TestMethod] + public void SetupFlowTest1() + { + session.FindElementByName("Dev Setup tool").Click(); + WindowsElement title = session.FindElementByName("Add packages"); + Assert.AreEqual("Add packages", title.Text); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + Setup(context); + } + + [ClassCleanup] + public static void ClassCleanup() + { + TearDown(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UITest/SetupFlowSession.cs b/tools/SetupFlow/DevHome.SetupFlow.UITest/SetupFlowSession.cs new file mode 100644 index 0000000000..1d627ef351 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UITest/SetupFlowSession.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; + +namespace DevHome.SetupFlow.UITest; + +public class SetupFlowSession +{ + private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723"; + private const string DevHomeAppId = "Microsoft.DevHome_8wekyb3d8bbwe!App"; + +#pragma warning disable SA1401 // Fields should be private +#pragma warning disable CA2211 // Non-constant fields should not be visible + protected static WindowsDriver session; +#pragma warning restore CA2211 // Non-constant fields should not be visible +#pragma warning restore SA1401 // Fields should be private + + public static void Setup(TestContext context) + { + if (session == null) + { + // Create a new session to bring up an instance of the Dev Home application + // Note: Multiple calculator windows (instances) share the same process Id + AppiumOptions options = new AppiumOptions(); + options.AddAdditionalCapability("deviceName", "WindowsPC"); + options.AddAdditionalCapability("platformName", "Windows"); + options.AddAdditionalCapability("app", DevHomeAppId); + + session = new WindowsDriver(new Uri(WindowsApplicationDriverUrl), options); + Assert.IsNotNull(session); + + // Set implicit timeout to 1.5 seconds to make element search to retry every 500 ms for at most three times + session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1.5); + } + } + + public static void TearDown() + { + // Close the application and delete the session + if (session != null) + { + session.Quit(); + session = null; + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj new file mode 100644 index 0000000000..e28ff369d3 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj @@ -0,0 +1,25 @@ + + + net6.0-windows10.0.19041.0 + DevHome.SetupFlow.UnitTest + x86;x64;arm64 + false + enable + enable + true + true + resources.pri + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs new file mode 100644 index 0000000000..8690fbda15 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.SetupFlow.AppManagement.Models; +using Moq; + +namespace DevHome.SetupFlow.UnitTest.Helpers; +public class PackageHelper +{ + public static Mock CreatePackage() + { + var package = new Mock(); + package.Setup(p => p.Name).Returns("Mock Package Name"); + package.Setup(p => p.ImageUri).Returns(new Uri("https://mock/")); + package.Setup(p => p.PackageUri).Returns(new Uri("https://microsoft.com")); + package.Setup(p => p.Version).Returns("Mock Version"); + return package; + } + + public static PackageCatalog CreatePackageCatalog(int packageCount, Action? customizeCatalog = null) + { + var packageCatalog = new PackageCatalog() + { + Name = "Mock PackageCatalog Name", + Description = "Mock PackageCatalog Description", + Packages = Enumerable.Range(1, packageCount).Select(x => CreatePackage().Object).ToList(), + }; + customizeCatalog?.Invoke(packageCatalog); + return packageCatalog; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Initialize.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Initialize.cs new file mode 100644 index 0000000000..4bd9c0e7e7 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Initialize.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Microsoft.Windows.ApplicationModel.DynamicDependency; + +[assembly: WinUITestTarget(typeof(DevHome.App))] + +namespace DevHome.SetupFlow.Test; + +[TestClass] +public class Initialize +{ + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + // TODO: Initialize the appropriate version of the Windows App SDK. + // This is required when testing MSIX apps that are framework-dependent on the Windows App SDK. + Bootstrap.TryInitialize(0x00010001, out var _); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + Bootstrap.Shutdown(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Usings.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Usings.cs new file mode 100644 index 0000000000..037943bebe --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Usings.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageCatalogViewModelTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageCatalogViewModelTest.cs new file mode 100644 index 0000000000..22f999a07f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageCatalogViewModelTest.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using DevHome.SetupFlow.UnitTest.Helpers; + +namespace DevHome.SetupFlow.UnitTest; + +[TestClass] +public class PackageCatalogViewModelTest +{ + [TestMethod] + public void NewPackageCatalogViewModel_Success() + { + var packageCatalog = PackageHelper.CreatePackageCatalog(10); + var packageViewModel = new PackageCatalogViewModel(packageCatalog); + Assert.AreEqual(packageCatalog.Name, packageViewModel.Name); + Assert.AreEqual(packageCatalog.Description, packageViewModel.Description); + Assert.AreEqual(packageCatalog.Packages.Count, packageViewModel.Packages.Count); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs new file mode 100644 index 0000000000..a35dd90c12 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.SetupFlow.AppManagement.ViewModels; +using DevHome.SetupFlow.UnitTest.Helpers; + +namespace DevHome.SetupFlow.UnitTest.ViewModels; + +[TestClass] +public class PackageViewModelTest +{ + [TestMethod] + public void CreatePackageViewModel_Success() + { + var packageCatalog = PackageHelper.CreatePackageCatalog(10); + var packageViewModel = new PackageCatalogViewModel(packageCatalog); + var expectedPackages = packageCatalog.Packages.ToList(); + var packages = packageViewModel.Packages.ToList(); + Assert.AreEqual(expectedPackages.Count, packages.Count); + for (var i = 0; i < expectedPackages.Count; ++i) + { + Assert.AreEqual(expectedPackages[i].Name, packages[i].Name); + Assert.AreEqual(expectedPackages[i].ImageUri, packages[i].ImageUri); + Assert.AreEqual(expectedPackages[i].Version, packages[i].Version); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/SearchViewModelTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/SearchViewModelTest.cs new file mode 100644 index 0000000000..08fe2e041b --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/SearchViewModelTest.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHome.Common.Services; +using DevHome.SetupFlow.AppManagement.Exceptions; +using DevHome.SetupFlow.AppManagement.Models; +using DevHome.SetupFlow.AppManagement.Services; +using DevHome.SetupFlow.AppManagement.ViewModels; +using DevHome.Telemetry; +using Moq; + +namespace DevHome.SetupFlow.UnitTest.ViewModels; + +[TestClass] +public class SearchViewModelTest +{ + private Mock? _logger; + private Mock? _stringResource; + private Mock? _wpm; + + [TestInitialize] + public void TestInitialize() + { + _logger = new Mock(); + _stringResource = new Mock(); + _wpm = new Mock(); + } + + [TestMethod] + [DataRow("")] // Empty + [DataRow(" ")] // Space + [DataRow(null)] + public void Search_NullOrEmptyText_ReturnsEmptyStatusAndNull(string text) + { + var searchViewModel = new SearchViewModel(_logger!.Object, _wpm!.Object, _stringResource!.Object); + + var (status, packages) = searchViewModel.SearchAsync(text, new CancellationToken(canceled: false)).GetAwaiter().GetResult(); + + Assert.AreEqual(SearchViewModel.SearchResultStatus.EmptySearchQuery, status); + Assert.IsNull(packages); + } + + [TestMethod] + public void Search_CancelledToken_ReturnsCancelledStatusAndNull() + { + var allcatalogs = new Mock(); + allcatalogs.Setup(c => c.IsConnected).Returns(true); + var searchViewModel = new SearchViewModel(_logger!.Object, _wpm!.Object, _stringResource!.Object); + _wpm.Setup(wpm => wpm.AllCatalogs).Returns(allcatalogs.Object); + + var (status, packages) = searchViewModel.SearchAsync("mock", new CancellationToken(canceled: true)).GetAwaiter().GetResult(); + + Assert.AreEqual(SearchViewModel.SearchResultStatus.Canceled, status); + Assert.IsNull(packages); + } + + [TestMethod] + public void Search_CatalogNotConnected_ReturnsNotConnectedStatusAndNull() + { + var allcatalogs = new Mock(); + allcatalogs.Setup(c => c.IsConnected).Returns(false); + var searchViewModel = new SearchViewModel(_logger!.Object, _wpm!.Object, _stringResource!.Object); + _wpm.Setup(wpm => wpm.AllCatalogs).Returns(allcatalogs.Object); + + var (status, packages) = searchViewModel.SearchAsync("mock", new CancellationToken(false)).GetAwaiter().GetResult(); + + Assert.AreEqual(SearchViewModel.SearchResultStatus.CatalogNotConnect, status); + Assert.IsNull(packages); + } + + [TestMethod] + public void Search_Exception_ReturnsExceptionStatusAndNull() + { + var allcatalogs = new Mock(); + allcatalogs.Setup(c => c.IsConnected).Returns(true); + allcatalogs.Setup(c => c.SearchAsync(It.IsAny())).ThrowsAsync(new InvalidOperationException()); + var searchViewModel = new SearchViewModel(_logger!.Object, _wpm!.Object, _stringResource!.Object); + _wpm.Setup(wpm => wpm.AllCatalogs).Returns(allcatalogs.Object); + + var (status, packages) = searchViewModel.SearchAsync("mock", new CancellationToken(false)).GetAwaiter().GetResult(); + + Assert.AreEqual(SearchViewModel.SearchResultStatus.ExceptionThrown, status); + Assert.IsNull(packages); + } + + [TestMethod] + public void Search_NonEmptyText_ReturnsOkStatusAndNonNullResult() + { + var allcatalogs = new Mock(); + allcatalogs.Setup(c => c.IsConnected).Returns(true); + allcatalogs.Setup(c => c.SearchAsync(It.IsAny())).ReturnsAsync(new List() + { + // Mock a single result package + new Mock().Object, + }); + var searchViewModel = new SearchViewModel(_logger!.Object, _wpm!.Object, _stringResource!.Object); + _wpm.Setup(wpm => wpm.AllCatalogs).Returns(allcatalogs.Object); + + var (status, packages) = searchViewModel.SearchAsync("mock", new CancellationToken(false)).GetAwaiter().GetResult(); + + Assert.AreEqual(SearchViewModel.SearchResultStatus.Ok, status); + Assert.AreEqual(1, packages.Count); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Assets/DefaultPackageIcon.png b/tools/SetupFlow/DevHome.SetupFlow/Assets/DefaultPackageIcon.png new file mode 100644 index 0000000000..6d22352004 Binary files /dev/null and b/tools/SetupFlow/DevHome.SetupFlow/Assets/DefaultPackageIcon.png differ diff --git a/tools/SetupFlow/DevHome.SetupFlow/Assets/MainPageDefaultBanner.png b/tools/SetupFlow/DevHome.SetupFlow/Assets/MainPageDefaultBanner.png new file mode 100644 index 0000000000..8447d8212b Binary files /dev/null and b/tools/SetupFlow/DevHome.SetupFlow/Assets/MainPageDefaultBanner.png differ diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj new file mode 100644 index 0000000000..db6570b77f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -0,0 +1,56 @@ + + + net6.0-windows10.0.19041.0 + 10.0.17763.0 + DevHome.SetupFlow + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(DefaultXamlRuntime) + Designer + + + + + + MSBuild:Compile + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs new file mode 100644 index 0000000000..e585bc012c --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using DevHome.SetupFlow.AppManagement.ViewModels; +using DevHome.SetupFlow.ConfigurationFile.ViewModels; +using DevHome.SetupFlow.DevVolume.ViewModels; +using DevHome.SetupFlow.Loading.ViewModels; +using DevHome.SetupFlow.MainPage.ViewModels; +using DevHome.SetupFlow.RepoConfig.ViewModels; +using DevHome.SetupFlow.Review.ViewModels; +using DevHome.SetupFlow.Summary.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Common.Selectors; + +/// +/// Data template selector class for rendering the current active page in the +/// setup flow. For example, if the MainPageViewModel is currently bound to the +/// content control, then the MainPageView will render. +/// +public class SetupFlowViewSelector : DataTemplateSelector +{ + public DataTemplate MainPageTemplate { get; set; } + + public DataTemplate DevVolumeTemplate { get; set; } + + public DataTemplate RepoConfigTemplate { get; set; } + + public DataTemplate AppManagementTemplate { get; set; } + + public DataTemplate ReviewTemplate { get; set; } + + public DataTemplate LoadingTemplate { get; set; } + + public DataTemplate SummaryTemplate { get; set; } + + public DataTemplate ConfigurationFileTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item) + { + return ResolveDataTemplate(item, () => base.SelectTemplateCore(item)); + } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + return ResolveDataTemplate(item, () => base.SelectTemplateCore(item, container)); + } + + /// + /// Resolve the data template for the given object type. + /// + /// Selected item. + /// Default data template function. + /// Data template or default data template if no corresponding data template was found. + private DataTemplate ResolveDataTemplate(object item, Func defaultDataTemplate) + { + return item switch + { + MainPageViewModel => MainPageTemplate, + DevVolumeViewModel => DevVolumeTemplate, + RepoConfigViewModel => RepoConfigTemplate, + AppManagementViewModel => AppManagementTemplate, + ReviewViewModel => ReviewTemplate, + LoadingViewModel => LoadingTemplate, + SummaryViewModel => SummaryTemplate, + ConfigurationFileViewModel => ConfigurationFileTemplate, + _ => defaultDataTemplate(), + }; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..852080ace0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -0,0 +1,356 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add all + Label for adding all items for selection + + + Select packages to restore from your backup, clone from repository, or search with Windows Package Manager. + Description for Add packages page + + + Transfer developer machine settings + Title text of an instruction banner section for transferring settings from a developer machine + + + Use Dev Home to quickly transfer you development machine settings from another PC to get back to coding. + {Locked="Dev Home"}Description text of an instruction banner section for transferring settings from a developer machine + + + Get started + Button text for getting started with transferring settings from a developer machine + + + Install Applications + Description for the application management page + + + Install Applications + Title for the application management page + + + I have read, understand and agree to the above policy + Checkbox label for reading, understanding and agreeing to policies presented above this label + + + {0} application(s) selected + {Locked="{0}"} Label for the number of selected applications. {0} is replaced by the number of selected applications. + + + Browse + Label for a browse button + + + Cancel + Label for a cancel button + + + Close + Label for a close button + + + Configuration file name (Preview) + Title of the configuration file window + + + The configuration file should be a YAML file. Windows 11 marks a visual evolutions of the operating system. + Configuration file type not supported error message + + + Configuration file warning + Title of the configuration file warning message + + + This is a test feature. Windows 11 marks a visual evolution of the operating system. We have evolved our design language alongside with Fluent to create a design which is human, universal and truly feels like Windows. + Text of the configuration file warninig message + + + Get ready to code in minutes + Title text of an instruction banner section for setting up a development machine + + + Find out how you can use Dev Home to get your development machine set up quickly. + {Locked="Dev Home"}Description text of an instruction banner section for setting up a development machine + + + Learn more + Button text for learning more about setting up a development machine + + + File type not supported + File type not supported error message + + + Learn more + Text for a clickable learn more button + + + Get started on a project by cloning a repository + Body text description for a card that when clicked takes the user to a page for cloning git repositories + + + Clone a repository + Header for a card that when clicked takes the user to a page for cloning git repositories + + + Use one or more configuration (YAML) files + Body text description for a card that when clicked allows the user to select a configuration file and run it + + + Use a configuration file + Header for a card that when clicked allows the user to select a configuration file and run it + + + Create a new virtual hard disk for development projects + Body text description for a card that when clicked takes the user to a page for creating a developer drive/volume + + + Add a developer drive + Header for a card that when clicked takes the user to a page for creating a developer drive (virtual hard disk) + + + Set up development environment + Header text for a group of controls giving multiple choices to set up the environment/machine + + + Download apps and packages from the WinGet library + Body text description for a card that when clicked takes the user to a page for installing apps. WinGet refers to the Windows Package Manager command line tool. {StrContains=WinGet} + + + Install apps and packages + Header for a card that when clicked takes the user to a page for installing apps + + + Quick steps + Header text for a group of controls giving multiple choices for configuring the machine, but not a full setup flow + + + Use an end-to-end- setup flow + Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine + + + Step-by-step + Header for a card than when clicked takes the user to a multi-step flow for setting up their machine + + + No apps to install + Text shown if no applications were selected to install + + + Dev Setup tool + Navigation pane content + + + No applications selected + Text displayed when a list of applications selected by the user is empty + + + No results found for "{0}" + {Locked="{0}"}Text displayed when no search results were found. {0} is replaced by the search query. + + + Did't find what you're looking for? Check the spelling, try new keywords. + Text displayed when no search results were found + + + {0} Result(s) + {Locked="{0}"} Label for a search result counter. {0} is replaced by the number of result items. + + + {0} packages + {Locked="{0}"} Display the number of packages. {0} is replaced by the number of packages. + + + Previous + Label for a "go to previous page" button + + + Restore + Label for restore button + + + End-User License Agreement + Header for the license agreement text + + + IMPORTANT—READ CAREFULLY: This Microsoft End-User License Agreement ("EULA") is ... + License agreements + + + I read the terms and allow Windows to accept all EULA on my behalf + Checkbox text for user to accept the EULA + + + I allow Windows to automatically reboot my system + Checkbox text for user to accept the possibility of reboots during setup + + + Set up details + Header for a section detailing the set up steps to be performed. "Set up" is the noun + + + Terms + As in "Terms of use" + + + By clicking Create, I (a) agree to the legal terms and privacy statement(s) associated with the Marketplace offering(s) listed above; (b) authorize Microsoft to bill my current payment method for the fees associated with the offering(s), with the same billing frequency as my Azure subscription; and (c) agree that Microsoft may share my contact, usage and transactional information with the provider(s) of the offering(s) for support, billing and other transactional activities. Microsoft does not provide rights for third-party offerings. + Terms of use shown before starting setup. + + + System reboot + Header for text indicating that a reboot may be needed + + + A reboot may be needed + Text indicating that a reboot may be needed to finish the setup + + + Try "GitHub" + {Locked="GitHub"}Placeholder text displayed in a search box + + + Select all + Label for select all checkbox + + + Selected packages + Label for selected packages + + + Set up + Label for the button that starts the setup + + + Applications + Header for a section showing a summary of applications to be installed + + + Basics + Header for a section showing a summary of "basic" setup steps to be performed + + + Repository + Header for a section showing a summary of repositories to be cloned to the machine + + + View {0} + {Locked="{0}"} Text for the view action of a file content. {0} is replaced by a file name. + + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/SetupFlowStyles.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/SetupFlowStyles.xaml new file mode 100644 index 0000000000..f18ffbb764 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/SetupFlowStyles.xaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs new file mode 100644 index 0000000000..f9267df012 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.Common.Models; +using DevHome.SetupFlow.Common.Services; +using DevHome.SetupFlow.Common.ViewModels; +using DevHome.SetupFlow.Loading.ViewModels; +using DevHome.SetupFlow.MainPage.ViewModels; +using DevHome.SetupFlow.Review.ViewModels; +using DevHome.SetupFlow.Summary.ViewModels; +using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; + +namespace DevHome.SetupFlow.ViewModels; + +public partial class SetupFlowViewModel : ObservableObject +{ + private readonly IHost _host; + private readonly ILogger _logger; + private readonly SetupFlowOrchestrator _orchestrator; + private readonly MainPageViewModel _mainPageViewModel; + private readonly List _flowPages; + private int _currentPageIndex; + + [ObservableProperty] + private SetupPageViewModelBase _currentPageViewModel; + + public bool IsPreviousButtonVisible => _currentPageIndex > 0; + + private int CurrentPageIndex + { + get => _currentPageIndex; + set + { + _currentPageIndex = value; + CurrentPageViewModel = _flowPages[_currentPageIndex]; + CurrentPageViewModel.OnNavigateToPageAsync(); + _orchestrator.NotifyNavigationCanExecuteChanged(); + OnPropertyChanged(nameof(IsPreviousButtonVisible)); + } + } + + public SetupFlowViewModel(IHost host, ILogger logger, SetupFlowOrchestrator orchestrator) + { + _host = host; + _logger = logger; + _orchestrator = orchestrator; + + _orchestrator.SetNavigationButtonsCommands(new List { GoToNextPageCommand, GoToPreviousPageCommand, CancelCommand }); + + // Set initial view + _mainPageViewModel = _host.GetService(); + _flowPages = new List + { + _mainPageViewModel, + }; + + CurrentPageIndex = 0; + + _mainPageViewModel.StartSetupFlow += (object sender, IList taskGroups) => + { + _orchestrator.TaskGroups = taskGroups; + StartSetupFlowWithCurrentTaskGroups(); + }; + } + + private void StartSetupFlowWithCurrentTaskGroups() + { + _flowPages.Clear(); + _flowPages.AddRange(_orchestrator.TaskGroups.Select(flow => flow.GetSetupPageViewModel())); + _flowPages.Add(_host.GetService()); + + // The Loading page can advance to the next page + // without user interaction once it is complete + var loadingPageViewModel = _host.GetService(); + _flowPages.Add(loadingPageViewModel); + + /* + loadingPageViewModel.ExecutionFinished += (object _, EventArgs _) => + { + GoToNextPage(); + }; + */ + + _flowPages.Add(_host.GetService()); + + CurrentPageIndex = 0; + } + + [RelayCommand(CanExecute = nameof(CanGoToPreviousPage))] + public void GoToPreviousPage() + { + CurrentPageIndex--; + } + + private bool CanGoToPreviousPage() + { + return CurrentPageIndex > 0 && CurrentPageViewModel.CanGoToPreviousPage; + } + + [RelayCommand(CanExecute = nameof(CanGoToNextPage))] + public void GoToNextPage() + { + CurrentPageIndex++; + } + + private bool CanGoToNextPage() + { + return CurrentPageIndex + 1 < _flowPages.Count && CurrentPageViewModel.CanGoToNextPage; + } + + [RelayCommand(CanExecute = nameof(CanCancel))] + public void Cancel() + { + _flowPages.Clear(); + _flowPages.Add(_mainPageViewModel); + + CurrentPageIndex = 0; + } + + private bool CanCancel() + { + return CurrentPageViewModel.CanCancel; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml new file mode 100644 index 0000000000..9c8f0d4597 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +