[EnvVar][Hosts][RegPrev]Decouple and refactor to make it "packable" as nuget package (#32604)

* WIP Hosts - remove deps

* Add consumer app

* Move App and MainWindow to Consumer app. Make Hosts dll

* Try consume it

* Fix errors

* Make it work with custom build targets

* Dependency injection
Refactor
Explicit page creation
Wire missing dependencies

* Fix installer

* Remove unneeded stuff

* Fix build again

* Extract UI and logic from MainWindow to RegistryPreviewMainPage

* Convert to lib
Change namespace to RegistryPreviewUILib
Remove PT deps

* Add exe app and move App.xaml and MainWindow.xaml

* Consume the lib

* Update Hosts package creation

* Fix RegistryPreview package creation

* Rename RegistryPreviewUI back to RegistryPreview

* Back to consuming lib

* Ship icons and assets in nuget packages

* Rename to EnvironmentVariablesUILib and convert to lib

* Add app and consume

* Telemetry

* GPO

* nuget

* Rename HostsPackageConsumer to Hosts and Hosts lib to HostsUILib

* Assets cleanup

* nuget struct

* v0

* assets

* [Hosts] Re-add AppList to Lib Assets, [RegPrev] Copy lib assets to out dir

* Sign UI dlls

* Revert WinUIEx bump

* Cleanup

* Align deps

* version exception dll

* Fix RegistryPreview crashes

* XAML format

* XAML format 2

* Pack .pri files in lib/ dir

---------

Co-authored-by: Darshak Bhatti <dabhatti@microsoft.com>
This commit is contained in:
Stefan Markovic
2024-04-26 19:41:44 +02:00
committed by GitHub
parent 28ba2bd301
commit 41a0114efe
125 changed files with 2097 additions and 1212 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using EnvironmentVariablesUILib.Models;
using Microsoft.UI.Xaml.Data;
namespace EnvironmentVariablesUILib.Converters;
public class EnvironmentStateToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var type = (EnvironmentState)value;
return type switch
{
EnvironmentState.Unchanged => false,
_ => true,
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using EnvironmentVariablesUILib.Helpers;
using EnvironmentVariablesUILib.Models;
using Microsoft.UI.Xaml.Data;
namespace EnvironmentVariablesUILib.Converters;
public class EnvironmentStateToMessageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
var type = (EnvironmentState)value;
return type switch
{
EnvironmentState.Unchanged => string.Empty,
EnvironmentState.ChangedOnStartup => resourceLoader.GetString("StateNotUpToDateOnStartupMsg"),
EnvironmentState.EnvironmentMessageReceived => resourceLoader.GetString("StateNotUpToDateEnvironmentMessageReceivedMsg"),
EnvironmentState.ProfileNotApplicable => resourceLoader.GetString("StateProfileNotApplicableMsg"),
_ => throw new NotImplementedException(),
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using EnvironmentVariablesUILib.Helpers;
using EnvironmentVariablesUILib.Models;
using Microsoft.UI.Xaml.Data;
namespace EnvironmentVariablesUILib.Converters;
public class EnvironmentStateToTitleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
var type = (EnvironmentState)value;
return type switch
{
EnvironmentState.ProfileNotApplicable => resourceLoader.GetString("ProfileNotApplicableTitle"),
_ => resourceLoader.GetString("StateNotUpToDateTitle"),
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using EnvironmentVariablesUILib.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace EnvironmentVariablesUILib.Converters;
public class EnvironmentStateToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var type = (EnvironmentState)value;
return type switch
{
EnvironmentState.Unchanged => Visibility.Collapsed,
_ => Visibility.Visible,
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using EnvironmentVariablesUILib.Models;
using Microsoft.UI.Xaml.Data;
namespace EnvironmentVariablesUILib.Converters;
public class VariableTypeToGlyphConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var type = (VariablesSetType)value;
return type switch
{
VariablesSetType.User => "\uE77B",
VariablesSetType.System => "\uE977",
VariablesSetType.Profile => "\uEDE3",
VariablesSetType.Path => "\uE8AC",
VariablesSetType.Duplicate => "\uE8C8",
_ => throw new NotImplementedException(),
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,912 @@
<Page
x:Class="EnvironmentVariablesUILib.EnvironmentVariablesMainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:EnvironmentVariablesUILib.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:EnvironmentVariablesUILib.Models"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
x:Name="RootPage"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<!-- These resource dictionaries are needed to reference styles part of SettingsControls -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<StaticResource x:Key="SubtleButtonBackground" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonBackgroundPointerOver" ResourceKey="SubtleFillColorSecondary" />
<StaticResource x:Key="SubtleButtonBackgroundPressed" ResourceKey="SubtleFillColorTertiary" />
<StaticResource x:Key="SubtleButtonBackgroundDisabled" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonBorderBrush" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonBorderBrushPointerOver" ResourceKey="SubtleFillColorSecondary" />
<StaticResource x:Key="SubtleButtonBorderBrushPressed" ResourceKey="SubtleFillColorTertiary" />
<StaticResource x:Key="SubtleButtonBorderBrushDisabled" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonForeground" ResourceKey="TextFillColorPrimary" />
<StaticResource x:Key="SubtleButtonForegroundPointerOver" ResourceKey="TextFillColorPrimary" />
<StaticResource x:Key="SubtleButtonForegroundPressed" ResourceKey="TextFillColorSecondary" />
<StaticResource x:Key="SubtleButtonForegroundDisabled" ResourceKey="TextFillColorDisabled" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="SubtleButtonBackground" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonBackgroundPointerOver" ResourceKey="SubtleFillColorSecondary" />
<StaticResource x:Key="SubtleButtonBackgroundPressed" ResourceKey="SubtleFillColorTertiary" />
<StaticResource x:Key="SubtleButtonBackgroundDisabled" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonBorderBrush" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonBorderBrushPointerOver" ResourceKey="SubtleFillColorSecondary" />
<StaticResource x:Key="SubtleButtonBorderBrushPressed" ResourceKey="SubtleFillColorTertiary" />
<StaticResource x:Key="SubtleButtonBorderBrushDisabled" ResourceKey="SubtleFillColorTransparent" />
<StaticResource x:Key="SubtleButtonForeground" ResourceKey="TextFillColorPrimary" />
<StaticResource x:Key="SubtleButtonForegroundPointerOver" ResourceKey="TextFillColorPrimary" />
<StaticResource x:Key="SubtleButtonForegroundPressed" ResourceKey="TextFillColorSecondary" />
<StaticResource x:Key="SubtleButtonForegroundDisabled" ResourceKey="TextFillColorDisabled" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="SubtleButtonBackground" ResourceKey="SystemColorWindowColorBrush" />
<StaticResource x:Key="SubtleButtonBackgroundPointerOver" ResourceKey="SystemColorHighlightTextColorBrush" />
<StaticResource x:Key="SubtleButtonBackgroundPressed" ResourceKey="SystemColorWindowColorBrush" />
<StaticResource x:Key="SubtleButtonBackgroundDisabled" ResourceKey="SystemControlBackgroundBaseLowBrush" />
<StaticResource x:Key="SubtleButtonBorderBrush" ResourceKey="SystemColorWindowColorBrush" />
<StaticResource x:Key="SubtleButtonBorderBrushPointerOver" ResourceKey="SystemColorHighlightColorBrush" />
<StaticResource x:Key="SubtleButtonBorderBrushPressed" ResourceKey="SystemColorHighlightColorBrush" />
<StaticResource x:Key="SubtleButtonBorderBrushDisabled" ResourceKey="SystemColorGrayTextColor" />
<StaticResource x:Key="SubtleButtonForeground" ResourceKey="SystemColorButtonTextColorBrush" />
<StaticResource x:Key="SubtleButtonForegroundPointerOver" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="SubtleButtonForegroundPressed" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="SubtleButtonForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style x:Key="SubtleButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource SubtleButtonBackground}" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="Foreground" Value="{ThemeResource SubtleButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource SubtleButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}"
Foreground="{TemplateBinding Foreground}">
<ContentPresenter.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</ContentPresenter.BackgroundTransition>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<x:Double x:Key="SecondaryTextFontSize">12</x:Double>
<Style x:Key="SecondaryTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource SecondaryTextFontSize}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<DataTemplate x:Key="VariableTemplate" x:DataType="models:Variable">
<tkcontrols:SettingsCard
CommandParameter="{x:Bind (models:Variable)}"
IsActionIconVisible="False"
IsClickEnabled="False"
Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<tkcontrols:SettingsCard.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Name, Mode=TwoWay}" />
<FontIcon
Margin="6,0,6,0"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE930;"
Visibility="{x:Bind IsAppliedFromProfile, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="VariableIsAppliedByActiveProfileTooltip" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</FontIcon>
</StackPanel>
</tkcontrols:SettingsCard.Header>
<tkcontrols:SettingsCard.Description>
<StackPanel HorizontalAlignment="Left">
<ItemsControl
x:Name="VariableValuesList"
HorizontalAlignment="Left"
ItemsSource="{x:Bind ValuesList, Mode=TwoWay}"
Visibility="{x:Bind ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock
Margin="0,2,0,2"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Text}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Values, Mode=TwoWay}"
Visibility="{x:Bind ShowAsList, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
</StackPanel>
</tkcontrols:SettingsCard.Description>
<Button
Content="{ui:FontIcon Glyph=&#xE712;}"
IsEnabled="{x:Bind IsEditable}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="EditItem"
Click="EditVariable_Click"
CommandParameter="{x:Bind (models:Variable)}"
Icon="{ui:FontIcon Glyph=&#xE70F;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="RemoveItem"
Click="Delete_Variable_Click"
CommandParameter="{x:Bind (models:Variable)}"
Icon="{ui:FontIcon Glyph=&#xE74D;}" />
</MenuFlyout>
</Button.Flyout>
<ToolTipService.ToolTip>
<TextBlock x:Uid="More_Options_ButtonTooltip" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</Button>
</tkcontrols:SettingsCard>
</DataTemplate>
<converters:VariableTypeToGlyphConverter x:Key="VariableTypeToGlyphConverter" />
<converters:EnvironmentStateToBoolConverter x:Key="EnvironmentStateToBoolConverter" />
<converters:EnvironmentStateToMessageConverter x:Key="EnvironmentStateToMessageConverter" />
<converters:EnvironmentStateToTitleConverter x:Key="EnvironmentStateToTitleConverter" />
<converters:EnvironmentStateToVisibilityConverter x:Key="EnvironmentStateToVisibilityConverter" />
<tkconverters:BoolToVisibilityConverter
x:Key="BoolToInvertedVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<tkconverters:BoolNegationConverter x:Key="BoolNegationConverter" />
</ResourceDictionary>
</Page.Resources>
<Grid Margin="16" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- buttons -->
<RowDefinition Height="Auto" />
<!-- Warning messages -->
<RowDefinition Height="*" />
<!-- content -->
<RowDefinition Height="Auto" />
<!-- content -->
</Grid.RowDefinitions>
<!-- buttons -->
<StackPanel Orientation="Horizontal">
<Button Command="{x:Bind NewProfileCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
x:Name="Icon"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xe710;" />
<TextBlock x:Uid="NewProfileBtn" />
</StackPanel>
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="N" Modifiers="Control" />
</Button.KeyboardAccelerators>
</Button>
</StackPanel>
<Grid Grid.Row="1">
<InfoBar
x:Name="InvalidStateInfoBar"
x:Uid="InvalidStateInfoBar"
Title="{x:Bind ViewModel.EnvironmentState, Mode=OneWay, Converter={StaticResource EnvironmentStateToTitleConverter}}"
CloseButtonClick="InvalidStateInfoBar_CloseButtonClick"
IsOpen="{x:Bind ViewModel.EnvironmentState, Mode=OneWay, Converter={StaticResource EnvironmentStateToBoolConverter}}"
Message="{x:Bind ViewModel.EnvironmentState, Mode=OneWay, Converter={StaticResource EnvironmentStateToMessageConverter}}"
Severity="Warning"
Visibility="{x:Bind ViewModel.EnvironmentState, Mode=OneWay, Converter={StaticResource EnvironmentStateToVisibilityConverter}}">
<InfoBar.ActionButton>
<Button
Click="ReloadButton_Click"
Content="{ui:FontIcon Glyph=&#xe72c;}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind ViewModel.IsInfoBarButtonVisible, Mode=OneWay}" />
</InfoBar.ActionButton>
</InfoBar>
</Grid>
<Grid
Grid.Row="2"
Margin="0,24,0,0"
ColumnSpacing="12">
<Grid.ColumnDefinitions>
<!-- Left side -->
<ColumnDefinition Width="*" />
<!-- GridSplitter -->
<ColumnDefinition Width="Auto" />
<!-- Applied values -->
<ColumnDefinition Width="480" />
</Grid.ColumnDefinitions>
<ScrollViewer
Grid.Row="1"
Grid.Column="0"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock x:Uid="ProfilesLbl" Style="{StaticResource BodyStrongTextBlockStyle}" />
<FontIcon
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="ProfilesDescriptionLbl" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</FontIcon>
</StackPanel>
<ItemsControl Margin="0,4,0,0" ItemsSource="{x:Bind ViewModel.Profiles, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:ProfileVariablesSet">
<tkcontrols:SettingsExpander
Margin="0,0,0,4"
Header="{x:Bind Name, Mode=TwoWay}"
HeaderIcon="{ui:FontIcon Glyph=&#xEDE3;}">
<tkcontrols:SettingsExpander.ItemsHeader>
<ItemsRepeater
ItemTemplate="{StaticResource VariableTemplate}"
ItemsSource="{x:Bind Variables, Mode=OneWay}"
TabFocusNavigation="Local"
VerticalCacheLength="10" />
</tkcontrols:SettingsExpander.ItemsHeader>
<StackPanel Orientation="Horizontal" Spacing="12">
<ToggleSwitch
x:Uid="ToggleSwitch"
IsOn="{x:Bind IsEnabled, Mode=TwoWay}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}" />
<Button Content="{ui:FontIcon Glyph=&#xE712;}" Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="EditItem"
Click="EditProfileBtn_Click"
CommandParameter="{x:Bind (models:ProfileVariablesSet)}"
Icon="{ui:FontIcon Glyph=&#xE70F;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="RemoveItem"
Click="RemoveProfileBtn_Click"
CommandParameter="{x:Bind (models:ProfileVariablesSet)}"
Icon="{ui:FontIcon Glyph=&#xE74D;}" />
</MenuFlyout>
</Button.Flyout>
<ToolTipService.ToolTip>
<TextBlock x:Uid="More_Options_ButtonTooltip" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</tkcontrols:SettingsExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Margin="0,24,0,0"
Orientation="Horizontal"
Spacing="8">
<TextBlock x:Uid="DefaultVariablesLbl" Style="{StaticResource BodyStrongTextBlockStyle}" />
<FontIcon
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="DefaultVariablesDescriptionLbl" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</FontIcon>
</StackPanel>
<tkcontrols:SettingsExpander
Margin="0,4,0,0"
Header="{x:Bind ViewModel.UserDefaultSet.Name}"
HeaderIcon="{ui:FontIcon Glyph=&#xE77B;}">
<tkcontrols:SettingsExpander.ItemsHeader>
<ItemsRepeater
ItemTemplate="{StaticResource VariableTemplate}"
ItemsSource="{x:Bind ViewModel.UserDefaultSet.Variables, Mode=OneWay}"
TabFocusNavigation="Local"
VerticalCacheLength="10" />
</tkcontrols:SettingsExpander.ItemsHeader>
<StackPanel Orientation="Horizontal">
<Button
x:Name="AddDefaultVariableUserBtn"
x:Uid="AddVariableContent"
Click="AddDefaultVariableBtn_Click"
CommandParameter="{x:Bind ViewModel.UserDefaultSet}"
Content="Add variable"
Style="{StaticResource AccentButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="AddVariableTooltip" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander Header="{x:Bind ViewModel.SystemDefaultSet.Name}" HeaderIcon="{ui:FontIcon Glyph=&#xE977;}">
<tkcontrols:SettingsExpander.ItemsHeader>
<ItemsRepeater
ItemTemplate="{StaticResource VariableTemplate}"
ItemsSource="{x:Bind ViewModel.SystemDefaultSet.Variables, Mode=OneWay}"
TabFocusNavigation="Local"
VerticalCacheLength="10" />
</tkcontrols:SettingsExpander.ItemsHeader>
<tkcontrols:SettingsExpander.Description>
<StackPanel
Orientation="Horizontal"
Spacing="4"
Visibility="{x:Bind ViewModel.IsElevated, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<Border
Width="12"
Height="12"
AutomationProperties.AccessibilityView="Raw"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="12">
<FontIcon
FontSize="8"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Glyph="&#xEA1F;" />
</Border>
<TextBlock x:Uid="EditSystemDefaultSetInfoBar" />
</StackPanel>
</tkcontrols:SettingsExpander.Description>
<StackPanel Orientation="Horizontal">
<Button
x:Name="AddDefaultVariableSystemBtn"
x:Uid="AddVariableContent"
Click="AddDefaultVariableBtn_Click"
CommandParameter="{x:Bind ViewModel.SystemDefaultSet}"
IsEnabled="{x:Bind ViewModel.IsElevated}"
Style="{StaticResource AccentButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="AddVariableTooltip" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</tkcontrols:SettingsExpander>
</StackPanel>
</ScrollViewer>
<tkcontrols:GridSplitter
x:Name="Splitter"
Grid.Row="2"
Grid.Column="1"
Width="8"
Foreground="Transparent"
ResizeBehavior="BasedOnAlignment"
ResizeDirection="Auto" />
<Grid
x:Name="AppliedValuesPanel"
Grid.Row="2"
Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock x:Uid="AppliedVariablesLbl" Style="{StaticResource BodyStrongTextBlockStyle}" />
<FontIcon
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="AppliedVariablesDescriptionLbl" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
</FontIcon>
</StackPanel>
<Grid
Grid.Row="1"
Margin="0,8,0,0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}">
<ScrollViewer x:Name="AppliedVariablesScrollViewer" HorizontalScrollBarVisibility="Auto">
<ItemsControl Margin="12,8,12,0" ItemsSource="{x:Bind ViewModel.AppliedVariables, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:Variable">
<Grid Height="56" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
VerticalAlignment="Center"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="{x:Bind ParentType, Mode=OneWay, Converter={StaticResource VariableTypeToGlyphConverter}}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
Grid.Row="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Values}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind Values}" TextWrapping="WrapWholeWords" />
</ToolTipService.ToolTip>
</TextBlock>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
<ContentDialog
x:Name="AddDefaultVariableDialog"
x:Uid="AddDefaultVariableDialog"
IsPrimaryButtonEnabled="{Binding Valid, Mode=OneWay}"
IsSecondaryButtonEnabled="True"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<ContentDialog.DataContext>
<models:Variable />
</ContentDialog.DataContext>
<ScrollViewer>
<StackPanel
MinWidth="320"
HorizontalAlignment="Stretch"
Spacing="16">
<TextBox
x:Uid="AddNewVariableName"
IsSpellCheckEnabled="False"
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="AddDefaultVariableNameTxtBox_TextChanged" />
<TextBox
x:Uid="AddNewVariableValue"
AcceptsReturn="False"
IsSpellCheckEnabled="False"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
Text="{Binding Values, Mode=TwoWay}"
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
</ContentDialog>
<ContentDialog
x:Name="EditVariableDialog"
x:Uid="EditVariableDialog"
IsPrimaryButtonEnabled="{Binding Valid, Mode=OneWay}"
IsSecondaryButtonEnabled="True"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<ContentDialog.DataContext>
<models:Variable />
</ContentDialog.DataContext>
<ScrollViewer>
<StackPanel
MinWidth="320"
HorizontalAlignment="Stretch"
Spacing="16">
<TextBox
x:Name="EditVariableDialogNameTxtBox"
x:Uid="AddNewVariableName"
IsSpellCheckEnabled="False"
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="EditVariableDialogNameTxtBox_TextChanged" />
<TextBox
x:Name="EditVariableDialogValueTxtBox"
x:Uid="AddNewVariableValue"
AcceptsReturn="False"
IsSpellCheckEnabled="False"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
Text="{Binding Values, Mode=TwoWay}"
TextChanged="EditVariableDialogValueTxtBox_TextChanged"
TextWrapping="Wrap" />
<MenuFlyoutSeparator Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}" />
<ItemsControl
x:Name="EditVariableValuesList"
Margin="0,-8,0,12"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ValuesList, Mode=TwoWay}"
Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<TextBox
Background="Transparent"
BorderBrush="Transparent"
LostFocus="EditVariableValuesListTextBox_LostFocus"
Text="{Binding Text}" />
<Button
x:Uid="More_Options_Button"
Grid.Column="1"
VerticalAlignment="Center"
Content="&#xE712;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="MoveUp"
Click="ReorderButtonUp_Click"
Icon="{ui:FontIcon Glyph=&#xE74A;}" />
<MenuFlyoutItem
x:Uid="MoveDown"
Click="ReorderButtonDown_Click"
Icon="{ui:FontIcon Glyph=&#xE74B;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="RemoveListItem"
Click="RemoveListVariableButton_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="InsertListEntryBefore"
Click="InsertListEntryBeforeButton_Click"
Icon="{ui:FontIcon Glyph=&#xECC8;}" />
<MenuFlyoutItem
x:Uid="InsertListEntryAfter"
Click="InsertListEntryAfterButton_Click"
Icon="{ui:FontIcon Glyph=&#xECC8;}" />
</MenuFlyout>
</Button.Flyout>
<ToolTipService.ToolTip>
<TextBlock x:Uid="More_Options_ButtonTooltip" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</ContentDialog>
<ContentDialog
x:Name="AddProfileDialog"
x:Uid="AddProfileDialog"
IsPrimaryButtonEnabled="{Binding Valid, Mode=OneWay}"
IsSecondaryButtonEnabled="True"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<ContentDialog.DataContext>
<models:ProfileVariablesSet />
</ContentDialog.DataContext>
<ScrollViewer>
<StackPanel
MinWidth="360"
HorizontalAlignment="Stretch"
Spacing="12">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Uid="NewProfileNameTxtBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsSpellCheckEnabled="False"
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ToggleSwitch
x:Uid="NewProfileEnabled"
Grid.Column="1"
MinWidth="0"
Margin="0,0,0,0"
VerticalAlignment="Center"
IsOn="{Binding IsEnabled, Mode=TwoWay}"
OffContent=""
OnContent="" />
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock x:Uid="NewProfileVariablesListViewHeader" Margin="0,12,0,8" />
<TextBlock
x:Uid="NewProfileVariablesListViewApplyToSystemHeader"
Grid.Column="1"
Margin="0,12,0,8"
HorizontalAlignment="Right"
Visibility="Collapsed" />
<ItemsControl
x:Name="NewProfileVariablesListView"
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,-8,0,12"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Variables, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:Variable">
<Grid Height="48" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
<TextBlock
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Values}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
<ToggleSwitch
x:Uid="ApplyAsSystemBtn"
Grid.Column="1"
IsOn="{x:Bind Path=ApplyToSystem, Mode=TwoWay}"
Visibility="Collapsed" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Button>
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
x:Name="AddVariableIcon"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xe710;" />
<TextBlock x:Uid="AddVariableBtn" />
</StackPanel>
<Button.Flyout>
<Flyout
x:Name="AddVariableFlyout"
Closed="AddVariableFlyout_Closed"
Placement="Right"
ShouldConstrainToRootBounds="False">
<Grid Width="320" Height="480">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<tkcontrols:Segmented
x:Name="SwitchViewsSegmentedView"
MaxWidth="500"
HorizontalAlignment="Stretch"
SelectionMode="Single">
<tkcontrols:SegmentedItem
x:Name="AddNewVariableSegmentedItem"
x:Uid="NewVariableSegmentedButton"
Tag="NewVariable" />
<tkcontrols:SegmentedItem
x:Name="AddExistingVariableSegmentedItem"
x:Uid="ExistingVariableSegmentedButton"
Tag="ExistingVariable" />
</tkcontrols:Segmented>
<tkcontrols:SwitchPresenter
x:Name="AddVariableSwitchPresenter"
Grid.Row="1"
Value="{Binding SelectedItem.Tag, ElementName=SwitchViewsSegmentedView}">
<tkcontrols:Case Value="NewVariable">
<StackPanel Grid.Row="1" Orientation="Vertical">
<!-- Adding new variable -->
<TextBox
x:Name="AddNewVariableName"
x:Uid="AddNewVariableName"
Margin="0,16,0,0"
TextChanged="AddNewVariableName_TextChanged" />
<TextBox
x:Name="AddNewVariableValue"
x:Uid="AddNewVariableValue"
Margin="0,16,0,0" />
</StackPanel>
</tkcontrols:Case>
<tkcontrols:Case Value="ExistingVariable">
<!-- Adding existing variables -->
<ListView
x:Name="ExistingVariablesListView"
Grid.Row="1"
Margin="-12,12,0,12"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.DefaultVariables.Variables, Mode=OneWay}"
Loaded="ExistingVariablesListView_Loaded"
SelectionChanged="ExistingVariablesListView_SelectionChanged"
SelectionMode="Multiple">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Variable">
<Grid Height="48" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
VerticalAlignment="Center"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="{x:Bind ParentType, Mode=OneWay, Converter={StaticResource VariableTypeToGlyphConverter}}" />
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Vertical">
<TextBlock
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Values}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</tkcontrols:Case>
</tkcontrols:SwitchPresenter>
<Grid Grid.Row="2" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
x:Name="ConfirmAddVariableBtn"
x:Uid="ConfirmAddVariableBtn"
Margin="4,0,4,0"
HorizontalAlignment="Stretch"
Command="{x:Bind AddVariableCommand}"
IsEnabled="False"
Style="{StaticResource AccentButtonStyle}" />
<Button
x:Name="CancelAddVariableBtn"
x:Uid="CancelAddVariableBtn"
Grid.Column="1"
Margin="4,0,4,0"
HorizontalAlignment="Stretch"
Command="{x:Bind CancelAddVariableCommand}" />
</Grid>
</Grid>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</ScrollViewer>
</ContentDialog>
</Grid>
</Page>

