mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-10-07 14:06:47 +00:00
Add settings UI for theme and language (#780)
This commit is contained in:
@@ -6,6 +6,15 @@
|
||||
"MessageBoxNo": "No",
|
||||
"TitleBarSupportCreator": "Support the creator",
|
||||
"TitleBarOptions": "Options",
|
||||
"SettingsTitle": "Settings",
|
||||
"SettingsSave": "Save",
|
||||
"SettingsTheme": "Theme",
|
||||
"SettingsThemeAuto": "Auto",
|
||||
"SettingsThemeLight": "Light",
|
||||
"SettingsThemeDark": "Dark",
|
||||
"SettingsLanguage": "Language",
|
||||
"SettingsLanguageAuto": "System default",
|
||||
"SettingsSaveError": "Unable to save settings. Please try again.",
|
||||
"TitleBarClose": "Close",
|
||||
|
||||
"MenuImportConfig": "Import config",
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
"MessageBoxNo": "Nee",
|
||||
"TitleBarSupportCreator": "Steun de maker",
|
||||
"TitleBarOptions": "Opties",
|
||||
"SettingsTitle": "Instellingen",
|
||||
"SettingsSave": "Opslaan",
|
||||
"SettingsTheme": "Thema",
|
||||
"SettingsThemeAuto": "Automatisch",
|
||||
"SettingsThemeLight": "Licht",
|
||||
"SettingsThemeDark": "Donker",
|
||||
"SettingsLanguage": "Taal",
|
||||
"SettingsLanguageAuto": "Systeemstandaard",
|
||||
"SettingsSaveError": "Kan de instellingen niet opslaan. Probeer het opnieuw.",
|
||||
"TitleBarClose": "Sluiten",
|
||||
"MenuImportConfig": "Configuratie importeren",
|
||||
"MenuImportConfigTooltip": "Configuratie importeren",
|
||||
|
||||
@@ -478,7 +478,7 @@
|
||||
},
|
||||
"HideHome": {
|
||||
"Label": "'Start' in het navigatievenster verbergen",
|
||||
"ToolTip": "Verbergt de sectie 'Start' in het navigatievenster van Verkenner. Waarschuwing: dit kan ervoor zorgen dat snelkoppelingen in Snelle toegang niet worden bijgewerkt totdat je je pc of Verkenner opnieuw opstart."
|
||||
"ToolTip": "Verbergt de sectie 'Start' in het navigatievenster van Verkenner. Waarschuwing: dit kan ervoor zorgen dat snelkoppelingen in Snelle toegang niet worden bijgewerkt totdat je je pc of Verkenner opnieuw opstart.",
|
||||
"ApplyText": "De sectie 'Start' in het navigatievenster van Verkenner verbergen",
|
||||
"UndoLabel": "'Start' in het navigatievenster tonen",
|
||||
"ApplyUndoText": "De sectie 'Start' in het navigatievenster van Verkenner tonen"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -7,6 +7,15 @@
|
||||
"TitleBarSupportCreator": "Apoiar o criador",
|
||||
"TitleBarOptions": "Opções",
|
||||
"TitleBarClose": "Fechar",
|
||||
"SettingsTitle": "Configurações",
|
||||
"SettingsSave": "Salvar",
|
||||
"SettingsTheme": "Tema",
|
||||
"SettingsThemeAuto": "Auto",
|
||||
"SettingsThemeLight": "Claro",
|
||||
"SettingsThemeDark": "Escuro",
|
||||
"SettingsLanguage": "Idioma",
|
||||
"SettingsLanguageAuto": "Padrão do sistema",
|
||||
"SettingsSaveError": "Não foi possível salvar as configurações. Tente novamente.",
|
||||
|
||||
"MenuImportConfig": "Importar configuração",
|
||||
"MenuImportConfigTooltip": "Importar configuração",
|
||||
|
||||
+2
-17
@@ -363,6 +363,7 @@
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="%LANG:TitleBarSupportCreator%" AutomationProperties.Name="%LANG:TitleBarSupportCreator%"/>
|
||||
<Button x:Name="SettingsBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="%LANG:SettingsTitle%" AutomationProperties.Name="%LANG:SettingsTitle%"/>
|
||||
<Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="%LANG:TitleBarOptions%" AutomationProperties.Name="%LANG:TitleBarOptions%">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu x:Name="MainMenu">
|
||||
@@ -685,23 +686,7 @@
|
||||
</ScrollViewer>
|
||||
<Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="28" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="0"/>
|
||||
</TextBlock.RenderTransform>
|
||||
<TextBlock.Triggers>
|
||||
<EventTrigger RoutedEvent="Loaded">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
|
||||
From="0" To="360"
|
||||
Duration="0:0:1.5"
|
||||
RepeatBehavior="Forever"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</TextBlock.Triggers>
|
||||
</TextBlock>
|
||||
<TextBlock Style="{DynamicResource LoadingSpinnerStyle}" Margin="0,0,0,8"/>
|
||||
<TextBlock Text="%LANG:AppLoadingIndicator%" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="%LANG:SettingsTitle%" Width="500" SizeToContent="Height"
|
||||
ResizeMode="NoResize" WindowStartupLocation="CenterOwner" WindowStyle="None"
|
||||
AllowsTransparency="True" Background="Transparent" ShowInTaskbar="False">
|
||||
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
|
||||
<TextBlock Text="%LANG:SettingsTitle%" Style="{DynamicResource ModalTitleTextStyle}" HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
<Button x:Name="CloseBtn" Grid.Row="0"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||
Width="36" Height="32"
|
||||
BorderThickness="0" IsCancel="True"
|
||||
Cursor="Hand"
|
||||
ToolTip="%LANG:TitleBarClose%"
|
||||
AutomationProperties.Name="%LANG:TitleBarClose%">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" BorderThickness="0" CornerRadius="0,8,0,0">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10"/>
|
||||
</Button>
|
||||
<StackPanel Grid.Row="1" Margin="20,4,20,10">
|
||||
<TextBlock Text="%LANG:SettingsTheme%" Style="{DynamicResource ModalInfoLabelTextStyle}" HorizontalAlignment="Left" Margin="0,0,0,10"/>
|
||||
<UniformGrid Columns="3" Margin="0,0,0,20">
|
||||
<Button x:Name="ThemeAutoButton" Tag="Auto" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="12,0" Margin="0,0,6,0" AutomationProperties.Name="%LANG:SettingsThemeAuto%">
|
||||
<StackPanel Orientation="Horizontal"><TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" Margin="0,0,8,0" VerticalAlignment="Center"/><TextBlock Text="%LANG:SettingsThemeAuto%"/></StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="ThemeLightButton" Tag="Light" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="12,0" Margin="3,0,3,0" AutomationProperties.Name="%LANG:SettingsThemeLight%">
|
||||
<StackPanel Orientation="Horizontal"><TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" Margin="0,0,8,0" VerticalAlignment="Center"/><TextBlock Text="%LANG:SettingsThemeLight%"/></StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="ThemeDarkButton" Tag="Dark" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="12,0" Margin="6,0,0,0" AutomationProperties.Name="%LANG:SettingsThemeDark%">
|
||||
<StackPanel Orientation="Horizontal"><TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" Margin="0,0,8,0" VerticalAlignment="Center"/><TextBlock Text="%LANG:SettingsThemeDark%"/></StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
<TextBlock Text="%LANG:SettingsLanguage%" Style="{DynamicResource ModalInfoLabelTextStyle}" HorizontalAlignment="Left" Margin="0,0,0,10"/>
|
||||
<ComboBox x:Name="SettingsLanguageCombo" AutomationProperties.Name="%LANG:SettingsLanguage%"/>
|
||||
<TextBlock x:Name="SettingsErrorText" Style="{DynamicResource ModalSubtextStyle}" Visibility="Collapsed" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button x:Name="SaveButton" Grid.Column="1" Content="%LANG:SettingsSave%" IsDefault="True" Padding="12,0" Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -1,6 +1,30 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- Loading spinner for app inventory. -->
|
||||
<Style x:Key="LoadingSpinnerStyle" TargetType="TextBlock">
|
||||
<Setter Property="Text" Value=""/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppIconFontFamily}"/>
|
||||
<Setter Property="FontSize" Value="28"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
|
||||
<Setter Property="RenderTransform"><Setter.Value><RotateTransform Angle="0"/></Setter.Value></Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsVisible" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard x:Name="LoadingSpinStoryboard">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
|
||||
From="0" To="360" Duration="0:0:1.5" RepeatBehavior="Forever"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions><RemoveStoryboard BeginStoryboardName="LoadingSpinStoryboard"/></Trigger.ExitActions>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- Primary Button Style -->
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonBgColor}"/>
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the required language catalog names.
|
||||
|
||||
.OUTPUTS
|
||||
System.String. The required catalog names.
|
||||
#>
|
||||
function Get-LanguageCatalogNames {
|
||||
return @('Chrome', 'Features', 'Categories', 'Apps')
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Lists language folders containing every required JSON catalog.
|
||||
|
||||
.PARAMETER LanguagesPath
|
||||
The directory containing language folders. Defaults to $script:LanguagesPath.
|
||||
|
||||
.OUTPUTS
|
||||
System.IO.DirectoryInfo. The available language folders.
|
||||
#>
|
||||
function Get-AvailableLanguageFolders {
|
||||
param([string]$LanguagesPath = $script:LanguagesPath)
|
||||
|
||||
$catalogNames = Get-LanguageCatalogNames
|
||||
foreach ($folder in (Get-ChildItem -LiteralPath $LanguagesPath -Directory | Sort-Object Name)) {
|
||||
$missingCatalogs = @($catalogNames | Where-Object {
|
||||
-not (Test-Path -LiteralPath (Join-Path $folder.FullName "$_.json") -PathType Leaf)
|
||||
})
|
||||
if ($missingCatalogs.Count -eq 0) { $folder }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves a language code to an available Config/Languages folder, or falls back to en-US.
|
||||
@@ -35,22 +68,19 @@ function Import-LanguageContent {
|
||||
)
|
||||
|
||||
$folderPath = Join-Path $LanguagesPath $LanguageFolder
|
||||
$chrome = Import-JsonFile -filePath (Join-Path $folderPath 'Chrome.json')
|
||||
$features = Import-JsonFile -filePath (Join-Path $folderPath 'Features.json')
|
||||
$categories = Import-JsonFile -filePath (Join-Path $folderPath 'Categories.json')
|
||||
$apps = Import-JsonFile -filePath (Join-Path $folderPath 'Apps.json')
|
||||
|
||||
if (-not $chrome -or -not $features -or -not $categories -or -not $apps) {
|
||||
return $null
|
||||
$catalogs = @{}
|
||||
foreach ($name in (Get-LanguageCatalogNames)) {
|
||||
$catalogs[$name] = Import-JsonFile -filePath (Join-Path $folderPath "$name.json")
|
||||
}
|
||||
if (@($catalogs.Values | Where-Object { -not $_ }).Count) { return $null }
|
||||
|
||||
return [PSCustomObject]@{
|
||||
LanguageCode = $LanguageFolder
|
||||
Chrome = $chrome
|
||||
Features = $features.Features
|
||||
UiGroups = $features.UiGroups
|
||||
Categories = $categories
|
||||
Apps = $apps
|
||||
Chrome = $catalogs.Chrome
|
||||
Features = $catalogs.Features.Features
|
||||
UiGroups = $catalogs.Features.UiGroups
|
||||
Categories = $catalogs.Categories
|
||||
Apps = $catalogs.Apps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,25 +428,33 @@ function Test-LanguageKeyCoverage {
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Substitutes %LANG:Key% markers in XAML text with translated, XML-escaped values.
|
||||
Replaces %LANG:Key% markers with translated text or dynamic language resources.
|
||||
|
||||
.DESCRIPTION
|
||||
Only resolves flat Chrome.json keys, since XAML markers never reference a Feature/Category/
|
||||
UiGroup field directly (those get their text from Get-Translation calls in the GUI scripts
|
||||
that build dynamic controls). Runs a single pass over every marker rather than sequential
|
||||
.Replace() calls, so an already-substituted value can't be re-matched by a later key.
|
||||
Resolves Chrome catalog markers using the active language and its English fallback.
|
||||
Supports embedded text or dynamic resources for windows that allow language changes.
|
||||
Throws when a marker's key is missing from both languages.
|
||||
|
||||
After substitution, scans for any %LANG:...% text that survived unresolved and throws,
|
||||
since Get-Translation's key-as-fallback behavior means a missing key would otherwise render
|
||||
as plain, un-marked text (e.g. "TitleBarClose" instead of a visible error) rather than being
|
||||
caught here. Checks both the active language and its Fallback before flagging a key missing,
|
||||
so a partially-translated language degrades to en-US text instead of failing to load.
|
||||
.PARAMETER Xaml
|
||||
The XAML text containing %LANG:Key% markers to replace.
|
||||
|
||||
.PARAMETER Lang
|
||||
The loaded language object and its optional Fallback. Defaults to $script:Lang.
|
||||
|
||||
.PARAMETER DynamicResources
|
||||
Uses DynamicResource references to Language_<key> resources for live language updates.
|
||||
The window must provide those resources. When omitted, embeds XML-escaped translated text.
|
||||
|
||||
.OUTPUTS
|
||||
System.String. The XAML text with localization markers replaced.
|
||||
#>
|
||||
function ConvertTo-LocalizedXaml {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Xaml,
|
||||
[object]$Lang = $script:Lang
|
||||
[object]$Lang = $script:Lang,
|
||||
# For live windows, keep a resource reference instead of embedding the translated text.
|
||||
[switch]$DynamicResources
|
||||
)
|
||||
|
||||
$missingKeys = New-Object System.Collections.Generic.List[string]
|
||||
@@ -426,6 +464,7 @@ function ConvertTo-LocalizedXaml {
|
||||
$key = $match.Groups[1].Value
|
||||
$resolvesSomewhere = @(Get-LanguageFallbackChain -Lang $Lang) | Where-Object { $_.Chrome -and $_.Chrome.PSObject.Properties[$key] }
|
||||
if (-not $resolvesSomewhere) { $missingKeys.Add($key) }
|
||||
if ($DynamicResources) { return "{DynamicResource Language_$key}" }
|
||||
$value = if ($resolvesSomewhere) { Get-Translation -Key $key -Lang $Lang } else { $key }
|
||||
return [System.Security.SecurityElement]::Escape($value)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#>
|
||||
function Get-SystemUsesDarkMode {
|
||||
try {
|
||||
$personalizeKey = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
|
||||
$personalizeKey = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -ErrorAction Stop
|
||||
|
||||
if ($null -eq $personalizeKey) {
|
||||
Write-Host "WARNING: Unable to retrieve personalization settings." -ForegroundColor Yellow
|
||||
|
||||
@@ -444,11 +444,12 @@ function Add-AppsToMainWindow {
|
||||
$dot = New-Object System.Windows.Shapes.Ellipse
|
||||
$dot.Style = $Window.Resources['AppRecommendationDotStyle']
|
||||
$dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
|
||||
$dot.ToolTip = switch ($app.Recommendation) {
|
||||
'safe' { Get-Translation -Key 'AppLegendRecommendedTooltip' }
|
||||
'unsafe' { Get-Translation -Key 'AppLegendNotRecommendedTooltip' }
|
||||
default { Get-Translation -Key 'AppLegendOptionalTooltip' }
|
||||
$recommendationKey = switch ($app.Recommendation) {
|
||||
'safe' { 'AppLegendRecommendedTooltip' }
|
||||
'unsafe' { 'AppLegendNotRecommendedTooltip' }
|
||||
default { 'AppLegendOptionalTooltip' }
|
||||
}
|
||||
$dot.SetResourceReference([System.Windows.FrameworkElement]::ToolTipProperty, "Language_$recommendationKey")
|
||||
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
|
||||
|
||||
$tbName = New-Object System.Windows.Controls.TextBlock
|
||||
@@ -479,6 +480,8 @@ function Add-AppsToMainWindow {
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId)
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppNameText' -Value $tbName
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescriptionText' -Value $tbDesc
|
||||
|
||||
$checkbox.Add_Checked({
|
||||
$w = $script:MainWindow
|
||||
@@ -574,6 +577,8 @@ function Initialize-MainWindowApps {
|
||||
# Prevent concurrent loads
|
||||
if ($script:IsLoadingApps) { return }
|
||||
$script:IsLoadingApps = $true
|
||||
$settingsButton = $Window.FindName('SettingsBtn')
|
||||
if ($settingsButton) { $settingsButton.IsEnabled = $false }
|
||||
|
||||
if ($ImportConfigBtn) {
|
||||
$ImportConfigBtn.IsEnabled = $false
|
||||
@@ -622,6 +627,8 @@ function Initialize-MainWindowApps {
|
||||
}
|
||||
finally {
|
||||
$script:IsLoadingApps = $false
|
||||
$settingsButton = $Window.FindName('SettingsBtn')
|
||||
if ($settingsButton) { $settingsButton.IsEnabled = $true }
|
||||
}
|
||||
}) | Out-Null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Populates dynamic language resources for a window and its detached menus.
|
||||
|
||||
.DESCRIPTION
|
||||
Keeps text bound to language resources in sync with the active language, including
|
||||
menus and popups that use separate resources.
|
||||
|
||||
.PARAMETER Window
|
||||
The window whose language resources are updated, along with its menus and popups.
|
||||
#>
|
||||
function Update-WindowLanguageResources {
|
||||
param([System.Windows.Window]$Window)
|
||||
|
||||
$keys = @(foreach ($language in (Get-LanguageFallbackChain -Lang $script:Lang)) {
|
||||
$language.Chrome.PSObject.Properties.Name
|
||||
}) | Sort-Object -Unique
|
||||
# Popups and context menus can have a separate resource lookup tree.
|
||||
$targets = @($Window, $Window.FindName('MainMenu'), $Window.FindName('PresetsPopup'), $Window.FindName('TweaksPresetsPopup')) | Where-Object { $null -ne $_ }
|
||||
foreach ($key in $keys) {
|
||||
$text = Get-Translation -Key $key -Section 'Chrome'
|
||||
foreach ($target in $targets) { $target.Resources["Language_$key"] = $text }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Refreshes localized text on existing main window tweak controls.
|
||||
|
||||
.DESCRIPTION
|
||||
Updates tweak text and accessibility labels for the active language while preserving
|
||||
existing controls and selections.
|
||||
|
||||
.PARAMETER Window
|
||||
The main window containing the registered dynamic tweak controls.
|
||||
#>
|
||||
function Update-MainWindowTweakLanguage {
|
||||
param([System.Windows.Window]$Window)
|
||||
|
||||
foreach ($category in $script:CategoryLanguageControls) {
|
||||
$label = Get-Translation -Key $category.CategoryId -Field 'Label' -Section 'Categories'
|
||||
$category.Header.Text = $label
|
||||
$category.HelpButton.ToolTip = Get-Translation -Key 'TweaksCategoryHelpTooltip' -FormatArgs @($label)
|
||||
}
|
||||
foreach ($name in $script:UiControlMappings.Keys) {
|
||||
$mapping = $script:UiControlMappings[$name]
|
||||
$control = $Window.FindName($name)
|
||||
if (-not $control) { continue }
|
||||
$text = Get-TweakControlText -Mapping $mapping
|
||||
if ($mapping.Type -eq 'group') {
|
||||
# Update item contents, keeping item identity and SelectedIndex unchanged.
|
||||
for ($i = 0; $i -lt $text.Options.Count; $i++) {
|
||||
$control.Items[$i].Content = $text.Options[$i]
|
||||
}
|
||||
}
|
||||
$control.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $text.Label)
|
||||
if ($control -is [System.Windows.Controls.CheckBox]) { $control.Content = $text.Label }
|
||||
else { $Window.FindName("${name}_LabelBorder").Child.Text = $text.Label }
|
||||
# Label borders share this tooltip TextBlock with their combobox.
|
||||
if ($control.ToolTip -is [System.Windows.Controls.TextBlock]) { $control.ToolTip.Text = $text.ToolTip }
|
||||
}
|
||||
Update-FeatureLabelLookup
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Refreshes localized app text and preset tooltips.
|
||||
|
||||
.DESCRIPTION
|
||||
Updates app details, accessibility labels, and preset tooltips for the active language
|
||||
while preserving app selections.
|
||||
|
||||
.PARAMETER AppsPanel
|
||||
The panel containing the app selection controls.
|
||||
#>
|
||||
function Update-MainWindowAppLanguage {
|
||||
param([System.Windows.Controls.Panel]$AppsPanel)
|
||||
|
||||
foreach ($app in $AppsPanel.Children) {
|
||||
if ($app -isnot [System.Windows.Controls.CheckBox]) { continue }
|
||||
$primaryAppId = @($app.AppIds)[0]
|
||||
$app.AppName = Get-Translation -Key $primaryAppId -Field 'FriendlyName' -Section 'Apps'
|
||||
$app.AppDescription = Get-Translation -Key $primaryAppId -Field 'Description' -Section 'Apps'
|
||||
$app.AppNameText.Text = $app.AppName
|
||||
$app.AppDescriptionText.Text = $app.AppDescription
|
||||
$app.AppDescriptionText.ToolTip = $app.AppDescription
|
||||
$app.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $app.AppName)
|
||||
}
|
||||
foreach ($preset in $script:JsonPresetCheckboxes) {
|
||||
$preset.ToolTip = Get-Translation -Key 'AppPresetSelectTooltip' -FormatArgs @($preset.Content)
|
||||
}
|
||||
# A future installed-only toggle must not consume a catalog in the old language.
|
||||
$script:PreloadedAppData = $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Applies the current language to an existing main window.
|
||||
|
||||
.DESCRIPTION
|
||||
Updates the interface and search results for the active language while preserving
|
||||
user selections and app scroll position.
|
||||
|
||||
.PARAMETER Window
|
||||
The initialized main window to refresh.
|
||||
#>
|
||||
function Update-MainWindowLanguage {
|
||||
param([System.Windows.Window]$Window)
|
||||
|
||||
$appsPanel = $Window.FindName('AppSelectionPanel')
|
||||
$appsScroll = Find-ParentScrollViewer -Element $appsPanel
|
||||
$appOffset = if ($appsScroll) { $appsScroll.VerticalOffset } else { 0 }
|
||||
$activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) {
|
||||
$script:AppSearchMatches[$script:AppSearchMatchIndex]
|
||||
}
|
||||
|
||||
Update-WindowLanguageResources -Window $Window
|
||||
Update-MainWindowTweakLanguage -Window $Window
|
||||
Update-MainWindowAppLanguage -AppsPanel $appsPanel
|
||||
|
||||
$userCombo = $Window.FindName('UserSelectionCombo')
|
||||
$userCombo.Items[0].Content = Get-Translation -Key 'HomeCurrentUserWithName' -FormatArgs @(Get-UserName)
|
||||
Update-UserSelectionDescription -Window $Window -UserSelectionCombo $userCombo -OtherUsernameTextBox $Window.FindName('OtherUsernameTextBox') -UserSelectionDescription $Window.FindName('UserSelectionDescription')
|
||||
Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $Window.FindName('AppSelectionStatus') -AppRemovalScopeCombo $Window.FindName('AppRemovalScopeCombo') -AppRemovalScopeSection $Window.FindName('AppRemovalScopeSection') -AppRemovalScopeDescription $Window.FindName('AppRemovalScopeDescription') -UserSelectionCombo $userCombo
|
||||
$validationMessage = $Window.FindName('UsernameValidationMessage')
|
||||
if ($validationMessage.Text) {
|
||||
Test-OtherUsername -Window $Window -UserSelectionCombo $userCombo -OtherUsernameTextBox $Window.FindName('OtherUsernameTextBox') -UsernameValidationMessage $validationMessage -AppRemovalScopeCombo $Window.FindName('AppRemovalScopeCombo') | Out-Null
|
||||
}
|
||||
|
||||
Update-AppsPanelSort -AppsPanel $appsPanel -SortArrowName $Window.FindName('SortArrowName') -SortArrowDescription $Window.FindName('SortArrowDescription') -SortArrowAppId $Window.FindName('SortArrowAppId')
|
||||
# Rerun matching against the translated labels without moving the user's viewport.
|
||||
$script:UpdatingLanguage = $true
|
||||
try {
|
||||
foreach ($name in @('AppSearchBox', 'TweakSearchBox')) {
|
||||
$Window.FindName($name).RaiseEvent([System.Windows.Controls.TextChangedEventArgs]::new([System.Windows.Controls.TextBox]::TextChangedEvent, [System.Windows.Controls.UndoAction]::None))
|
||||
}
|
||||
}
|
||||
finally { $script:UpdatingLanguage = $false }
|
||||
$activeIndex = [array]::IndexOf($script:AppSearchMatches, $activeMatch)
|
||||
if ($activeIndex -ge 0) {
|
||||
$script:AppSearchMatches[0].SetResourceReference([System.Windows.Controls.Control]::BackgroundProperty, 'SearchHighlightColor')
|
||||
$activeMatch.SetResourceReference([System.Windows.Controls.Control]::BackgroundProperty, 'SearchHighlightActiveColor')
|
||||
$script:AppSearchMatchIndex = $activeIndex
|
||||
}
|
||||
if ($appsScroll) { $appsScroll.ScrollToVerticalOffset($appOffset) }
|
||||
Hide-Bubble -Immediate
|
||||
}
|
||||
@@ -1,6 +1,77 @@
|
||||
# MainWindow-TweaksBuilder.ps1
|
||||
# Dynamic tweaks UI construction from Features.json, tweak state management, selection clear, and search/highlight.
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves localized labels, tooltips, and options for a tweak control.
|
||||
|
||||
.DESCRIPTION
|
||||
Provides consistent text for individual features and groups during control creation
|
||||
and language changes.
|
||||
|
||||
.PARAMETER Mapping
|
||||
A control mapping with Type and either FeatureId or GroupId and Values. Group values
|
||||
contain FeatureIds and fallback Label text.
|
||||
|
||||
.PARAMETER Features
|
||||
The feature definitions keyed by FeatureId. Defaults to $script:Features.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject. Label, ToolTip, and Options, beginning with the No Change option.
|
||||
#>
|
||||
function Get-TweakControlText {
|
||||
param(
|
||||
[hashtable]$Mapping,
|
||||
[hashtable]$Features = $script:Features
|
||||
)
|
||||
|
||||
$options = @((Get-Translation -Key 'TweaksOptionNoChange'))
|
||||
if ($Mapping.Type -eq 'group') {
|
||||
$label = Get-Translation -Key $Mapping.GroupId -Field 'Label' -Section 'UiGroups'
|
||||
$tooltip = Get-Translation -Key $Mapping.GroupId -Field 'ToolTip' -Section 'UiGroups'
|
||||
$options += @($Mapping.Values | ForEach-Object {
|
||||
Get-GroupValueTranslation -GroupId $Mapping.GroupId -FeatureId $_.FeatureIds[0] -FallbackLabel $_.Label
|
||||
})
|
||||
}
|
||||
else {
|
||||
$feature = $Features[$Mapping.FeatureId]
|
||||
$label = Get-Translation -Key $Mapping.FeatureId -Field 'Label' -Section 'Features'
|
||||
$tooltip = if ($feature.DisableWhenApplied -eq $true) {
|
||||
Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
|
||||
}
|
||||
else { Get-Translation -Key $Mapping.FeatureId -Field 'ToolTip' -Section 'Features' }
|
||||
$optionKey = if ($Mapping.FeatureId -match '^Disable') { 'TweaksOptionDisable' }
|
||||
elseif ($Mapping.FeatureId -match '^Enable') { 'TweaksOptionEnable' }
|
||||
else { 'TweaksOptionApply' }
|
||||
$options += Get-Translation -Key $optionKey
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{ Label = $label; ToolTip = $tooltip; Options = $options }
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Rebuilds localized feature and undo label lookups.
|
||||
|
||||
.DESCRIPTION
|
||||
Keeps feature names used in change overviews in sync with the active language.
|
||||
|
||||
.PARAMETER Features
|
||||
The feature definitions to translate. Defaults to the values of $script:Features.
|
||||
#>
|
||||
function Update-FeatureLabelLookup {
|
||||
param([object[]]$Features = @($script:Features.Values))
|
||||
|
||||
$script:FeatureLabelLookup = @{}
|
||||
$script:UndoFeatureLabelLookup = @{}
|
||||
foreach ($feature in $Features) {
|
||||
$script:FeatureLabelLookup[$feature.FeatureId] = Get-Translation -Key $feature.FeatureId -Field 'Label' -Section 'Features'
|
||||
if ($feature.UndoLabel) {
|
||||
$script:UndoFeatureLabelLookup[$feature.FeatureId] = Get-Translation -Key $feature.FeatureId -Field 'UndoLabel' -Section 'Features'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the main window's dynamic tweak controls from Features.json.
|
||||
@@ -39,6 +110,7 @@ function New-DynamicTweakControls {
|
||||
|
||||
$script:UiControlMappings = @{}
|
||||
$script:CategoryCardMap = @{}
|
||||
$script:CategoryLanguageControls = @()
|
||||
$script:TweaksCompactMode = $null
|
||||
$script:TweaksCardsMovedFromCol2 = @()
|
||||
|
||||
@@ -128,6 +200,56 @@ function New-DynamicTweakControls {
|
||||
return $combo
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Attaches a wrapping tooltip to a tweak control and its label border.
|
||||
|
||||
.PARAMETER Control
|
||||
The tweak control receiving the tooltip.
|
||||
|
||||
.PARAMETER TooltipText
|
||||
The localized text displayed by the tooltip.
|
||||
|
||||
.PARAMETER ShowOnDisabled
|
||||
Enables tooltip display even when the tweak control is disabled.
|
||||
#>
|
||||
function Set-TweakControlToolTip($control, $tooltipText, [switch]$ShowOnDisabled) {
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
$tipBlock.Text = $tooltipText
|
||||
$tipBlock.TextWrapping = 'Wrap'
|
||||
$tipBlock.MaxWidth = 420
|
||||
$control.ToolTip = $tipBlock
|
||||
if ($ShowOnDisabled) {
|
||||
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($control, $true)
|
||||
}
|
||||
$labelBorder = $Window.FindName("$($control.Name)_LabelBorder")
|
||||
if ($labelBorder) { $labelBorder.ToolTip = $tipBlock }
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates and maps a localized control for an individual feature.
|
||||
|
||||
.PARAMETER Feature
|
||||
The feature definition to display.
|
||||
|
||||
.PARAMETER Parent
|
||||
The category panel that receives the generated control.
|
||||
|
||||
.PARAMETER CategoryId
|
||||
The category identifier stored in the control mapping.
|
||||
#>
|
||||
function Add-FeatureTweakControl($feature, $parent, $categoryId) {
|
||||
$mapping = @{ Type = 'feature'; FeatureId = $feature.FeatureId; CategoryId = $categoryId }
|
||||
$text = Get-TweakControlText -Mapping $mapping -Features $featureMap
|
||||
$controlName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
$control = New-LabeledCombo -parent $parent -labelText $text.Label -comboName $controlName -items $text.Options
|
||||
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
|
||||
Set-TweakControlToolTip -control $control -tooltipText $text.ToolTip -ShowOnDisabled
|
||||
}
|
||||
$script:UiControlMappings[$controlName] = $mapping
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the Features wiki URL for a tweak category.
|
||||
@@ -213,6 +335,7 @@ function New-DynamicTweakControls {
|
||||
if ($button.Tag) { Start-Process $button.Tag }
|
||||
})
|
||||
$headerRow.Children.Add($helpBtn) | Out-Null
|
||||
$script:CategoryLanguageControls += @{ CategoryId = $categoryObj.CategoryId; Header = $header; HelpButton = $helpBtn }
|
||||
|
||||
$panel.Children.Add($headerRow) | Out-Null
|
||||
|
||||
@@ -348,93 +471,31 @@ function New-DynamicTweakControls {
|
||||
$soleFid = $featureIds[0]
|
||||
|
||||
if ($featureMap.ContainsKey($soleFid)) {
|
||||
$soleFeature = $featureMap[$soleFid]
|
||||
$soleFeatureLabel = Get-Translation -Key $soleFeature.FeatureId -Field 'Label' -Section 'Features'
|
||||
$opt = Get-Translation -Key 'TweaksOptionApply'
|
||||
if ($soleFeature.FeatureId -match '^Disable') { $opt = Get-Translation -Key 'TweaksOptionDisable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = Get-Translation -Key 'TweaksOptionEnable' }
|
||||
$items = @((Get-Translation -Key 'TweaksOptionNoChange'), $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $soleFeature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $soleFeatureLabel -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present
|
||||
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = Get-Translation -Key $soleFeature.FeatureId -Field 'ToolTip' -Section 'Features'
|
||||
if ($soleFeature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
|
||||
}
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
$tipBlock.Text = $tooltipText
|
||||
$tipBlock.TextWrapping = 'Wrap'
|
||||
$tipBlock.MaxWidth = 420
|
||||
$combo.ToolTip = $tipBlock
|
||||
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true)
|
||||
$lblBorderObj = $null
|
||||
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeatureLabel; CategoryId = $categoryId }
|
||||
Add-FeatureTweakControl -feature $featureMap[$soleFid] -parent $panel -categoryId $categoryId
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$groupLabel = Get-Translation -Key $group.GroupId -Field 'Label' -Section 'UiGroups'
|
||||
$items = @((Get-Translation -Key 'TweaksOptionNoChange')) + ($filteredValues | ForEach-Object { Get-GroupValueTranslation -GroupId $group.GroupId -FeatureId $_.FeatureIds[0] -FallbackLabel $_.Label })
|
||||
$mapping = @{ Type = 'group'; GroupId = $group.GroupId; Values = $filteredValues; CategoryId = $categoryId }
|
||||
$text = Get-TweakControlText -Mapping $mapping -Features $featureMap
|
||||
$comboName = 'Group_{0}Combo' -f $group.GroupId
|
||||
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $groupLabel -comboName $comboName -items $items
|
||||
# attach tooltip from UiGroups if present
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $text.Label -comboName $comboName -items $text.Options
|
||||
if ($group.ToolTip) {
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
$tipBlock.Text = Get-Translation -Key $group.GroupId -Field 'ToolTip' -Section 'UiGroups'
|
||||
$tipBlock.TextWrapping = 'Wrap'
|
||||
$tipBlock.MaxWidth = 420
|
||||
$combo.ToolTip = $tipBlock
|
||||
$lblBorderObj = $null
|
||||
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
Set-TweakControlToolTip -control $combo -tooltipText $text.ToolTip
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $groupLabel; CategoryId = $categoryId }
|
||||
$script:UiControlMappings[$comboName] = $mapping
|
||||
}
|
||||
elseif ($item.Type -eq 'feature') {
|
||||
$feature = $item.Data
|
||||
$featureLabel = Get-Translation -Key $feature.FeatureId -Field 'Label' -Section 'Features'
|
||||
$opt = Get-Translation -Key 'TweaksOptionApply'
|
||||
if ($feature.FeatureId -match '^Disable') { $opt = Get-Translation -Key 'TweaksOptionDisable' } elseif ($feature.FeatureId -match '^Enable') { $opt = Get-Translation -Key 'TweaksOptionEnable' }
|
||||
$items = @((Get-Translation -Key 'TweaksOptionNoChange'), $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $featureLabel -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present, and include the disabled-state reason
|
||||
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = Get-Translation -Key $feature.FeatureId -Field 'ToolTip' -Section 'Features'
|
||||
if ($feature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
|
||||
}
|
||||
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
$tipBlock.Text = $tooltipText
|
||||
$tipBlock.TextWrapping = 'Wrap'
|
||||
$tipBlock.MaxWidth = 420
|
||||
$combo.ToolTip = $tipBlock
|
||||
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true)
|
||||
$lblBorderObj = $null
|
||||
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; CategoryId = $categoryId }
|
||||
Add-FeatureTweakControl -feature $item.Data -parent $panel -categoryId $categoryId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
|
||||
$script:FeatureLabelLookup = @{}
|
||||
$script:UndoFeatureLabelLookup = @{}
|
||||
foreach ($f in $featuresJson.Features) {
|
||||
$script:FeatureLabelLookup[$f.FeatureId] = Get-Translation -Key $f.FeatureId -Field 'Label' -Section 'Features'
|
||||
if ($f.UndoLabel) {
|
||||
$script:UndoFeatureLabelLookup[$f.FeatureId] = Get-Translation -Key $f.FeatureId -Field 'UndoLabel' -Section 'Features'
|
||||
}
|
||||
}
|
||||
# Overview generation uses the same labels as language refresh.
|
||||
Update-FeatureLabelLookup -Features $featuresJson.Features
|
||||
}
|
||||
|
||||
function Update-CurrentTweakSystemState {
|
||||
|
||||
@@ -113,6 +113,7 @@ function Set-WindowThemeResources {
|
||||
}
|
||||
|
||||
$Theme = if ($usesDarkMode) { 'Dark' } else { 'Light' }
|
||||
$window.Resources['AppUsesDarkMode'] = $usesDarkMode
|
||||
|
||||
foreach ($Group in $ThemeColor.GetEnumerator()) {
|
||||
foreach ($Resource in $Group.Value.GetEnumerator()) {
|
||||
@@ -130,12 +131,13 @@ function Set-WindowThemeResources {
|
||||
$window.Resources['AppIconFontFamily'] = [System.Windows.Media.FontFamily]::new($iconFontName)
|
||||
|
||||
# Load and merge shared styles
|
||||
if ($script:SharedStylesSchema -and (Test-Path $script:SharedStylesSchema)) {
|
||||
if (-not $window.Resources.Contains('AppSharedStylesLoaded') -and $script:SharedStylesSchema -and (Test-Path $script:SharedStylesSchema)) {
|
||||
$sharedXaml = Get-Content -Path $script:SharedStylesSchema -Raw
|
||||
$sharedReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($sharedXaml))
|
||||
try {
|
||||
$sharedDict = [System.Windows.Markup.XamlReader]::Load($sharedReader)
|
||||
$window.Resources.MergedDictionaries.Add($sharedDict)
|
||||
$window.Resources['AppSharedStylesLoaded'] = $true
|
||||
}
|
||||
finally {
|
||||
$sharedReader.Close()
|
||||
|
||||
@@ -13,7 +13,7 @@ function Show-AboutDialog {
|
||||
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||
|
||||
$usesDarkMode = Get-SystemUsesDarkMode
|
||||
$usesDarkMode = Get-AppUsesDarkMode
|
||||
|
||||
# Determine owner window
|
||||
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
function Show-AppSelectionWindow {
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||
|
||||
$usesDarkMode = Get-SystemUsesDarkMode
|
||||
$usesDarkMode = Get-AppUsesDarkMode
|
||||
|
||||
# Show overlay if main window exists
|
||||
$overlay = $null
|
||||
|
||||
@@ -18,7 +18,7 @@ function Show-ApplyModal {
|
||||
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||
|
||||
$usesDarkMode = Get-SystemUsesDarkMode
|
||||
$usesDarkMode = Get-AppUsesDarkMode
|
||||
|
||||
# Determine owner window
|
||||
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
Creates and displays the main Win11Debloat window.
|
||||
#>
|
||||
function Show-MainWindow {
|
||||
if (-not $script:Lang) { throw 'Unable to load UI language.' }
|
||||
$script:CancelRequested = $false
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
|
||||
|
||||
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
|
||||
$usesDarkMode = Get-SystemUsesDarkMode
|
||||
$usesDarkMode = Get-AppUsesDarkMode
|
||||
|
||||
# ---- Load XAML ----
|
||||
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml -DynamicResources
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
@@ -20,6 +22,7 @@ function Show-MainWindow {
|
||||
}
|
||||
|
||||
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
|
||||
Update-WindowLanguageResources -Window $window
|
||||
|
||||
$mainBorder = $window.FindName('MainBorder')
|
||||
$titleBarBackground = $window.FindName('TitleBarBackground')
|
||||
@@ -46,12 +49,13 @@ function Show-MainWindow {
|
||||
$script:GuiWindow = $window
|
||||
|
||||
# ---- Handle unhandled exceptions on the dispatcher thread ----
|
||||
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.Add_UnhandledException({
|
||||
$dispatcherErrorHandler = [System.Windows.Threading.DispatcherUnhandledExceptionEventHandler]{
|
||||
param($sender, $e)
|
||||
Write-Warning "Unhandled exception in GUI: $($e.Exception.Message)"
|
||||
Write-Warning "Stack trace: $($e.Exception.StackTrace)"
|
||||
$e.Handled = $true
|
||||
})
|
||||
}
|
||||
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.Add_UnhandledException($dispatcherErrorHandler)
|
||||
|
||||
# ---- Window chrome helpers ----
|
||||
$updateWindowChrome = { Update-MainWindowChrome -Window $window -MainBorder $mainBorder -TitleBarBackground $titleBarBackground -NormalWindowShadow $normalWindowShadow }
|
||||
@@ -82,6 +86,7 @@ function Show-MainWindow {
|
||||
|
||||
# ---- Menu/button event wiring ----
|
||||
$kofiBtn.Add_Click({ Start-Process "https://ko-fi.com/raphire" })
|
||||
$window.FindName('SettingsBtn').Add_Click({ Open-MainWindowSettings -Window $window })
|
||||
|
||||
$menuBtn.Add_Click({
|
||||
$menuBtn.ContextMenu.PlacementTarget = $menuBtn
|
||||
@@ -167,7 +172,7 @@ function Show-MainWindow {
|
||||
# ---- Wire export/import ----
|
||||
$exportConfigBtn.Add_Click({
|
||||
try {
|
||||
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
|
||||
Export-Configuration -Owner $window -UsesDarkMode (Get-AppUsesDarkMode) -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Export configuration failed: $($_.Exception.Message)"
|
||||
@@ -177,7 +182,7 @@ function Show-MainWindow {
|
||||
|
||||
$importConfigBtn.Add_Click({
|
||||
try {
|
||||
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo; Update-AppPresetStates -AppsPanel $appsPanel } -OnImportCompleted {
|
||||
Import-Configuration -Owner $window -UsesDarkMode (Get-AppUsesDarkMode) -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo; Update-AppPresetStates -AppsPanel $appsPanel } -OnImportCompleted {
|
||||
$tabControl.SelectedIndex = 3
|
||||
Update-NavigationButtons -Window $window -TabControl $tabControl
|
||||
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
|
||||
@@ -382,16 +387,14 @@ function Show-MainWindow {
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($searchText)) { return }
|
||||
|
||||
$highlightBrush = $window.Resources["SearchHighlightColor"]
|
||||
$activeHighlightBrush = $window.Resources["SearchHighlightActiveColor"]
|
||||
|
||||
# Keep existing highlights in sync when Auto changes the window's theme resources.
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') {
|
||||
$appName = if ($child.AppName) { $child.AppName } else { '' }
|
||||
$appId = if ($child.Tag) { $child.Tag.ToString() } else { '' }
|
||||
$appDesc = if ($child.AppDescription) { $child.AppDescription } else { '' }
|
||||
if ($appName.ToLower().Contains($searchText) -or $appId.ToLower().Contains($searchText) -or $appDesc.ToLower().Contains($searchText)) {
|
||||
$child.Background = $highlightBrush
|
||||
$child.SetResourceReference([System.Windows.Controls.Control]::BackgroundProperty, 'SearchHighlightColor')
|
||||
$script:AppSearchMatches += $child
|
||||
}
|
||||
}
|
||||
@@ -399,9 +402,9 @@ function Show-MainWindow {
|
||||
|
||||
if ($script:AppSearchMatches.Count -gt 0) {
|
||||
$script:AppSearchMatchIndex = 0
|
||||
$script:AppSearchMatches[0].Background = $activeHighlightBrush
|
||||
$script:AppSearchMatches[0].SetResourceReference([System.Windows.Controls.Control]::BackgroundProperty, 'SearchHighlightActiveColor')
|
||||
$scrollViewer = Find-ParentScrollViewer -Element $appsPanel
|
||||
if ($scrollViewer) {
|
||||
if ($scrollViewer -and -not $script:UpdatingLanguage) {
|
||||
Scroll-ToItemIfNotVisible -ScrollViewer $scrollViewer -Item $script:AppSearchMatches[0] -Container $appsPanel
|
||||
}
|
||||
}
|
||||
@@ -410,9 +413,9 @@ function Show-MainWindow {
|
||||
$appSearchBox.Add_KeyDown({
|
||||
param($sourceControl, $e)
|
||||
if ($e.Key -eq [System.Windows.Input.Key]::Enter -and $script:AppSearchMatches.Count -gt 0) {
|
||||
$script:AppSearchMatches[$script:AppSearchMatchIndex].Background = $window.Resources["SearchHighlightColor"]
|
||||
$script:AppSearchMatches[$script:AppSearchMatchIndex].SetResourceReference([System.Windows.Controls.Control]::BackgroundProperty, 'SearchHighlightColor')
|
||||
$script:AppSearchMatchIndex = ($script:AppSearchMatchIndex + 1) % $script:AppSearchMatches.Count
|
||||
$script:AppSearchMatches[$script:AppSearchMatchIndex].Background = $window.Resources["SearchHighlightActiveColor"]
|
||||
$script:AppSearchMatches[$script:AppSearchMatchIndex].SetResourceReference([System.Windows.Controls.Control]::BackgroundProperty, 'SearchHighlightActiveColor')
|
||||
$scrollViewer = Find-ParentScrollViewer -Element $appsPanel
|
||||
if ($scrollViewer) {
|
||||
Scroll-ToItemIfNotVisible -ScrollViewer $scrollViewer -Item $script:AppSearchMatches[$script:AppSearchMatchIndex] -Container $appsPanel
|
||||
@@ -440,7 +443,6 @@ function Show-MainWindow {
|
||||
if ([string]::IsNullOrWhiteSpace($searchText)) { return }
|
||||
|
||||
$firstMatch = $null
|
||||
$highlightBrush = $window.Resources["SearchHighlightColor"]
|
||||
$col0 = $window.FindName('Column0Panel')
|
||||
$col1 = $window.FindName('Column1Panel')
|
||||
$col2 = $window.FindName('Column2Panel')
|
||||
@@ -472,7 +474,13 @@ function Show-MainWindow {
|
||||
}
|
||||
|
||||
if ($matchFound -and $controlToHighlight) {
|
||||
$controlToHighlight.Background = $highlightBrush
|
||||
$backgroundProperty = if ($controlToHighlight -is [System.Windows.Controls.Border]) {
|
||||
[System.Windows.Controls.Border]::BackgroundProperty
|
||||
}
|
||||
else {
|
||||
[System.Windows.Controls.Control]::BackgroundProperty
|
||||
}
|
||||
$controlToHighlight.SetResourceReference($backgroundProperty, 'SearchHighlightColor')
|
||||
if ($null -eq $firstMatch) { $firstMatch = $controlToHighlight }
|
||||
}
|
||||
}
|
||||
@@ -480,7 +488,7 @@ function Show-MainWindow {
|
||||
}
|
||||
}
|
||||
|
||||
if ($firstMatch -and $tweaksScrollViewer) {
|
||||
if ($firstMatch -and $tweaksScrollViewer -and -not $script:UpdatingLanguage) {
|
||||
Scroll-ToItemIfNotVisible -ScrollViewer $tweaksScrollViewer -Item $firstMatch -Container $tweaksGrid
|
||||
}
|
||||
})
|
||||
@@ -896,6 +904,10 @@ function Show-MainWindow {
|
||||
})
|
||||
|
||||
$window.Show() | Out-Null
|
||||
$themeTimer = [System.Windows.Threading.DispatcherTimer]::new()
|
||||
$themeTimer.Interval = [TimeSpan]::FromSeconds(2)
|
||||
$themeTimer.Add_Tick({ Update-AutoThemeResources -Window $window })
|
||||
$themeTimer.Start()
|
||||
|
||||
# If WhatIf mode is enabled, notify the user that no changes will be made
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
@@ -904,6 +916,11 @@ function Show-MainWindow {
|
||||
}) | Out-Null
|
||||
}
|
||||
|
||||
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
|
||||
try { [System.Windows.Threading.Dispatcher]::PushFrame($frame) }
|
||||
finally {
|
||||
$themeTimer.Stop()
|
||||
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.Remove_UnhandledException($dispatcherErrorHandler)
|
||||
if ($script:BubblePopup) { $script:BubblePopup.IsOpen = $false }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ function Show-MessageBox {
|
||||
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||
|
||||
$usesDarkMode = Get-SystemUsesDarkMode
|
||||
$usesDarkMode = Get-AppUsesDarkMode
|
||||
|
||||
# Determine owner window - use provided Owner, or fall back to main GUI window
|
||||
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
|
||||
|
||||
@@ -26,7 +26,7 @@ function Show-RestoreBackupDialog {
|
||||
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||
|
||||
$usesDarkMode = Get-SystemUsesDarkMode
|
||||
$usesDarkMode = Get-AppUsesDarkMode
|
||||
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
|
||||
|
||||
$overlay = $null
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shows the modal theme and language settings dialog.
|
||||
|
||||
.DESCRIPTION
|
||||
Lets users choose a theme and language. Changes take effect only after Save;
|
||||
closing without saving discards edits. Displays an error if saving fails.
|
||||
|
||||
.PARAMETER Owner
|
||||
The window that owns the settings dialog.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when changed preferences were saved; $false when closed
|
||||
without changes or without saving.
|
||||
#>
|
||||
function Show-SettingsDialog {
|
||||
param([Parameter(Mandatory)][System.Windows.Window]$Owner)
|
||||
$path = Join-Path (Split-Path $script:MainWindowSchema -Parent) 'SettingsWindow.xaml'
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml (Get-Content -LiteralPath $path -Raw)
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try { $dialog = [System.Windows.Markup.XamlReader]::Load($reader) }
|
||||
finally { $reader.Close() }
|
||||
$dialog.Owner = $Owner
|
||||
Set-WindowThemeResources -window $dialog -usesDarkMode (Get-AppUsesDarkMode)
|
||||
$dialog.Tag = $false
|
||||
$overlay = $Owner.FindName('ModalOverlay')
|
||||
$languageCombo = $dialog.FindName('SettingsLanguageCombo')
|
||||
$auto = [System.Windows.Controls.ComboBoxItem]::new()
|
||||
$auto.Content = Get-Translation -Key 'SettingsLanguageAuto'
|
||||
$auto.Tag = 'Auto'
|
||||
[void]$languageCombo.Items.Add($auto)
|
||||
foreach ($folder in (Get-AvailableLanguageFolders)) {
|
||||
$item = [System.Windows.Controls.ComboBoxItem]::new()
|
||||
try {
|
||||
$culture = [System.Globalization.CultureInfo]::GetCultureInfo($folder.Name)
|
||||
$name = $culture.NativeName
|
||||
$item.Content = $culture.TextInfo.ToUpper($name.Substring(0, 1)) + $name.Substring(1)
|
||||
}
|
||||
catch { $item.Content = $folder.Name }
|
||||
$item.Tag = $folder.Name
|
||||
[void]$languageCombo.Items.Add($item)
|
||||
}
|
||||
foreach ($item in $languageCombo.Items) {
|
||||
if ($item.Tag -eq $script:UiPreferences.Language) { $languageCombo.SelectedItem = $item; break }
|
||||
}
|
||||
|
||||
# Event handlers share the pending value without changing saved preferences.
|
||||
$pendingTheme = [ref]([string]$script:UiPreferences.Theme)
|
||||
$savePreferences = {
|
||||
$theme = $pendingTheme.Value
|
||||
$language = [string]$languageCombo.SelectedItem.Tag
|
||||
if ($theme -eq $script:UiPreferences.Theme -and $language -eq $script:UiPreferences.Language) { $dialog.Close(); return }
|
||||
try {
|
||||
$lang = if ($language -eq $script:UiPreferences.Language) { $script:Lang } else { Get-UiLanguage -Language $language }
|
||||
if (-not $lang) { throw 'Unable to load language.' }
|
||||
if ($language -ne 'Auto' -and $lang.LanguageCode -ne $language) { throw 'The selected language could not be loaded.' }
|
||||
$preferences = @{ Theme = $theme; Language = $language }
|
||||
Save-UiPreferences -Preferences $preferences
|
||||
$script:UiPreferences = $preferences
|
||||
$script:Lang = $lang
|
||||
$dialog.Tag = $true
|
||||
$dialog.Close()
|
||||
}
|
||||
catch {
|
||||
$errorText = $dialog.FindName('SettingsErrorText')
|
||||
$errorText.Text = Get-Translation -Key 'SettingsSaveError'
|
||||
$errorText.Visibility = 'Visible'
|
||||
Write-Warning "Unable to update UI preferences: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
foreach ($mode in @('Auto', 'Light', 'Dark')) {
|
||||
$button = $dialog.FindName("Theme${mode}Button")
|
||||
if ($mode -eq $script:UiPreferences.Theme) { $button.SetResourceReference([System.Windows.FrameworkElement]::StyleProperty, 'PrimaryButtonStyle') }
|
||||
$button.Add_Click({
|
||||
param($sender, $e)
|
||||
$pendingTheme.Value = [string]$sender.Tag
|
||||
foreach ($themeMode in @('Auto', 'Light', 'Dark')) {
|
||||
$style = if ($themeMode -eq $pendingTheme.Value) { 'PrimaryButtonStyle' } else { 'SecondaryButtonStyle' }
|
||||
$dialog.FindName("Theme${themeMode}Button").SetResourceReference([System.Windows.FrameworkElement]::StyleProperty, $style)
|
||||
}
|
||||
})
|
||||
}
|
||||
$dialog.FindName('SaveButton').Add_Click($savePreferences)
|
||||
$dialog.FindName('TitleBar').Add_MouseLeftButtonDown({ $dialog.DragMove() })
|
||||
$dialog.FindName('CloseBtn').Add_Click({ $dialog.Close() })
|
||||
try {
|
||||
if ($overlay) { $overlay.Visibility = 'Visible' }
|
||||
[void]$dialog.ShowDialog()
|
||||
return [bool]$dialog.Tag
|
||||
}
|
||||
finally { if ($overlay) { $overlay.Visibility = 'Collapsed' } }
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads saved UI theme and language preferences.
|
||||
|
||||
.DESCRIPTION
|
||||
Uses Auto when no valid saved theme or language preference is available.
|
||||
UI preferences are separate from exported debloat configurations.
|
||||
|
||||
.PARAMETER Path
|
||||
The preferences JSON file. Defaults to $script:UiPreferencesFilePath.
|
||||
|
||||
.OUTPUTS
|
||||
System.Collections.Hashtable. Theme and Language preference values.
|
||||
#>
|
||||
function Get-UiPreferences {
|
||||
param([string]$Path = $script:UiPreferencesFilePath)
|
||||
$preferences = @{ Theme = 'Auto'; Language = 'Auto' }
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
try {
|
||||
$saved = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($saved.Theme -in @('Auto', 'Light', 'Dark')) { $preferences.Theme = $saved.Theme }
|
||||
if ($saved.Language -eq 'Auto' -or $saved.Language -match '^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})+$') {
|
||||
$preferences.Language = $saved.Language
|
||||
}
|
||||
}
|
||||
catch { Write-Warning "Unable to read UI preferences: $($_.Exception.Message)" }
|
||||
}
|
||||
return $preferences
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Saves UI preferences to a JSON file.
|
||||
|
||||
.PARAMETER Preferences
|
||||
The Theme and Language preferences to save.
|
||||
|
||||
.PARAMETER Path
|
||||
The destination JSON file. Defaults to $script:UiPreferencesFilePath.
|
||||
#>
|
||||
function Save-UiPreferences {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Preferences,
|
||||
[string]$Path = $script:UiPreferencesFilePath
|
||||
)
|
||||
$directory = Split-Path -Parent $Path
|
||||
[void][System.IO.Directory]::CreateDirectory($directory)
|
||||
$temporaryPath = "$Path.tmp"
|
||||
try {
|
||||
$Preferences | ConvertTo-Json | Set-Content -LiteralPath $temporaryPath -Encoding UTF8 -ErrorAction Stop
|
||||
Move-Item -LiteralPath $temporaryPath -Destination $Path -Force -ErrorAction Stop
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $temporaryPath) { Remove-Item -LiteralPath $temporaryPath -Force }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves the application's effective dark mode setting.
|
||||
|
||||
.DESCRIPTION
|
||||
Uses the selected Light or Dark theme, or follows the Windows app theme in Auto mode.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. Whether the application should use dark mode.
|
||||
#>
|
||||
function Get-AppUsesDarkMode {
|
||||
switch ($script:UiPreferences.Theme) {
|
||||
'Light' { return $false }
|
||||
'Dark' { return $true }
|
||||
default { return (Get-SystemUsesDarkMode) }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads the language selected by a UI preference.
|
||||
|
||||
.DESCRIPTION
|
||||
Auto follows the current UI culture. Resolves the requested language to an available
|
||||
translation, falling back to English when needed.
|
||||
|
||||
.PARAMETER Language
|
||||
Auto to use the current UI culture, or a language code such as nl-NL.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject. The loaded language content, or $null when loading fails.
|
||||
#>
|
||||
function Get-UiLanguage {
|
||||
param([string]$Language)
|
||||
if ($Language -eq 'Auto') { return (Import-LanguageFile) }
|
||||
return (Import-LanguageFile -LanguageCode $Language)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Initializes UI preferences and the active language.
|
||||
|
||||
.DESCRIPTION
|
||||
Loads saved preferences and resolves the startup language, allowing an explicit
|
||||
language choice to override the saved preference.
|
||||
|
||||
.PARAMETER Language
|
||||
An optional language preference override, including Auto. When omitted or empty,
|
||||
uses the saved language preference.
|
||||
#>
|
||||
function Initialize-UiPreferences {
|
||||
param([string]$Language)
|
||||
|
||||
$script:UiPreferences = Get-UiPreferences
|
||||
if ($Language) { $script:UiPreferences.Language = $Language }
|
||||
$script:Lang = Get-UiLanguage -Language $script:UiPreferences.Language
|
||||
if ($script:Lang -and $script:UiPreferences.Language -ne 'Auto') {
|
||||
$script:UiPreferences.Language = $script:Lang.LanguageCode
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Refreshes window themes when the application follows the Windows theme.
|
||||
|
||||
.DESCRIPTION
|
||||
Keeps the window and its owned dialogs in sync with the Windows app theme when
|
||||
Auto is selected.
|
||||
|
||||
.PARAMETER Window
|
||||
The window to check, along with its owned windows.
|
||||
#>
|
||||
function Update-AutoThemeResources {
|
||||
param([System.Windows.Window]$Window)
|
||||
if ($script:UiPreferences.Theme -ne 'Auto') { return }
|
||||
$dark = Get-SystemUsesDarkMode
|
||||
if ($Window.Resources['AppUsesDarkMode'] -ne $dark) {
|
||||
Set-WindowThemeResources -window $Window -usesDarkMode $dark
|
||||
}
|
||||
foreach ($owned in $Window.OwnedWindows) { Update-AutoThemeResources -Window $owned }
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Opens settings and applies saved presentation changes to the main window.
|
||||
|
||||
.PARAMETER Window
|
||||
The main window that owns the settings dialog and receives saved changes.
|
||||
#>
|
||||
function Open-MainWindowSettings {
|
||||
param([System.Windows.Window]$Window)
|
||||
if ($script:IsLoadingApps) { return }
|
||||
$previousLanguageCode = $script:Lang.LanguageCode
|
||||
if (Show-SettingsDialog -Owner $Window) {
|
||||
Set-WindowThemeResources -window $Window -usesDarkMode (Get-AppUsesDarkMode)
|
||||
if ($script:Lang.LanguageCode -ne $previousLanguageCode) {
|
||||
Update-MainWindowLanguage -Window $Window
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,26 @@ BeforeAll {
|
||||
$script:LanguagesPath = Join-Path $PSScriptRoot 'TestData\LanguageLoading'
|
||||
}
|
||||
|
||||
Describe 'Get-AvailableLanguageFolders' {
|
||||
It 'lists complete folders in name order' {
|
||||
@(Get-AvailableLanguageFolders | Select-Object -ExpandProperty Name) | Should -Be @('en-US', 'es-ES')
|
||||
}
|
||||
|
||||
It 'excludes folders with a missing catalog or a directory in place of a catalog' {
|
||||
$languagesPath = Join-Path $TestDrive 'Discovery'
|
||||
New-Item -ItemType Directory -Path $languagesPath | Out-Null
|
||||
foreach ($name in @('complete', 'missing', 'directory')) {
|
||||
Copy-Item -LiteralPath (Join-Path $script:LanguagesPath 'en-US') -Destination (Join-Path $languagesPath $name) -Recurse
|
||||
}
|
||||
Remove-Item -LiteralPath (Join-Path $languagesPath 'missing/Apps.json')
|
||||
Remove-Item -LiteralPath (Join-Path $languagesPath 'directory/Apps.json')
|
||||
New-Item -ItemType Directory -Path (Join-Path $languagesPath 'directory/Apps.json') | Out-Null
|
||||
|
||||
@(Get-AvailableLanguageFolders -LanguagesPath $languagesPath | Select-Object -ExpandProperty Name) | Should -Be @('complete')
|
||||
(Import-LanguageContent -LanguageFolder 'complete' -LanguagesPath $languagesPath).Apps.'TestApp.One'.FriendlyName | Should -Be 'Test App'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Resolve-LanguageFolder' {
|
||||
It 'resolves <Case>' -ForEach @(
|
||||
@{ Case = 'an exact match'; LanguageCode = 'en-US'; Expected = 'en-US' }
|
||||
@@ -259,4 +279,12 @@ Describe 'ConvertTo-LocalizedXaml' {
|
||||
|
||||
{ ConvertTo-LocalizedXaml -Xaml $xaml -Lang $script:EnLang } | Should -Throw '*DisableTelemetry*'
|
||||
}
|
||||
|
||||
It 'emits live resource references while retaining missing-key and fallback validation' {
|
||||
$xaml = '<Button Content="%LANG:TitleBarOptions%"/>'
|
||||
ConvertTo-LocalizedXaml -Xaml $xaml -Lang $script:EsLang -DynamicResources |
|
||||
Should -Be '<Button Content="{DynamicResource Language_TitleBarOptions}"/>'
|
||||
{ ConvertTo-LocalizedXaml -Xaml '<Button Content="%LANG:MissingKey%"/>' -Lang $script:EnLang -DynamicResources } |
|
||||
Should -Throw '*MissingKey*'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,21 @@ Describe 'Get-UndoFeatureLabel' {
|
||||
Mock Get-ItemProperty { throw 'Registry unavailable' }
|
||||
Get-SystemUsesDarkMode | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'quietly falls back to light mode on repeated missing personalization lookups' {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
Mock Get-ItemProperty {
|
||||
param($ErrorAction)
|
||||
$lookupErrorAction = if ($ErrorAction) { $ErrorAction } else { 'Continue' }
|
||||
Write-Error -Message 'Personalization key was not found.' -Category ObjectNotFound -ErrorAction $lookupErrorAction
|
||||
}
|
||||
|
||||
foreach ($poll in 1..3) {
|
||||
$output = @(Get-SystemUsesDarkMode *>&1)
|
||||
$output.Count | Should -Be 1
|
||||
$output[0] | Should -BeFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-OtherUsername' {
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
# Headless component tests: no main/settings XAML, Show/ShowDialog, or message loop.
|
||||
BeforeAll {
|
||||
Add-Type -AssemblyName PresentationFramework
|
||||
$script:RepoRoot = Split-Path $PSScriptRoot -Parent
|
||||
foreach ($file in @('FileIO/Import-JsonFile', 'FileIO/Import-LanguageFile',
|
||||
'GUI/Ui-Preferences', 'GUI/Show-SettingsDialog', 'GUI/Set-WindowThemeResources',
|
||||
'GUI/MainWindow-Localization', 'GUI/MainWindow-TweaksBuilder', 'GUI/MainWindow-AppSelection',
|
||||
'GUI/MainWindow-Deployment', 'GUI/Show-Bubble', 'Helpers/Get-UserName')) {
|
||||
. "$script:RepoRoot/Scripts/$file.ps1"
|
||||
}
|
||||
$script:LanguagesPath = Join-Path $script:RepoRoot 'Config/Languages'
|
||||
$script:FeaturesFilePath = Join-Path $script:RepoRoot 'Config/Features.json'
|
||||
$script:Features = @{}
|
||||
foreach ($feature in (Import-JsonFile -filePath $script:FeaturesFilePath).Features) { $script:Features[$feature.FeatureId] = $feature }
|
||||
$tokens = $null; $parseErrors = $null
|
||||
$mainAst = [System.Management.Automation.Language.Parser]::ParseFile("$script:RepoRoot/Scripts/GUI/Show-MainWindow.ps1", [ref]$tokens, [ref]$parseErrors)
|
||||
$script:SearchHandlers = @{}
|
||||
foreach ($handler in $mainAst.FindAll({
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and
|
||||
$node.Member.Value -eq 'Add_TextChanged' -and $node.Expression.VariablePath.UserPath -in @('appSearchBox', 'tweakSearchBox')
|
||||
}, $true)) {
|
||||
$script:SearchHandlers[$handler.Expression.VariablePath.UserPath] = $handler.Arguments[0].ScriptBlock.GetScriptBlock()
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a minimal window for dynamic tweak localization tests.
|
||||
|
||||
.OUTPUTS
|
||||
System.Windows.Window. The test window with its registered column panels.
|
||||
#>
|
||||
function New-LanguageTestWindow {
|
||||
$window = [System.Windows.Window]::new()
|
||||
[System.Windows.NameScope]::SetNameScope($window, [System.Windows.NameScope]::new())
|
||||
$window.Content = [System.Windows.Controls.StackPanel]::new()
|
||||
foreach ($name in @('Column0Panel', 'Column1Panel', 'Column2Panel')) {
|
||||
$panel = [System.Windows.Controls.StackPanel]::new()
|
||||
$window.RegisterName($name, $panel)
|
||||
[void]$window.Content.Children.Add($panel)
|
||||
}
|
||||
return $window
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Language changes on existing controls' {
|
||||
BeforeEach {
|
||||
$script:Lang = Import-LanguageFile -LanguageCode 'en-US'
|
||||
$script:ModernStandbySupported = $false
|
||||
}
|
||||
|
||||
It 'matches fresh and refreshed tweak text on Windows build <Build>' -ForEach @(
|
||||
@{ Build = 19045 }
|
||||
@{ Build = 26100 }
|
||||
) {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Serializes current tweak presentation text for comparison.
|
||||
|
||||
.DESCRIPTION
|
||||
Captures labels, tooltips, and options to compare freshly created controls
|
||||
with controls refreshed after a language change.
|
||||
|
||||
.PARAMETER Window
|
||||
The test window containing registered tweak controls and label borders.
|
||||
|
||||
.OUTPUTS
|
||||
System.String. The serialized presentation snapshot.
|
||||
#>
|
||||
function Get-TweakPresentationSnapshot($window) {
|
||||
$rows = foreach ($name in ($script:UiControlMappings.Keys | Sort-Object)) {
|
||||
$control = $window.FindName($name)
|
||||
$label = if ($control -is [System.Windows.Controls.CheckBox]) { $control.Content }
|
||||
else { $window.FindName("${name}_LabelBorder").Child.Text }
|
||||
[PSCustomObject]@{
|
||||
Name = $name
|
||||
Label = $label
|
||||
ToolTip = if ($control.ToolTip) { $control.ToolTip.Text } else { $null }
|
||||
Options = @($control.Items | ForEach-Object { $_.Content })
|
||||
}
|
||||
}
|
||||
return ConvertTo-Json -InputObject @($rows) -Depth 4 -Compress
|
||||
}
|
||||
|
||||
foreach ($language in @('nl-NL', 'pt-BR')) {
|
||||
$script:Lang = Import-LanguageFile -LanguageCode $language
|
||||
$fresh = New-LanguageTestWindow
|
||||
New-DynamicTweakControls -Window $fresh -WinVersion $Build
|
||||
$expected = Get-TweakPresentationSnapshot $fresh
|
||||
|
||||
$script:Lang = Import-LanguageFile -LanguageCode 'en-US'
|
||||
$refreshed = New-LanguageTestWindow
|
||||
New-DynamicTweakControls -Window $refreshed -WinVersion $Build
|
||||
$script:Lang = Import-LanguageFile -LanguageCode $language
|
||||
Update-MainWindowTweakLanguage -Window $refreshed
|
||||
Get-TweakPresentationSnapshot $refreshed | Should -Be $expected
|
||||
}
|
||||
}
|
||||
|
||||
It 'updates text, accessibility names and detached menu resources with English fallback' {
|
||||
$window = New-LanguageTestWindow
|
||||
$button = [System.Windows.Controls.Button]::new()
|
||||
$button.SetResourceReference([System.Windows.Controls.ContentControl]::ContentProperty, 'Language_SettingsTitle')
|
||||
$button.SetResourceReference([System.Windows.Automation.AutomationProperties]::NameProperty, 'Language_SettingsTitle')
|
||||
[void]$window.Content.Children.Add($button)
|
||||
$menu = [System.Windows.Controls.ContextMenu]::new()
|
||||
$window.RegisterName('MainMenu', $menu)
|
||||
$item = [System.Windows.Controls.MenuItem]::new()
|
||||
$item.SetResourceReference([System.Windows.Controls.HeaderedItemsControl]::HeaderProperty, 'Language_MenuAbout')
|
||||
[void]$menu.Items.Add($item)
|
||||
|
||||
foreach ($language in @('en-US', 'nl-NL', 'pt-BR', 'en-US')) {
|
||||
$script:Lang = Import-LanguageFile -LanguageCode $language
|
||||
Update-WindowLanguageResources -Window $window
|
||||
$button.Content | Should -Be (Get-Translation -Key 'SettingsTitle')
|
||||
[System.Windows.Automation.AutomationProperties]::GetName($button) | Should -Be $button.Content
|
||||
$item.Header | Should -Be (Get-Translation -Key 'MenuAbout')
|
||||
}
|
||||
$script:Lang = [PSCustomObject]@{ Chrome = [PSCustomObject]@{ SettingsTitle = 'Custom & <text>' }; Fallback = $script:Lang }
|
||||
Update-WindowLanguageResources -Window $window
|
||||
$button.Content | Should -Be 'Custom & <text>'
|
||||
$item.Header | Should -Be 'About'
|
||||
}
|
||||
|
||||
It 'translates generated tweaks without replacing controls, selections or applied-state metadata' {
|
||||
$window = New-LanguageTestWindow
|
||||
New-DynamicTweakControls -Window $window -WinVersion 26100
|
||||
$checkbox = $window.FindName('Feature_DisableTelemetry_Combo')
|
||||
$checkbox.IsChecked = $false
|
||||
$checkbox | Add-Member -NotePropertyMembers @{ InitialState = $true; SystemState = $true }
|
||||
$disabled = $window.FindName('Feature_DisableWidgets_Combo')
|
||||
$disabled.IsChecked = $true
|
||||
$disabled.IsEnabled = $false
|
||||
$disabled | Add-Member -NotePropertyMembers @{ InitialState = $true; SystemState = $true; DisableWhenApplied = $true }
|
||||
$group = $window.FindName('Group_SearchIconCombo')
|
||||
$group.SelectedIndex = 2
|
||||
$group | Add-Member -NotePropertyMembers @{ InitialIndex = 1; SystemIndex = 1 }
|
||||
$selectedItem = $group.SelectedItem
|
||||
$script:SelectionChanges = 0
|
||||
$group.Add_SelectionChanged({ $script:SelectionChanges++ })
|
||||
$before = @(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode $true | ForEach-Object { "$($_.Action):$($_.FeatureId)" } | Sort-Object)
|
||||
|
||||
foreach ($language in @('nl-NL', 'pt-BR', 'en-US')) {
|
||||
$script:Lang = Import-LanguageFile -LanguageCode $language
|
||||
Update-MainWindowTweakLanguage -Window $window
|
||||
[object]::ReferenceEquals($window.FindName('Feature_DisableTelemetry_Combo'), $checkbox) | Should -BeTrue
|
||||
$checkbox.Content | Should -Be (Get-Translation -Key 'DisableTelemetry' -Field 'Label' -Section 'Features')
|
||||
$checkbox.ToolTip.Text | Should -Be (Get-Translation -Key 'DisableTelemetry' -Field 'ToolTip' -Section 'Features')
|
||||
$disabled.IsEnabled | Should -BeFalse
|
||||
$disabled.DisableWhenApplied | Should -BeTrue
|
||||
$disabled.ToolTip.Text | Should -Be (Get-Translation -Key 'TweaksAlreadyAppliedTooltip')
|
||||
[object]::ReferenceEquals($group.SelectedItem, $selectedItem) | Should -BeTrue
|
||||
$group.SelectedIndex | Should -Be 2
|
||||
$selectedItem.Content | Should -Be (Get-GroupValueTranslation -GroupId 'SearchIcon' -FeatureId $script:UiControlMappings['Group_SearchIconCombo'].Values[1].FeatureIds[0] -FallbackLabel 'fallback')
|
||||
@(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode $true | ForEach-Object { "$($_.Action):$($_.FeatureId)" } | Sort-Object) | Should -Be $before
|
||||
$script:FeatureLabelLookup['DisableTelemetry'] | Should -Be $checkbox.Content
|
||||
foreach ($category in $script:CategoryLanguageControls) {
|
||||
$category.Header.Text | Should -Be (Get-Translation -Key $category.CategoryId -Field 'Label' -Section 'Categories')
|
||||
}
|
||||
}
|
||||
$script:SelectionChanges | Should -Be 0
|
||||
}
|
||||
|
||||
It 'updates app display and search metadata while preserving removal IDs and selection' {
|
||||
$panel = [System.Windows.Controls.StackPanel]::new()
|
||||
$app = [System.Windows.Controls.CheckBox]::new()
|
||||
$app.IsChecked = $true
|
||||
$app.Tag = 'Microsoft.WindowsCalculator, Other.Id'
|
||||
$app | Add-Member -NotePropertyMembers @{
|
||||
AppIds = @('Microsoft.WindowsCalculator', 'Other.Id'); AppName = ''; AppDescription = ''
|
||||
AppNameText = [System.Windows.Controls.TextBlock]::new()
|
||||
AppDescriptionText = [System.Windows.Controls.TextBlock]::new()
|
||||
}
|
||||
[void]$panel.Children.Add($app)
|
||||
$script:JsonPresetCheckboxes = @()
|
||||
$script:PreloadedAppData = @('old catalog')
|
||||
|
||||
foreach ($language in @('nl-NL', 'pt-BR', 'en-US')) {
|
||||
$script:Lang = Import-LanguageFile -LanguageCode $language
|
||||
Update-MainWindowAppLanguage -AppsPanel $panel
|
||||
$app.AppNameText.Text | Should -Be (Get-Translation -Key 'Microsoft.WindowsCalculator' -Field 'FriendlyName' -Section 'Apps')
|
||||
$app.AppName | Should -Be $app.AppNameText.Text
|
||||
$app.AppDescriptionText.ToolTip | Should -Be $app.AppDescription
|
||||
$app.AppIds | Should -Be @('Microsoft.WindowsCalculator', 'Other.Id')
|
||||
$app.Tag | Should -Be 'Microsoft.WindowsCalculator, Other.Id'
|
||||
$app.IsChecked | Should -BeTrue
|
||||
}
|
||||
$script:PreloadedAppData | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'refreshes search and deployment text without changing user input or selections' {
|
||||
Mock Get-UserName { 'TestUser' }
|
||||
Mock Update-AppsPanelSort {}
|
||||
Mock Hide-Bubble {}
|
||||
$window = New-LanguageTestWindow
|
||||
New-DynamicTweakControls -Window $window -WinVersion 26100
|
||||
$appsPanel = [System.Windows.Controls.StackPanel]::new()
|
||||
$window.RegisterName('AppSelectionPanel', $appsPanel)
|
||||
[void]$window.Content.Children.Add($appsPanel)
|
||||
$app = [System.Windows.Controls.CheckBox]::new()
|
||||
$app.IsChecked = $true
|
||||
$app.Tag = 'Microsoft.WindowsCalculator'
|
||||
$app | Add-Member -NotePropertyMembers @{
|
||||
AppIds = @('Microsoft.WindowsCalculator'); AppName = 'Calculator'; AppDescription = ''
|
||||
AppNameText = [System.Windows.Controls.TextBlock]::new()
|
||||
AppDescriptionText = [System.Windows.Controls.TextBlock]::new()
|
||||
}
|
||||
[void]$appsPanel.Children.Add($app)
|
||||
foreach ($name in @('UserSelectionDescription', 'UsernameValidationMessage', 'AppSelectionStatus', 'AppRemovalScopeDescription', 'AppSearchPlaceholder', 'TweakSearchPlaceholder')) {
|
||||
$window.RegisterName($name, [System.Windows.Controls.TextBlock]::new())
|
||||
}
|
||||
foreach ($name in @('OtherUsernameTextBox', 'AppSearchBox', 'TweakSearchBox')) {
|
||||
$window.RegisterName($name, [System.Windows.Controls.TextBox]::new())
|
||||
}
|
||||
$userCombo = [System.Windows.Controls.ComboBox]::new()
|
||||
foreach ($label in @('Current', 'Other', 'Default')) {
|
||||
[void]$userCombo.Items.Add([System.Windows.Controls.ComboBoxItem]::new())
|
||||
}
|
||||
$userCombo.SelectedIndex = 1
|
||||
$window.RegisterName('UserSelectionCombo', $userCombo)
|
||||
$window.FindName('OtherUsernameTextBox').Text = 'Alice'
|
||||
$scope = [System.Windows.Controls.ComboBox]::new()
|
||||
$target = [System.Windows.Controls.ComboBoxItem]::new()
|
||||
$target.Name = 'AppRemovalScopeTargetUser'
|
||||
[void]$scope.Items.Add($target)
|
||||
$scope.SelectedIndex = 0
|
||||
$window.RegisterName('AppRemovalScopeCombo', $scope)
|
||||
$window.RegisterName('AppRemovalScopeSection', [System.Windows.Controls.Border]::new())
|
||||
$tabs = [System.Windows.Controls.TabControl]::new()
|
||||
foreach ($label in @('Home', 'Apps', 'Tweaks', 'Deploy')) { [void]$tabs.Items.Add($label) }
|
||||
$tabs.SelectedIndex = 2
|
||||
$window.RegisterName('MainTabControl', $tabs)
|
||||
$appSearchBox = $window.FindName('AppSearchBox')
|
||||
$appSearchPlaceholder = $window.FindName('AppSearchPlaceholder')
|
||||
$tweakSearchBox = $window.FindName('TweakSearchBox')
|
||||
$tweakSearchPlaceholder = $window.FindName('TweakSearchPlaceholder')
|
||||
$tweaksScrollViewer = $null
|
||||
$appSearchBox.Text = 'rekenmachine'
|
||||
$tweakSearchBox.Text = 'telemetrie'
|
||||
$appSearchBox.Add_TextChanged($script:SearchHandlers['appSearchBox'])
|
||||
$tweakSearchBox.Add_TextChanged($script:SearchHandlers['tweakSearchBox'])
|
||||
$script:AppSearchMatches = @(); $script:AppSearchMatchIndex = -1
|
||||
$script:JsonPresetCheckboxes = @()
|
||||
$script:SortColumn = 'Name'; $script:SortAscending = $true
|
||||
$window.Resources['SearchHighlightColor'] = [System.Windows.Media.Brushes]::Yellow
|
||||
$window.Resources['SearchHighlightActiveColor'] = [System.Windows.Media.Brushes]::Orange
|
||||
$checkbox = $window.FindName('Feature_DisableTelemetry_Combo')
|
||||
$checkbox.IsChecked = $true
|
||||
|
||||
$script:Lang = Import-LanguageFile -LanguageCode 'nl-NL'
|
||||
Update-MainWindowLanguage -Window $window
|
||||
|
||||
$userCombo.SelectedIndex | Should -Be 1
|
||||
$scope.SelectedItem | Should -Be $target
|
||||
$window.FindName('OtherUsernameTextBox').Text | Should -Be 'Alice'
|
||||
$tabs.SelectedIndex | Should -Be 2
|
||||
$appSearchBox.Text | Should -Be 'rekenmachine'
|
||||
$tweakSearchBox.Text | Should -Be 'telemetrie'
|
||||
$script:AppSearchMatches.Count | Should -Be 1
|
||||
[object]::ReferenceEquals($script:AppSearchMatches[0], $app) | Should -BeTrue
|
||||
$checkbox.Background | Should -Be ([System.Windows.Media.Brushes]::Yellow)
|
||||
$checkbox.IsChecked | Should -BeTrue
|
||||
$app.IsChecked | Should -BeTrue
|
||||
$window.FindName('AppSelectionStatus').Text | Should -Be (Get-Translation -Key 'AppSelectionStatusSelected' -Count 1 -FormatArgs @(1))
|
||||
$window.FindName('UserSelectionDescription').Text | Should -Be (Get-Translation -Key 'DeployUserDescriptionOtherUserNamed' -FormatArgs @('Alice'))
|
||||
$window.FindName('AppRemovalScopeDescription').Text | Should -Be (Get-Translation -Key 'AppRemovalScopeDescriptionTargetUser')
|
||||
$script:UpdatingLanguage | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Settings update the same window after Save' {
|
||||
BeforeEach {
|
||||
$script:IsLoadingApps = $false
|
||||
$script:UiPreferences = @{ Theme = 'Auto'; Language = 'en-US' }
|
||||
$script:Lang = [PSCustomObject]@{ LanguageCode = 'en-US' }
|
||||
Mock Set-WindowThemeResources {}
|
||||
Mock Update-MainWindowLanguage {}
|
||||
$script:WindowClosed = $false
|
||||
$script:WindowUnderTest = New-LanguageTestWindow
|
||||
$script:WindowUnderTest.Add_Closed({ $script:WindowClosed = $true })
|
||||
}
|
||||
|
||||
It 'updates a saved theme without changing language or closing the window' {
|
||||
Mock Show-SettingsDialog { $script:UiPreferences.Theme = 'Dark'; return $true }
|
||||
Open-MainWindowSettings -Window $script:WindowUnderTest
|
||||
Should -Invoke Set-WindowThemeResources -Times 1 -Exactly -ParameterFilter { $usesDarkMode -eq $true }
|
||||
Should -Invoke Update-MainWindowLanguage -Times 0 -Exactly
|
||||
$script:WindowClosed | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'updates a saved language without closing the window' {
|
||||
Mock Show-SettingsDialog {
|
||||
$script:Lang = [PSCustomObject]@{ LanguageCode = 'nl-NL' }
|
||||
$script:UiPreferences.Theme = 'Dark'
|
||||
return $true
|
||||
}
|
||||
Open-MainWindowSettings -Window $script:WindowUnderTest
|
||||
Should -Invoke Update-MainWindowLanguage -Times 1 -Exactly -ParameterFilter { [object]::ReferenceEquals($Window, $script:WindowUnderTest) }
|
||||
Should -Invoke Set-WindowThemeResources -Times 1 -Exactly -ParameterFilter { $usesDarkMode -eq $true }
|
||||
$script:WindowClosed | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'does not update presentation when the settings dialog is cancelled' {
|
||||
Mock Show-SettingsDialog { $false }
|
||||
Open-MainWindowSettings -Window $script:WindowUnderTest
|
||||
Should -Invoke Set-WindowThemeResources -Times 0 -Exactly
|
||||
Should -Invoke Update-MainWindowLanguage -Times 0 -Exactly
|
||||
$script:WindowClosed | Should -BeFalse
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
BeforeAll {
|
||||
$script:RepoRoot = Split-Path $PSScriptRoot -Parent
|
||||
. "$script:RepoRoot/Scripts/GUI/Ui-Preferences.ps1"
|
||||
. "$script:RepoRoot/Scripts/GUI/Get-SystemUsesDarkMode.ps1"
|
||||
. "$script:RepoRoot/Scripts/FileIO/Import-JsonFile.ps1"
|
||||
. "$script:RepoRoot/Scripts/FileIO/Import-LanguageFile.ps1"
|
||||
$script:LanguagesPath = Join-Path $script:RepoRoot 'Config/Languages'
|
||||
}
|
||||
|
||||
Describe 'UI preference initialization' {
|
||||
BeforeEach {
|
||||
$script:UiPreferencesFilePath = "$TestDrive/preferences.json"
|
||||
Save-UiPreferences -Preferences @{ Theme = 'Dark'; Language = 'nl-NL' }
|
||||
}
|
||||
|
||||
It 'loads the saved language and theme' {
|
||||
Initialize-UiPreferences
|
||||
$script:UiPreferences.Theme | Should -Be 'Dark'
|
||||
$script:UiPreferences.Language | Should -Be 'nl-NL'
|
||||
$script:Lang.LanguageCode | Should -Be 'nl-NL'
|
||||
}
|
||||
|
||||
It 'honors an explicit language without persisting the override' {
|
||||
Initialize-UiPreferences -Language 'pt-BR'
|
||||
$script:UiPreferences.Language | Should -Be 'pt-BR'
|
||||
$script:Lang.LanguageCode | Should -Be 'pt-BR'
|
||||
(Get-UiPreferences).Language | Should -Be 'nl-NL'
|
||||
}
|
||||
|
||||
It 'keeps Auto as a preference while resolving the system language' {
|
||||
Initialize-UiPreferences -Language 'Auto'
|
||||
$script:UiPreferences.Language | Should -Be 'Auto'
|
||||
$script:Lang.LanguageCode | Should -Be (Import-LanguageFile).LanguageCode
|
||||
}
|
||||
|
||||
It 'normalizes <Language> to the resolved folder' -ForEach @(
|
||||
@{ Language = 'nl-BE'; Expected = 'nl-NL' }
|
||||
@{ Language = 'fr-FR'; Expected = 'en-US' }
|
||||
) {
|
||||
Initialize-UiPreferences -Language $Language
|
||||
$script:UiPreferences.Language | Should -Be $Expected
|
||||
$script:Lang.LanguageCode | Should -Be $Expected
|
||||
}
|
||||
|
||||
It 'leaves language failure handling to the caller' {
|
||||
Mock Get-UiLanguage { $null }
|
||||
Initialize-UiPreferences
|
||||
$script:Lang | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'UI preference persistence and theme resolution' {
|
||||
It 'defaults to system settings when no preferences exist' {
|
||||
$result = Get-UiPreferences -Path "$TestDrive/missing.json"
|
||||
$result.Theme | Should -Be 'Auto'
|
||||
$result.Language | Should -Be 'Auto'
|
||||
}
|
||||
It 'round trips explicit preferences' {
|
||||
Save-UiPreferences -Preferences @{Theme='Dark'; Language='pt-BR'} -Path "$TestDrive/preferences.json"
|
||||
$result = Get-UiPreferences -Path "$TestDrive/preferences.json"
|
||||
$result.Theme | Should -Be 'Dark'
|
||||
$result.Language | Should -Be 'pt-BR'
|
||||
}
|
||||
It 'ignores unsupported preferences' {
|
||||
'{"Theme":"Unknown","Language":"../other"}' | Set-Content "$TestDrive/invalid.json"
|
||||
$result = Get-UiPreferences -Path "$TestDrive/invalid.json"
|
||||
$result.Theme | Should -Be 'Auto'
|
||||
$result.Language | Should -Be 'Auto'
|
||||
}
|
||||
It 'keeps the previous settings when writing fails' {
|
||||
Save-UiPreferences -Preferences @{Theme='Light'; Language='en-US'} -Path "$TestDrive/safe.json"
|
||||
Mock Set-Content { throw 'Disk full' }
|
||||
{ Save-UiPreferences -Preferences @{Theme='Dark'; Language='pt-BR'} -Path "$TestDrive/safe.json" } | Should -Throw
|
||||
(Get-UiPreferences -Path "$TestDrive/safe.json").Theme | Should -Be 'Light'
|
||||
}
|
||||
It 'honors explicit themes and resolves Auto from Windows' {
|
||||
Mock Get-SystemUsesDarkMode { $true }
|
||||
$script:UiPreferences = @{Theme='Light'}
|
||||
Get-AppUsesDarkMode | Should -BeFalse
|
||||
$script:UiPreferences.Theme = 'Dark'
|
||||
Get-AppUsesDarkMode | Should -BeTrue
|
||||
$script:UiPreferences.Theme = 'Auto'
|
||||
Get-AppUsesDarkMode | Should -BeTrue
|
||||
Should -Invoke Get-SystemUsesDarkMode -Times 1 -Exactly
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -184,6 +184,7 @@ $script:AppsListFilePath = Join-Path $configPath 'Apps.json'
|
||||
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
|
||||
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
|
||||
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
|
||||
$script:UiPreferencesFilePath = Join-Path $env:LOCALAPPDATA 'Win11Debloat\Preferences.json'
|
||||
$script:LanguagesPath = Join-Path $configPath 'Languages'
|
||||
$script:DefaultLanguagePath = Join-Path $script:LanguagesPath 'en-US'
|
||||
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
|
||||
@@ -405,6 +406,9 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
|
||||
# GUI functions
|
||||
. "$PSScriptRoot/Scripts/GUI/Get-SystemUsesDarkMode.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Ui-Preferences.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/MainWindow-Localization.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-SettingsDialog.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Set-WindowThemeResources.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Attach-ShiftClickBehavior.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Apply-SettingsToUiControls.ps1"
|
||||
@@ -465,9 +469,8 @@ $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\Current
|
||||
# Check if the machine supports Modern Standby, this is used to determine if the DisableModernStandbyNetworking option can be used
|
||||
$script:ModernStandbySupported = Test-ModernStandbySupport
|
||||
|
||||
# Load the GUI's language content. -Language overrides the current user's system UI culture,
|
||||
# falling back to en-US either way if the requested language isn't available.
|
||||
$script:Lang = if ($Language) { Import-LanguageFile -LanguageCode $Language } else { Import-LanguageFile }
|
||||
# Explicit -Language takes precedence over saved UI preferences and the system culture.
|
||||
Initialize-UiPreferences -Language $Language
|
||||
|
||||
$script:Params = $PSBoundParameters
|
||||
$script:UndoParams = @{}
|
||||
|
||||
Reference in New Issue
Block a user