Add settings UI for theme and language (#780)

This commit is contained in:
Jeffrey
2026-10-04 22:43:56 +02:00
committed by GitHub
parent 680f21ccfa
commit 0bc0c9a49e
27 changed files with 1216 additions and 144 deletions
+9
View File
@@ -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",
+9
View File
@@ -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",
+1 -1
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
{}
+9
View File
@@ -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
View File
@@ -363,6 +363,7 @@
</TextBlock>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xEB52;" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="%LANG:TitleBarSupportCreator%" AutomationProperties.Name="%LANG:TitleBarSupportCreator%"/>
<Button x:Name="SettingsBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE713;" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="%LANG:SettingsTitle%" AutomationProperties.Name="%LANG:SettingsTitle%"/>
<Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE700;" 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="&#xF16A;" 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>
+71
View File
@@ -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="&#xE8BB;" 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="&#xE770;" 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="&#xE706;" 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="&#xE708;" 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>
+24
View File
@@ -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="&#xF16A;"/>
<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}"/>
+62 -23
View File
@@ -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)
}
+1 -1
View File
@@ -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
+11 -4
View File
@@ -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
}
+147
View File
@@ -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
}
+132 -71
View File
@@ -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 {
+3 -1
View File
@@ -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()
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 }
+35 -18
View File
@@ -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
}
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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
+92
View File
@@ -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' } }
}
+157
View File
@@ -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
}
}
}
+28
View File
@@ -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*'
}
}
+15
View File
@@ -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' {
+310
View File
@@ -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
}
}
+86
View File
@@ -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
View File
@@ -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 = @{}