View File

@@ -0,0 +1,551 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using EnvironmentVariablesUILib.Models;
using EnvironmentVariablesUILib.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace EnvironmentVariablesUILib
{
public sealed partial class EnvironmentVariablesMainPage : Page
{
private sealed class RelayCommandParameter
{
public RelayCommandParameter(Variable variable, VariablesSet set)
{
Variable = variable;
this.Set = set;
}
public Variable Variable { get; set; }
public VariablesSet Set { get; set; }
}
public MainViewModel ViewModel { get; private set; }
public ICommand EditCommand => new RelayCommand<RelayCommandParameter>(EditVariable);
public ICommand NewProfileCommand => new AsyncRelayCommand(AddProfileAsync);
public ICommand AddProfileCommand => new RelayCommand(AddProfile);
public ICommand UpdateProfileCommand => new RelayCommand(UpdateProfile);
public ICommand AddVariableCommand => new RelayCommand(AddVariable);
public ICommand CancelAddVariableCommand => new RelayCommand(CancelAddVariable);
public ICommand AddDefaultVariableCommand => new RelayCommand<DefaultVariablesSet>(AddDefaultVariable);
public EnvironmentVariablesMainPage(MainViewModel viewModel)
{
this.InitializeComponent();
ViewModel = viewModel;
DataContext = ViewModel;
ViewModel.LoadEnvironmentVariables();
}
private async Task ShowEditDialogAsync(Variable variable, VariablesSet parentSet)
{
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
EditVariableDialog.Title = resourceLoader.GetString("EditVariableDialog_Title");
EditVariableDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn");
EditVariableDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
EditVariableDialog.PrimaryButtonCommand = EditCommand;
EditVariableDialog.PrimaryButtonCommandParameter = new RelayCommandParameter(variable, parentSet);
var clone = variable.Clone();
EditVariableDialog.DataContext = clone;
await EditVariableDialog.ShowAsync();
}
private async void EditVariable_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var btn = sender as MenuFlyoutItem;
var variablesSet = btn.DataContext as VariablesSet;
var variable = btn.CommandParameter as Variable;
if (variable != null)
{
await ShowEditDialogAsync(variable, variablesSet);
}
}
private void EditVariable(RelayCommandParameter param)
{
var variableSet = param.Set as ProfileVariablesSet;
var original = param.Variable;
var edited = EditVariableDialog.DataContext as Variable;
ViewModel.EditVariable(original, edited, variableSet);
}
private async Task AddProfileAsync()
{
SwitchViewsSegmentedView.SelectedIndex = 0;
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
AddProfileDialog.Title = resourceLoader.GetString("AddNewProfileDialog_Title");
AddProfileDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
AddProfileDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
AddProfileDialog.PrimaryButtonCommand = AddProfileCommand;
AddProfileDialog.DataContext = new ProfileVariablesSet(Guid.NewGuid(), string.Empty);
await AddProfileDialog.ShowAsync();
}
private void AddProfile()
{
var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
ViewModel.AddProfile(profile);
}
private void UpdateProfile()
{
var updatedProfile = AddProfileDialog.DataContext as ProfileVariablesSet;
ViewModel.UpdateProfile(updatedProfile);
}
private async void RemoveProfileBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var button = sender as MenuFlyoutItem;
var profile = button.CommandParameter as ProfileVariablesSet;
if (profile != null)
{
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
ContentDialog dialog = new ContentDialog();
dialog.XamlRoot = RootPage.XamlRoot;
dialog.Title = profile.Name;
dialog.PrimaryButtonText = resourceLoader.GetString("Yes");
dialog.CloseButtonText = resourceLoader.GetString("No");
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description"), TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords };
dialog.PrimaryButtonClick += (s, args) =>
{
ViewModel.RemoveProfile(profile);
};
var result = await dialog.ShowAsync();
}
}
private void AddVariable()
{
var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
if (profile != null)
{
if (AddVariableSwitchPresenter.Value as string == "NewVariable")
{
profile.Variables.Add(new Variable(AddNewVariableName.Text, AddNewVariableValue.Text, VariablesSetType.Profile));
}
else
{
foreach (Variable variable in ExistingVariablesListView.SelectedItems)
{
if (!profile.Variables.Where(x => x.Name == variable.Name).Any())
{
var clone = variable.Clone(true);
profile.Variables.Add(clone);
}
}
}
}
AddNewVariableName.Text = string.Empty;
AddNewVariableValue.Text = string.Empty;
ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
ExistingVariablesListView.SelectedItems.Clear();
ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
AddVariableFlyout.Hide();
}
private void CancelAddVariable()
{
AddNewVariableName.Text = string.Empty;
AddNewVariableValue.Text = string.Empty;
ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
ExistingVariablesListView.SelectedItems.Clear();
ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
AddVariableFlyout.Hide();
}
private void AddDefaultVariable(DefaultVariablesSet set)
{
var variable = AddDefaultVariableDialog.DataContext as Variable;
var type = set.Type;
ViewModel.AddDefaultVariable(variable, type);
}
private async void Delete_Variable_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
MenuFlyoutItem selectedItem = sender as MenuFlyoutItem;
var variableSet = selectedItem.DataContext as ProfileVariablesSet;
var variable = selectedItem.CommandParameter as Variable;
if (variable != null)
{
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
ContentDialog dialog = new ContentDialog();
dialog.XamlRoot = RootPage.XamlRoot;
dialog.Title = variable.Name;
dialog.PrimaryButtonText = resourceLoader.GetString("Yes");
dialog.CloseButtonText = resourceLoader.GetString("No");
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Variable_Description"), TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords };
dialog.PrimaryButtonClick += (s, args) =>
{
ViewModel.DeleteVariable(variable, variableSet);
};
var result = await dialog.ShowAsync();
}
}
private void AddNewVariableName_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox nameTxtBox = sender as TextBox;
var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
if (nameTxtBox != null)
{
if (nameTxtBox.Text.Length == 0 || nameTxtBox.Text.Length >= 255 || profile.Variables.Where(x => x.Name.Equals(nameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any())
{
ConfirmAddVariableBtn.IsEnabled = false;
}
else
{
ConfirmAddVariableBtn.IsEnabled = true;
}
}
}
private void ReloadButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ViewModel.LoadEnvironmentVariables();
ViewModel.EnvironmentState = EnvironmentState.Unchanged;
}
private void ExistingVariablesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
int toRemove = -1;
if (e.AddedItems.Count > 0)
{
var list = sender as ListView;
var duplicates = list.SelectedItems.GroupBy(x => ((Variable)x).Name.ToLowerInvariant()).Where(g => g.Count() > 1).ToList();
foreach (var dup in duplicates)
{
ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
list.SelectedItems.Remove(dup.ElementAt(1));
ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
}
}
if (e.RemovedItems.Count > 0)
{
Variable removedVariable = e.RemovedItems[0] as Variable;
for (int i = 0; i < profile.Variables.Count; i++)
{
if (profile.Variables[i].Name == removedVariable.Name && profile.Variables[i].Values == removedVariable.Values)
{
toRemove = i;
break;
}
}
if (toRemove != -1)
{
profile.Variables.RemoveAt(toRemove);
}
}
ConfirmAddVariableBtn.IsEnabled = false;
foreach (Variable variable in ExistingVariablesListView.SelectedItems)
{
if (variable != null)
{
if (!profile.Variables.Where(x => x.Name.Equals(variable.Name, StringComparison.Ordinal) && x.Values.Equals(variable.Values, StringComparison.Ordinal)).Any())
{
ConfirmAddVariableBtn.IsEnabled = true;
break;
}
}
}
}
private async void EditProfileBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
SwitchViewsSegmentedView.SelectedIndex = 0;
var button = sender as MenuFlyoutItem;
var profile = button.CommandParameter as ProfileVariablesSet;
if (profile != null)
{
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
AddProfileDialog.Title = resourceLoader.GetString("EditProfileDialog_Title");
AddProfileDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn");
AddProfileDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
AddProfileDialog.PrimaryButtonCommand = UpdateProfileCommand;
AddProfileDialog.DataContext = profile.Clone();
await AddProfileDialog.ShowAsync();
}
}
private void ExistingVariablesListView_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
foreach (Variable item in ExistingVariablesListView.Items)
{
if (item != null)
{
foreach (var profileItem in profile.Variables)
{
if (profileItem.Name == item.Name && profileItem.Values == item.Values)
{
if (ExistingVariablesListView.SelectedItems.Where(x => ((Variable)x).Name.Equals(profileItem.Name, StringComparison.OrdinalIgnoreCase)).Any())
{
continue;
}
ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
ExistingVariablesListView.SelectedItems.Add(item);
ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
}
}
}
}
}
private async Task ShowAddDefaultVariableDialogAsync(DefaultVariablesSet set)
{
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
AddDefaultVariableDialog.Title = resourceLoader.GetString("AddVariable_Title");
AddDefaultVariableDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn");
AddDefaultVariableDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
AddDefaultVariableDialog.PrimaryButtonCommand = AddDefaultVariableCommand;
AddDefaultVariableDialog.PrimaryButtonCommandParameter = set;
var variableType = set.Id == VariablesSet.SystemGuid ? VariablesSetType.System : VariablesSetType.User;
AddDefaultVariableDialog.DataContext = new Variable(string.Empty, string.Empty, variableType);
await AddDefaultVariableDialog.ShowAsync();
}
private async void AddDefaultVariableBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var button = sender as Button;
var defaultVariableSet = button.CommandParameter as DefaultVariablesSet;
if (defaultVariableSet != null)
{
await ShowAddDefaultVariableDialogAsync(defaultVariableSet);
}
}
private void EditVariableDialogNameTxtBox_TextChanged(object sender, TextChangedEventArgs e)
{
var variable = EditVariableDialog.DataContext as Variable;
var param = EditVariableDialog.PrimaryButtonCommandParameter as RelayCommandParameter;
var variableSet = param.Set;
if (variableSet == null)
{
// default set
variableSet = variable.ParentType == VariablesSetType.User ? ViewModel.UserDefaultSet : ViewModel.SystemDefaultSet;
}
if (variableSet != null)
{
if (variableSet.Variables.Where(x => x.Name.Equals(EditVariableDialogNameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any() || !variable.Valid)
{
EditVariableDialog.IsPrimaryButtonEnabled = false;
}
else
{
EditVariableDialog.IsPrimaryButtonEnabled = true;
}
}
if (!variable.Validate())
{
EditVariableDialog.IsPrimaryButtonEnabled = false;
}
}
private void AddDefaultVariableNameTxtBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox nameTxtBox = sender as TextBox;
var variable = AddDefaultVariableDialog.DataContext as Variable;
var defaultSet = variable.ParentType == VariablesSetType.User ? ViewModel.UserDefaultSet : ViewModel.SystemDefaultSet;
if (nameTxtBox != null)
{
if (nameTxtBox.Text.Length == 0 || defaultSet.Variables.Where(x => x.Name.Equals(nameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any())
{
AddDefaultVariableDialog.IsPrimaryButtonEnabled = false;
}
else
{
AddDefaultVariableDialog.IsPrimaryButtonEnabled = true;
}
}
if (!variable.Validate())
{
AddDefaultVariableDialog.IsPrimaryButtonEnabled = false;
}
}
private void EditVariableDialogValueTxtBox_TextChanged(object sender, TextChangedEventArgs e)
{
var txtBox = sender as TextBox;
var variable = EditVariableDialog.DataContext as Variable;
EditVariableDialog.IsPrimaryButtonEnabled = true;
variable.ValuesList = Variable.ValuesStringToValuesListItemCollection(txtBox.Text);
}
private void ReorderButtonUp_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem;
if (listItem == null)
{
return;
}
var variable = EditVariableDialog.DataContext as Variable;
var index = variable.ValuesList.IndexOf(listItem);
if (index > 0)
{
variable.ValuesList.Move(index, index - 1);
}
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
private void ReorderButtonDown_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem;
if (listItem == null)
{
return;
}
var variable = EditVariableDialog.DataContext as Variable;
var btn = EditVariableDialog.PrimaryButtonCommandParameter as Button;
var index = variable.ValuesList.IndexOf(listItem);
if (index < variable.ValuesList.Count - 1)
{
variable.ValuesList.Move(index, index + 1);
}
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
private void RemoveListVariableButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem;
if (listItem == null)
{
return;
}
var variable = EditVariableDialog.DataContext as Variable;
variable.ValuesList.Remove(listItem);
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
private void InsertListEntryBeforeButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listItem = (sender as MenuFlyoutItem)?.DataContext as Variable.ValuesListItem;
if (listItem == null)
{
return;
}
var variable = EditVariableDialog.DataContext as Variable;
var index = variable.ValuesList.IndexOf(listItem);
variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
}
private void InsertListEntryAfterButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listItem = (sender as MenuFlyoutItem)?.DataContext as Variable.ValuesListItem;
if (listItem == null)
{
return;
}
var variable = EditVariableDialog.DataContext as Variable;
var index = variable.ValuesList.IndexOf(listItem);
variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
}
private void EditVariableValuesListTextBox_LostFocus(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listItem = (sender as TextBox)?.DataContext as Variable.ValuesListItem;
if (listItem == null)
{
return;
}
if (listItem.Text == (sender as TextBox)?.Text)
{
return;
}
listItem.Text = (sender as TextBox)?.Text;
var variable = EditVariableDialog.DataContext as Variable;
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
}
private void InvalidStateInfoBar_CloseButtonClick(InfoBar sender, object args)
{
ViewModel.EnvironmentState = EnvironmentState.Unchanged;
}
private void AddVariableFlyout_Closed(object sender, object e)
{
CancelAddVariable();
ConfirmAddVariableBtn.IsEnabled = false;
}
}
}

