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
+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
}
}
}