View File

@@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Condition="exists('..\..\..\Version.props')" Project="..\..\..\Version.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0-windows10.0.20348</TargetFramework>
<RootNamespace>EnvironmentVariablesUILib</RootNamespace>
<UseWinUI>true</UseWinUI>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.EnvironmentVariablesUILib</AssemblyName>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.EnvironmentVariablesUILib.pri</ProjectPriFileName>
<GenerateLibraryLayout>true</GenerateLibraryLayout>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutDir)\PowerToys.EnvironmentVariablesUILib.pri" Pack="True" PackageCopyToOutput="true" PackagePath="lib/$(TargetFramework)" />
<None Include="$(OutDir)\PowerToys.EnvironmentVariablesUILib.pri" Pack="True" PackageCopyToOutput="True" PackagePath="contentFiles\any\$(TargetFramework)" />
<XBFFile Include="$(OutDir)**\*.xbf" />
<None Include="@(XBFFile)" Pack="True" PackageCopyToOutput="True" PackagePath="contentFiles\any\$(TargetFramework)" />
<None Include="$(OutDir)\PowerToys.EnvironmentVariablesUILib.pdb" Pack="True" PackageCopyToOutput="true" PackagePath="lib/$(TargetFramework)" />
<None Include="Assets\**\*.png" Pack="true" PackageCopyToOutput="true" PackagePath="contentFiles\any\$(TargetFramework)\Assets"/>
<None Include="Assets\**\*.ico" Pack="true" PackageCopyToOutput="true" PackagePath="contentFiles\any\$(TargetFramework)\Assets"/>
</ItemGroup>
<ItemGroup>
<Content Remove="Assets\**\*.png" Pack="false" />
<Content Remove="Assets\**\*.ico" Pack="false" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DebugSymbols>true</DebugSymbols>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WinUIEx" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Security.Principal;
namespace EnvironmentVariablesUILib.Helpers
{
public class ElevationHelper : IElevationHelper
{
private readonly bool _isElevated;
public bool IsElevated => _isElevated;
public ElevationHelper()
{
_isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
}
public static IElevationHelper ElevationHelperInstance { get; set; }
}
}

View File

@@ -0,0 +1,208 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using EnvironmentVariablesUILib.Helpers.Win32;
using EnvironmentVariablesUILib.Models;
using Microsoft.Win32;
namespace EnvironmentVariablesUILib.Helpers
{
internal sealed class EnvironmentVariablesHelper
{
internal static string GetBackupVariableName(Variable variable, string profileName)
{
return variable.Name + "_PowerToys_" + profileName;
}
internal static Variable GetExisting(string variableName)
{
DefaultVariablesSet userSet = new DefaultVariablesSet(Guid.NewGuid(), "tmpUser", VariablesSetType.User);
GetVariables(EnvironmentVariableTarget.User, userSet);
foreach (var variable in userSet.Variables)
{
if (variable.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase))
{
return new Variable(variable.Name, variable.Values, VariablesSetType.User);
}
}
DefaultVariablesSet systemSet = new DefaultVariablesSet(Guid.NewGuid(), "tmpSystem", VariablesSetType.System);
GetVariables(EnvironmentVariableTarget.Machine, systemSet);
foreach (var variable in systemSet.Variables)
{
if (variable.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase))
{
return new Variable(variable.Name, variable.Values, VariablesSetType.System);
}
}
return null;
}
private static RegistryKey OpenEnvironmentKeyIfExists(bool fromMachine, bool writable)
{
RegistryKey baseKey;
string keyName;
if (fromMachine)
{
baseKey = Registry.LocalMachine;
keyName = @"System\CurrentControlSet\Control\Session Manager\Environment";
}
else
{
baseKey = Registry.CurrentUser;
keyName = "Environment";
}
return baseKey.OpenSubKey(keyName, writable: writable);
}
// Code taken from https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.Win32.cs
// Set variables directly to registry instead of using Environment API - Environment.SetEnvironmentVariable() has 1 second timeout for SendNotifyMessage(WM_SETTINGSCHANGED).
// When applying profile, this would take num_of_variables * 1s to propagate the changes. We do manually SendNotifyMessage with no timeout where needed.
private static void SetEnvironmentVariableFromRegistryWithoutNotify(string variable, string value, bool fromMachine)
{
const int MaxUserEnvVariableLength = 255; // User-wide env vars stored in the registry have names limited to 255 chars
if (!fromMachine && variable.Length >= MaxUserEnvVariableLength)
{
LoggerInstance.Logger.LogError("Can't apply variable - name too long.");
return;
}
using (RegistryKey environmentKey = OpenEnvironmentKeyIfExists(fromMachine, writable: true))
{
if (environmentKey != null)
{
if (value == null)
{
environmentKey.DeleteValue(variable, throwOnMissingValue: false);
}
else
{
// If a variable contains %, we save it as a REG_EXPAND_SZ, which is the same behavior as the Windows default environment variables editor.
if (value.Contains('%'))
{
environmentKey.SetValue(variable, value, RegistryValueKind.ExpandString);
}
else
{
environmentKey.SetValue(variable, value, RegistryValueKind.String);
}
}
}
}
}
internal static void NotifyEnvironmentChange()
{
unsafe
{
// send a WM_SETTINGCHANGE message to all windows
fixed (char* lParam = "Environment")
{
_ = NativeMethods.SendNotifyMessage(new IntPtr(NativeMethods.HWND_BROADCAST), NativeMethods.WindowMessage.WM_SETTINGSCHANGED, (IntPtr)0x12345, (IntPtr)lParam);
}
}
}
// Code taken from https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.Win32.cs
// Reading variables from registry instead of using Environment API, because Environment API expands variables by default.
internal static void GetVariables(EnvironmentVariableTarget target, VariablesSet set)
{
var sortedList = new SortedList<string, Variable>();
bool fromMachine = target == EnvironmentVariableTarget.Machine ? true : false;
using (RegistryKey environmentKey = OpenEnvironmentKeyIfExists(fromMachine, writable: false))
{
if (environmentKey != null)
{
foreach (string name in environmentKey.GetValueNames())
{
string value = environmentKey.GetValue(name, string.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames).ToString();
try
{
Variable entry = new Variable(name, value, set.Type);
sortedList.Add(name, entry);
}
catch (ArgumentException)
{
// Throw and catch intentionally to provide non-fatal notification about corrupted environment block
}
}
}
}
set.Variables = new System.Collections.ObjectModel.ObservableCollection<Variable>(sortedList.Values);
}
internal static bool SetVariableWithoutNotify(Variable variable)
{
bool fromMachine = variable.ParentType switch
{
VariablesSetType.Profile => false,
VariablesSetType.User => false,
VariablesSetType.System => true,
_ => throw new NotImplementedException(),
};
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine);
return true;
}
internal static bool SetVariable(Variable variable)
{
bool fromMachine = variable.ParentType switch
{
VariablesSetType.Profile => false,
VariablesSetType.User => false,
VariablesSetType.System => true,
_ => throw new NotImplementedException(),
};
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine);
NotifyEnvironmentChange();
return true;
}
internal static bool UnsetVariableWithoutNotify(Variable variable)
{
bool fromMachine = variable.ParentType switch
{
VariablesSetType.Profile => false,
VariablesSetType.User => false,
VariablesSetType.System => true,
_ => throw new NotImplementedException(),
};
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine);
return true;
}
internal static bool UnsetVariable(Variable variable)
{
bool fromMachine = variable.ParentType switch
{
VariablesSetType.Profile => false,
VariablesSetType.User => false,
VariablesSetType.System => true,
_ => throw new NotImplementedException(),
};
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine);
NotifyEnvironmentChange();
return true;
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Text.Json;
using System.Threading.Tasks;
using EnvironmentVariablesUILib.Models;
namespace EnvironmentVariablesUILib.Helpers
{
public sealed class EnvironmentVariablesService : IEnvironmentVariablesService
{
private const string ProfilesJsonFileSubPath = "Microsoft\\PowerToys\\EnvironmentVariables\\";
private readonly string _profilesJsonFilePath;
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
};
public string ProfilesJsonFilePath => _profilesJsonFilePath;
public EnvironmentVariablesService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
_profilesJsonFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProfilesJsonFileSubPath, "profiles.json");
}
public void Dispose()
{
}
public List<ProfileVariablesSet> ReadProfiles()
{
if (!_fileSystem.File.Exists(ProfilesJsonFilePath))
{
return new List<ProfileVariablesSet>();
}
var fileContent = _fileSystem.File.ReadAllText(ProfilesJsonFilePath);
var profiles = JsonSerializer.Deserialize<List<ProfileVariablesSet>>(fileContent);
return profiles;
}
public async Task WriteAsync(IEnumerable<ProfileVariablesSet> profiles)
{
string jsonData = JsonSerializer.Serialize(profiles, _serializerOptions);
await _fileSystem.File.WriteAllTextAsync(ProfilesJsonFilePath, jsonData);
}
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace EnvironmentVariablesUILib.Helpers
{
public interface IElevationHelper
{
bool IsElevated { get; }
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using EnvironmentVariablesUILib.Models;
namespace EnvironmentVariablesUILib.Helpers
{
public interface IEnvironmentVariablesService : IDisposable
{
string ProfilesJsonFilePath { get; }
List<ProfileVariablesSet> ReadProfiles();
Task WriteAsync(IEnumerable<ProfileVariablesSet> profiles);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace EnvironmentVariablesUILib.Helpers
{
public interface ILogger
{
public void LogError(string message);
public void LogError(string message, Exception ex);
public void LogWarning(string message);
public void LogInfo(string message);
public void LogDebug(string message);
public void LogTrace();
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace EnvironmentVariablesUILib.Helpers
{
public static class LoggerInstance
{
public static ILogger Logger { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace EnvironmentVariablesUILib.Helpers.Win32
{
public static class NativeMethods
{
internal const int HWND_BROADCAST = 0xffff;
internal delegate IntPtr WinProc(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern int SendNotifyMessage(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
[DllImport("User32.dll")]
internal static extern int GetDpiForWindow(IntPtr hwnd);
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
internal static extern int SetWindowLong32(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
internal static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc);
[DllImport("user32.dll")]
internal static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
[Flags]
internal enum WindowLongIndexFlags : int
{
GWL_WNDPROC = -4,
}
internal enum WindowMessage : int
{
WM_SETTINGSCHANGED = 0x001A,
}
internal static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc)
{
if (IntPtr.Size == 8)
{
return SetWindowLongPtr64(hWnd, nIndex, newProc);
}
else
{
return new IntPtr(SetWindowLong32(hWnd, nIndex, newProc));
}
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Windows.ApplicationModel.Resources;
namespace EnvironmentVariablesUILib.Helpers
{
public static class ResourceLoaderInstance
{
public static ResourceLoader ResourceLoader { get; private set; }
static ResourceLoaderInstance()
{
ResourceLoader = new ResourceLoader("PowerToys.EnvironmentVariablesUILib.pri", "PowerToys.EnvironmentVariablesUILib/Resources");
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace EnvironmentVariablesUILib.Models
{
public class DefaultVariablesSet : VariablesSet
{
public DefaultVariablesSet(Guid id, string name, VariablesSetType type)
: base(id, name, type)
{
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace EnvironmentVariablesUILib.Models
{
public enum EnvironmentState
{
Unchanged = 0,
ChangedOnStartup,
EnvironmentMessageReceived,
ProfileNotApplicable,
}
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using EnvironmentVariablesUILib.Helpers;
namespace EnvironmentVariablesUILib.Models
{
public partial class ProfileVariablesSet : VariablesSet
{
[ObservableProperty]
private bool _isEnabled;
public ProfileVariablesSet()
: base()
{
Type = VariablesSetType.Profile;
IconPath = ProfileIconPath;
}
public ProfileVariablesSet(Guid id, string name)
: base(id, name, VariablesSetType.Profile)
{
IconPath = ProfileIconPath;
}
public Task Apply()
{
return Task.Run(() =>
{
foreach (var variable in Variables)
{
var applyToSystem = variable.ApplyToSystem;
// Get existing variable with the same name if it exist
var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name);
// It exists. Rename it to preserve it.
if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User)
{
variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name);
// Backup the variable
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
{
LoggerInstance.Logger.LogError("Failed to set backup variable.");
}
}
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variable))
{
LoggerInstance.Logger.LogError("Failed to set profile variable.");
}
}
EnvironmentVariablesHelper.NotifyEnvironmentChange();
});
}
public Task UnApply()
{
return Task.Run(() =>
{
foreach (var variable in Variables)
{
UnapplyVariable(variable);
}
EnvironmentVariablesHelper.NotifyEnvironmentChange();
});
}
public void UnapplyVariable(Variable variable)
{
// Unset the variable
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(variable))
{
LoggerInstance.Logger.LogError("Failed to unset variable.");
}
var originalName = variable.Name;
var backupName = EnvironmentVariablesHelper.GetBackupVariableName(variable, this.Name);
// Get backup variable if it exist
var backupVariable = EnvironmentVariablesHelper.GetExisting(backupName);
if (backupVariable != null)
{
var variableToRestore = new Variable(originalName, backupVariable.Values, backupVariable.ParentType);
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
{
LoggerInstance.Logger.LogError("Failed to unset backup variable.");
}
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
{
LoggerInstance.Logger.LogError("Failed to restore backup variable.");
}
}
}
public bool IsCorrectlyApplied()
{
if (!IsEnabled)
{
return false;
}
foreach (var variable in Variables)
{
var applied = EnvironmentVariablesHelper.GetExisting(variable.Name);
if (applied != null && applied.Values == variable.Values && applied.ParentType == VariablesSetType.User)
{
continue;
}
return false;
}
return true;
}
public bool IsApplicable()
{
foreach (var variable in Variables)
{
if (!variable.Validate())
{
return false;
}
// Get existing variable with the same name if it exist
var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name);
// It exists. Backup is needed.
if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User)
{
variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name);
if (!variableToOverride.Validate())
{
return false;
}
}
}
return true;
}
public ProfileVariablesSet Clone()
{
var clone = new ProfileVariablesSet(this.Id, this.Name);
clone.Variables = new ObservableCollection<Variable>(this.Variables);
clone.IsEnabled = this.IsEnabled;
return clone;
}
}
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using EnvironmentVariablesUILib.Helpers;
namespace EnvironmentVariablesUILib.Models
{
public partial class Variable : ObservableObject, IJsonOnDeserialized
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Valid))]
[NotifyPropertyChangedFor(nameof(ShowAsList))]
private string _name;
[ObservableProperty]
private string _values;
[ObservableProperty]
private bool _applyToSystem;
[JsonIgnore]
[property: JsonIgnore]
[ObservableProperty]
private bool _isAppliedFromProfile; // Used to mark that a variable in a default set is applied by a profile. Used to disable editing / mark it in the UI.
[JsonIgnore]
public bool IsEditable
{
get
{
return (ParentType != VariablesSetType.System || ElevationHelper.ElevationHelperInstance.IsElevated) && !IsAppliedFromProfile;
}
}
[JsonIgnore]
public VariablesSetType ParentType { get; set; }
// To store the strings in the Values List with actual objects that can be referenced and identity compared
public class ValuesListItem
{
public string Text { get; set; }
}
[ObservableProperty]
[property: JsonIgnore]
[JsonIgnore]
private ObservableCollection<ValuesListItem> _valuesList;
[JsonIgnore]
public bool Valid => Validate();
[JsonIgnore]
public bool ShowAsList => IsList();
private bool IsList()
{
List<string> listVariables = new() { "PATH", "PATHEXT", "PSMODULEPATH" };
foreach (var name in listVariables)
{
if (Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public void OnDeserialized()
{
// No need to save ValuesList to the Json, so we are generating it after deserializing
ValuesList = ValuesStringToValuesListItemCollection(Values);
}
public Variable()
{
}
public Variable(string name, string values, VariablesSetType parentType)
{
Name = name;
Values = values;
ParentType = parentType;
ValuesList = ValuesStringToValuesListItemCollection(Values);
}
internal static ObservableCollection<ValuesListItem> ValuesStringToValuesListItemCollection(string values)
{
return new ObservableCollection<ValuesListItem>(values.Split(';').Select(x => new ValuesListItem { Text = x }));
}
internal Task Update(Variable edited, bool propagateChange, ProfileVariablesSet parentProfile)
{
bool nameChanged = Name != edited.Name;
var clone = this.Clone();
// Update state
Name = edited.Name;
Values = edited.Values;
ValuesList = ValuesStringToValuesListItemCollection(Values);
return Task.Run(() =>
{
// Apply changes
if (propagateChange)
{
if (nameChanged)
{
if (!EnvironmentVariablesHelper.UnsetVariable(clone))
{
LoggerInstance.Logger.LogError("Failed to unset original variable.");
}
if (parentProfile != null)
{
var backupName = EnvironmentVariablesHelper.GetBackupVariableName(clone, parentProfile.Name);
// Get backup variable if it exist
var backupVariable = EnvironmentVariablesHelper.GetExisting(backupName);
if (backupVariable != null)
{
var variableToRestore = new Variable(clone.Name, backupVariable.Values, backupVariable.ParentType);
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
{
LoggerInstance.Logger.LogError("Failed to unset backup variable.");
}
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
{
LoggerInstance.Logger.LogError("Failed to restore backup variable.");
}
}
}
}
// Get existing variable with the same name if it exist
var variableToOverride = EnvironmentVariablesHelper.GetExisting(Name);
// It exists. Rename it to preserve it.
if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User && parentProfile != null)
{
// Gets which name the backup variable should have.
variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, parentProfile.Name);
// Only create a backup variable if there's not one already, to avoid overriding. (solves Path nuking errors, for example, after editing path on an enabled profile)
if (EnvironmentVariablesHelper.GetExisting(variableToOverride.Name) == null)
{
// Backup the variable
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
{
LoggerInstance.Logger.LogError("Failed to set backup variable.");
}
}
}
if (!EnvironmentVariablesHelper.SetVariable(this))
{
LoggerInstance.Logger.LogError("Failed to set new variable.");
}
}
});
}
internal Variable Clone(bool profile = false)
{
return new Variable
{
Name = Name,
Values = Values,
ParentType = profile ? VariablesSetType.Profile : ParentType,
ValuesList = ValuesStringToValuesListItemCollection(Values),
};
}
public bool Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
return false;
}
const int MaxUserEnvVariableLength = 255; // User-wide env vars stored in the registry have names limited to 255 chars
if (ParentType != VariablesSetType.System && Name.Length >= MaxUserEnvVariableLength)
{
LoggerInstance.Logger.LogError("Variable name too long.");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using EnvironmentVariablesUILib.ViewModels;
namespace EnvironmentVariablesUILib.Models
{
public partial class VariablesSet : ObservableObject
{
public static readonly Guid UserGuid = new Guid("92F7AA9A-AE31-49CD-83C8-80A71E432AA5");
public static readonly Guid SystemGuid = new Guid("F679C74D-DB00-4795-92E1-B1F6A4833279");
private static readonly string UserIconPath = "/Assets/EnvironmentVariables/UserIcon.png";
private static readonly string SystemIconPath = "/Assets/EnvironmentVariables/SystemIcon.png";
protected static readonly string ProfileIconPath = "/Assets/EnvironmentVariables/ProfileIcon.png";
public Guid Id { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Valid))]
private string _name;
[JsonIgnore]
public VariablesSetType Type { get; set; }
[JsonIgnore]
public string IconPath { get; set; }
[ObservableProperty]
private ObservableCollection<Variable> _variables;
public bool Valid => Validate();
public VariablesSet()
{
}
public VariablesSet(Guid id, string name, VariablesSetType type)
{
Id = id;
Name = name;
Type = type;
Variables = new ObservableCollection<Variable>();
IconPath = Type switch
{
VariablesSetType.User => UserIconPath,
VariablesSetType.System => SystemIconPath,
VariablesSetType.Profile => ProfileIconPath,
_ => throw new NotImplementedException(),
};
}
private bool Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace EnvironmentVariablesUILib.Models
{
public enum VariablesSetType
{
Path = 0,
Duplicate,
Profile,
User,
System,
}
}

View File

@@ -0,0 +1,286 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DefaultSetsLabel.Text" xml:space="preserve">
<value>Default variables</value>
</data>
<data name="DefaultVariablesLbl.Text" xml:space="preserve">
<value>Default variables</value>
</data>
<data name="NewProfileBtn.Text" xml:space="preserve">
<value>New profile</value>
</data>
<data name="ProfilesDescriptionLbl.Text" xml:space="preserve">
<value>You can create profiles to quickly apply a set of preconfigured variables</value>
</data>
<data name="ProfilesLbl.Text" xml:space="preserve">
<value>Profiles</value>
</data>
<data name="System" xml:space="preserve">
<value>System</value>
</data>
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Environment Variables</value>
<comment>Title of the window when running as user</comment>
</data>
<data name="EditDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="EditVariableDialog_Title" xml:space="preserve">
<value>Edit variable</value>
</data>
<data name="NewProfileNameTxtBox.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="SaveBtn" xml:space="preserve">
<value>Save</value>
</data>
<data name="ValueTxtBox.Header" xml:space="preserve">
<value>Value</value>
</data>
<data name="AddBtn" xml:space="preserve">
<value>Save</value>
</data>
<data name="AddNewProfileDialog_Title" xml:space="preserve">
<value>New profile</value>
</data>
<data name="NewProfileEnabled.Header" xml:space="preserve">
<value>Enabled</value>
</data>
<data name="AddVariableBtn.Text" xml:space="preserve">
<value>Add variable</value>
</data>
<data name="AddNewVariableName.Header" xml:space="preserve">
<value>Name</value>
<comment>Name as in Environment variable name</comment>
</data>
<data name="AddNewVariableValue.Header" xml:space="preserve">
<value>Value</value>
<comment>Value as in Environment variable value</comment>
</data>
<data name="ExistingVariableSegmentedButton.Content" xml:space="preserve">
<value>Existing</value>
</data>
<data name="NewVariableSegmentedButton.Content" xml:space="preserve">
<value>New</value>
</data>
<data name="WindowAdminTitle" xml:space="preserve">
<value>Administrator: Environment Variables</value>
<comment>Title of the window when running as administrator</comment>
</data>
<data name="CancelAddVariableBtn.Content" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ConfirmAddVariableBtn.Content" xml:space="preserve">
<value>Add</value>
</data>
<data name="AppliedVariablesDescriptionLbl.Text" xml:space="preserve">
<value>List of applied variables</value>
</data>
<data name="AppliedVariablesLbl.Text" xml:space="preserve">
<value>Applied variables</value>
</data>
<data name="NewProfileVariablesListViewHeader.Text" xml:space="preserve">
<value>Variables</value>
</data>
<data name="DeleteMenuItem.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Delete_Dialog_Description" xml:space="preserve">
<value>Are you sure you want to delete this profile? Deleting applied profile will remove all profile variables.</value>
</data>
<data name="EditSystemDefaultSetInfoBar.Text" xml:space="preserve">
<value>Administrator permissions are required to edit System variables</value>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="StateNotUpToDateTitle" xml:space="preserve">
<value>Changes were made outside of this app.</value>
</data>
<data name="StateNotUpToDateOnStartupMsg" xml:space="preserve">
<value>Variables included in applied profile have been modified. Review the latest changes before applying the profile again.</value>
</data>
<data name="CancelBtn" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="StateNotUpToDateEnvironmentMessageReceivedMsg" xml:space="preserve">
<value>Variables have been modified. Reload to get the latest changes.</value>
</data>
<data name="AddVariable_Title" xml:space="preserve">
<value>Add variable</value>
</data>
<data name="Delete_Variable_Description" xml:space="preserve">
<value>Are you sure you want to delete this variable?</value>
</data>
<data name="EditProfileDialog_Title" xml:space="preserve">
<value>Edit profile</value>
</data>
<data name="AddVariableTooltip.Text" xml:space="preserve">
<value>Add variable</value>
</data>
<data name="DefaultVariablesDescriptionLbl.Text" xml:space="preserve">
<value>Add, remove or edit USER and SYSTEM variables</value>
</data>
<data name="EditItem.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="More_Options_ButtonTooltip.Text" xml:space="preserve">
<value>More options</value>
</data>
<data name="MoveDown.Text" xml:space="preserve">
<value>Move down</value>
</data>
<data name="MoveUp.Text" xml:space="preserve">
<value>Move up</value>
</data>
<data name="InsertListEntryBefore.Text" xml:space="preserve">
<value>Insert Before</value>
</data>
<data name="InsertListEntryAfter.Text" xml:space="preserve">
<value>Insert After</value>
</data>
<data name="NewProfileVariablesListViewApplyToSystemHeader.Text" xml:space="preserve">
<value>Apply to SYSTEM?</value>
</data>
<data name="RemoveListItem.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="RemoveItem.Text" xml:space="preserve">
<value>Remove</value>
</data>
<data name="AddVariableContent.Content" xml:space="preserve">
<value>Add variable</value>
</data>
<data name="ProfileNotApplicableTitle" xml:space="preserve">
<value>Profile can not be applied.</value>
</data>
<data name="StateProfileNotApplicableMsg" xml:space="preserve">
<value>Variables or backup variables are invalid.</value>
</data>
<data name="VariableIsAppliedByActiveProfileTooltip.Text" xml:space="preserve">
<value>This variable is written by the active profile</value>
</data>
</root>

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using EnvironmentVariablesUILib.Models;
namespace EnvironmentVariablesUILib.Telemetry
{
public interface ITelemetry
{
abstract void LogEnvironmentVariablesProfileEnabledEvent(bool enabled);
abstract void LogEnvironmentVariablesVariableChangedEvent(VariablesSetType type);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using EnvironmentVariablesUILib.Telemetry;
namespace EnvironmentVariablesUILib.Helpers
{
public static class TelemetryInstance
{
public static ITelemetry Telemetry { get; set; }
}
}

View File

@@ -0,0 +1,421 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using EnvironmentVariablesUILib.Helpers;
using EnvironmentVariablesUILib.Models;
using EnvironmentVariablesUILib.Telemetry;
using Microsoft.UI.Dispatching;
namespace EnvironmentVariablesUILib.ViewModels
{
public partial class MainViewModel : ObservableObject
{
private readonly IEnvironmentVariablesService _environmentVariablesService;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
public DefaultVariablesSet UserDefaultSet { get; private set; } = new DefaultVariablesSet(VariablesSet.UserGuid, ResourceLoaderInstance.ResourceLoader.GetString("User"), VariablesSetType.User);
public DefaultVariablesSet SystemDefaultSet { get; private set; } = new DefaultVariablesSet(VariablesSet.SystemGuid, ResourceLoaderInstance.ResourceLoader.GetString("System"), VariablesSetType.System);
public VariablesSet DefaultVariables { get; private set; } = new DefaultVariablesSet(Guid.NewGuid(), "DefaultVariables", VariablesSetType.User);
[ObservableProperty]
private ObservableCollection<ProfileVariablesSet> _profiles;
[ObservableProperty]
private ObservableCollection<Variable> _appliedVariables = new ObservableCollection<Variable>();
[ObservableProperty]
private bool _isElevated;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInfoBarButtonVisible))]
private EnvironmentState _environmentState;
public bool IsInfoBarButtonVisible => EnvironmentState == EnvironmentState.EnvironmentMessageReceived;
public ProfileVariablesSet AppliedProfile { get; set; }
public MainViewModel(IElevationHelper elevationHelper, IEnvironmentVariablesService environmentVariablesService, ILogger logger, ITelemetry telemetry)
{
_environmentVariablesService = environmentVariablesService;
ElevationHelper.ElevationHelperInstance = elevationHelper;
LoggerInstance.Logger = logger;
TelemetryInstance.Telemetry = telemetry;
var isElevated = ElevationHelper.ElevationHelperInstance.IsElevated;
IsElevated = isElevated;
}
private void LoadDefaultVariables()
{
UserDefaultSet.Variables.Clear();
SystemDefaultSet.Variables.Clear();
DefaultVariables.Variables.Clear();
EnvironmentVariablesHelper.GetVariables(EnvironmentVariableTarget.Machine, SystemDefaultSet);
EnvironmentVariablesHelper.GetVariables(EnvironmentVariableTarget.User, UserDefaultSet);
foreach (var variable in UserDefaultSet.Variables)
{
DefaultVariables.Variables.Add(variable);
if (AppliedProfile != null)
{
if (AppliedProfile.Variables.Where(
x => (x.Name.Equals(variable.Name, StringComparison.OrdinalIgnoreCase) && x.Values.Equals(variable.Values, StringComparison.OrdinalIgnoreCase))
|| variable.Name.Equals(EnvironmentVariablesHelper.GetBackupVariableName(x, AppliedProfile.Name), StringComparison.OrdinalIgnoreCase)).Any())
{
// If it's a user variable that's also in the profile or is a backup variable, mark it as applied from profile.
variable.IsAppliedFromProfile = true;
}
}
}
foreach (var variable in SystemDefaultSet.Variables)
{
DefaultVariables.Variables.Add(variable);
}
}
public void LoadEnvironmentVariables()
{
LoadDefaultVariables();
LoadProfiles();
PopulateAppliedVariables();
}
private void LoadProfiles()
{
try
{
var profiles = _environmentVariablesService.ReadProfiles();
foreach (var profile in profiles)
{
profile.PropertyChanged += Profile_PropertyChanged;
foreach (var variable in profile.Variables)
{
variable.ParentType = VariablesSetType.Profile;
}
}
var appliedProfiles = profiles.Where(x => x.IsEnabled).ToList();
if (appliedProfiles.Count > 0)
{
var appliedProfile = appliedProfiles.First();
if (appliedProfile.IsCorrectlyApplied())
{
AppliedProfile = appliedProfile;
EnvironmentState = EnvironmentState.Unchanged;
}
else
{
EnvironmentState = EnvironmentState.ChangedOnStartup;
appliedProfile.IsEnabled = false;
}
}
Profiles = new ObservableCollection<ProfileVariablesSet>(profiles);
}
catch (Exception ex)
{
// Show some error
LoggerInstance.Logger.LogError("Failed to load profiles.json file", ex);
Profiles = new ObservableCollection<ProfileVariablesSet>();
}
}
private void PopulateAppliedVariables()
{
LoadDefaultVariables();
var variables = new List<Variable>();
if (AppliedProfile != null)
{
variables = variables.Concat(AppliedProfile.Variables.Select(x => new Variable(x.Name, Environment.ExpandEnvironmentVariables(x.Values), VariablesSetType.Profile)).OrderBy(x => x.Name)).ToList();
}
// Variables are expanded to be shown in the applied variables section, so the user sees their actual values.
variables = variables.Concat(UserDefaultSet.Variables.Select(x => new Variable(x.Name, Environment.ExpandEnvironmentVariables(x.Values), VariablesSetType.User)).OrderBy(x => x.Name))
.Concat(SystemDefaultSet.Variables.Select(x => new Variable(x.Name, Environment.ExpandEnvironmentVariables(x.Values), VariablesSetType.System)).OrderBy(x => x.Name))
.ToList();
// Handle PATH variable - add USER value to the end of the SYSTEM value
var profilePath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.Profile).FirstOrDefault();
var userPath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.User).FirstOrDefault();
var systemPath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.System).FirstOrDefault();
if (systemPath != null)
{
var clone = systemPath.Clone();
clone.ParentType = VariablesSetType.Path;
if (userPath != null)
{
clone.Values += ";" + userPath.Values;
variables.Remove(userPath);
}
if (profilePath != null)
{
variables.Remove(profilePath);
}
variables.Insert(variables.IndexOf(systemPath), clone);
variables.Remove(systemPath);
}
variables = variables.GroupBy(x => x.Name).Select(y => y.First()).ToList();
// Find duplicates
var duplicates = variables.Where(x => !x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase)).GroupBy(x => x.Name.ToLower(CultureInfo.InvariantCulture)).Where(g => g.Count() > 1);
foreach (var duplicate in duplicates)
{
var userVar = duplicate.ElementAt(0);
var systemVar = duplicate.ElementAt(1);
var clone = userVar.Clone();
clone.ParentType = VariablesSetType.Duplicate;
clone.Name = systemVar.Name;
variables.Insert(variables.IndexOf(userVar), clone);
variables.Remove(userVar);
variables.Remove(systemVar);
}
variables = variables.OrderBy(x => x.ParentType).ToList();
AppliedVariables = new ObservableCollection<Variable>(variables);
}
internal void AddDefaultVariable(Variable variable, VariablesSetType type)
{
if (type == VariablesSetType.User)
{
UserDefaultSet.Variables.Add(variable);
UserDefaultSet.Variables = new ObservableCollection<Variable>(UserDefaultSet.Variables.OrderBy(x => x.Name).ToList());
}
else if (type == VariablesSetType.System)
{
SystemDefaultSet.Variables.Add(variable);
SystemDefaultSet.Variables = new ObservableCollection<Variable>(SystemDefaultSet.Variables.OrderBy(x => x.Name).ToList());
}
EnvironmentVariablesHelper.SetVariable(variable);
PopulateAppliedVariables();
}
internal void EditVariable(Variable original, Variable edited, ProfileVariablesSet variablesSet)
{
bool propagateChange = variablesSet == null /* not a profile */ || variablesSet.Id.Equals(AppliedProfile?.Id);
bool changed = original.Name != edited.Name || original.Values != edited.Values;
if (changed)
{
var task = original.Update(edited, propagateChange, variablesSet);
task.ContinueWith(x =>
{
_dispatcherQueue.TryEnqueue(() =>
{
PopulateAppliedVariables();
});
});
TelemetryInstance.Telemetry.LogEnvironmentVariablesVariableChangedEvent(original.ParentType);
_ = Task.Run(SaveAsync);
}
}
internal void AddProfile(ProfileVariablesSet profile)
{
profile.PropertyChanged += Profile_PropertyChanged;
if (profile.IsEnabled)
{
UnsetAppliedProfile();
SetAppliedProfile(profile);
}
Profiles.Add(profile);
_ = Task.Run(SaveAsync);
}
internal void UpdateProfile(ProfileVariablesSet updatedProfile)
{
var existingProfile = Profiles.Where(x => x.Id == updatedProfile.Id).FirstOrDefault();
if (existingProfile != null)
{
if (updatedProfile.IsEnabled)
{
// Let's unset the profile before applying the update. Even if this one is the one that's currently set.
UnsetAppliedProfile();
}
existingProfile.Name = updatedProfile.Name;
existingProfile.IsEnabled = updatedProfile.IsEnabled;
existingProfile.Variables = updatedProfile.Variables;
}
_ = Task.Run(SaveAsync);
}
private async Task SaveAsync()
{
try
{
await _environmentVariablesService.WriteAsync(Profiles);
}
catch (Exception ex)
{
// Show some error
LoggerInstance.Logger.LogError("Failed to save to profiles.json file", ex);
}
}
private void Profile_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var profile = sender as ProfileVariablesSet;
if (profile != null)
{
if (e.PropertyName == nameof(ProfileVariablesSet.IsEnabled))
{
if (profile.IsEnabled)
{
UnsetAppliedProfile();
SetAppliedProfile(profile);
TelemetryInstance.Telemetry.LogEnvironmentVariablesProfileEnabledEvent(true);
}
else
{
UnsetAppliedProfile();
TelemetryInstance.Telemetry.LogEnvironmentVariablesProfileEnabledEvent(false);
}
}
}
_ = Task.Run(SaveAsync);
}
private void SetAppliedProfile(ProfileVariablesSet profile)
{
if (profile != null)
{
if (!profile.IsApplicable())
{
profile.PropertyChanged -= Profile_PropertyChanged;
profile.IsEnabled = false;
profile.PropertyChanged += Profile_PropertyChanged;
EnvironmentState = EnvironmentState.ProfileNotApplicable;
return;
}
}
var task = profile.Apply();
task.ContinueWith((a) =>
{
_dispatcherQueue.TryEnqueue(() =>
{
PopulateAppliedVariables();
});
});
AppliedProfile = profile;
}
private void UnsetAppliedProfile()
{
if (AppliedProfile != null)
{
var appliedProfile = AppliedProfile;
appliedProfile.PropertyChanged -= Profile_PropertyChanged;
var task = AppliedProfile.UnApply();
task.ContinueWith((a) =>
{
_dispatcherQueue.TryEnqueue(() =>
{
PopulateAppliedVariables();
});
});
AppliedProfile.IsEnabled = false;
AppliedProfile = null;
appliedProfile.PropertyChanged += Profile_PropertyChanged;
}
}
internal void RemoveProfile(ProfileVariablesSet profile)
{
if (profile.IsEnabled)
{
UnsetAppliedProfile();
}
Profiles.Remove(profile);
_ = Task.Run(SaveAsync);
}
internal void DeleteVariable(Variable variable, ProfileVariablesSet profile)
{
bool propagateChange = true;
if (profile != null)
{
// Profile variable
profile.Variables.Remove(variable);
if (!profile.IsEnabled)
{
propagateChange = false;
}
_ = Task.Run(SaveAsync);
}
else
{
if (variable.ParentType == VariablesSetType.User)
{
UserDefaultSet.Variables.Remove(variable);
}
else if (variable.ParentType == VariablesSetType.System)
{
SystemDefaultSet.Variables.Remove(variable);
}
}
if (propagateChange)
{
var task = Task.Run(() =>
{
if (profile == null)
{
EnvironmentVariablesHelper.UnsetVariable(variable);
}
else
{
profile.UnapplyVariable(variable);
}
});
task.ContinueWith((a) =>
{
_dispatcherQueue.TryEnqueue(() =>
{
PopulateAppliedVariables();
});
});
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="EnvironmentVariables.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!--The ID below informs the system that this application is compatible with OS features first introduced in Windows 8.
For more info see https://docs.microsoft.com/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
It is also necessary to support features in unpackaged applications, for example the custom title bar implementation.-->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>