Skip to content

Update add somme function and GUI tools#10

Open
JM2K69 wants to merge 2 commits intoNicolasBn:masterfrom
JM2K69:master
Open

Update add somme function and GUI tools#10
JM2K69 wants to merge 2 commits intoNicolasBn:masterfrom
JM2K69:master

Conversation

@JM2K69
Copy link

@JM2K69 JM2K69 commented May 19, 2025

Requirements

  • This template is required. Any request that does not include enough information may be closed at the maintainers' discretion.
  • Have you (put an X between the brackets on each line to confirm):
    • Written new test cases to ensure no regression bugs occur?
    • Ensured all test cases are now passing?
    • Ensured that PowerShell Script Analyser issues and warnings are completely resolved?
    • Updated any help or documentation that may be impacted by your changes?

Description of the Change

[ We must be able to understand the design of your change from this description. If we cannot get a good idea of what the code will be doing from the description here, the pull request may be closed at the maintainers' discretion. Keep in mind that the maintainer reviewing this PR may not be familiar with or have worked with the code here recently, so please walk us through the concepts. ]

Testing

[ Please describe the testing you have performed ]

Associated/Resolved Issues

[ Enter any applicable issues here ]

Note: By creating a pull request, you are expected to comply with this project's Code of Conduct.

Copilot AI review requested due to automatic review settings December 30, 2025 14:56
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a new GUI-based GPO Policy Manager and updates existing functions in the PSGPOTools module. While it introduces significant new functionality, there are several critical bugs and maintainability issues that should be addressed before merging.

Key Changes:

  • Added a Scope parameter to Get-PSGPOPolicy function to filter policies by User or Machine scope
  • Enhanced Initialize-PSGPOAdmx with improved formatting and comments
  • Introduced a new Show-GPOManager function providing a WPF-based graphical interface for browsing and managing GPO policies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

$scopeItem.Tag = $scope

$policies = Get-PSGPOPolicy -Scope $scope
if ($Filter -and $Filter -ne '' -and $Filter -ne 'Rechercher une GPO...') {
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Populate-TreeView function, the filter condition checks three different conditions for the same value which is redundant. The condition "$Filter -and $Filter -ne '' -and $Filter -ne 'Rechercher une GPO...'" can be simplified since the placeholder check already handles the empty string case. Consider simplifying to just check if the filter is meaningful.

Suggested change
if ($Filter -and $Filter -ne '' -and $Filter -ne 'Rechercher une GPO...') {
if (-not [string]::IsNullOrWhiteSpace($Filter) -and $Filter -ne 'Rechercher une GPO...') {

Copilot uses AI. Check for mistakes.
Comment on lines +1098 to +1168
function Set-Theme {
param([bool]$dark)

$resources = $window.Resources

if ($dark) {
# Dark Theme - Improved colors
$resources['PrimaryBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0086F0')
$resources['PrimaryHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#3399FF')
$resources['PrimaryPressedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0066CC')
$resources['AccentBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#00D4FF')
$resources['BackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E1E1E')
$resources['SecondaryBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#2D2D30')
$resources['BorderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#3F3F46')
$resources['TextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['SecondaryTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#CCCCCC')
$resources['TreeViewSelectedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0086F0')
$resources['TreeViewSelectedTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['TreeViewHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#2D2D30')
$resources['IconFolderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFD700')
$resources['IconFileBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#64B5F6')
$resources['IconScopeBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#81C784')
$resources['HeaderBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
$resources['HeaderTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')

$window.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E1E1E')
$themeToggleButton.Content = '☀️ Mode Clair'

# Update header backgrounds
$treeHeaderBorder = $window.FindName("treeHeaderBorder")
$detailsHeaderBorder = $window.FindName("detailsHeaderBorder")
if ($treeHeaderBorder) {
$treeHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
}
if ($detailsHeaderBorder) {
$detailsHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
}
} else {
# Light Theme
$resources['PrimaryBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['PrimaryHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#106EBE')
$resources['PrimaryPressedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#005A9E')
$resources['AccentBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#00BCF2')
$resources['BackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['SecondaryBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F5F5F5')
$resources['BorderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#E1E1E1')
$resources['TextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1F1F1F')
$resources['SecondaryTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#605E5C')
$resources['TreeViewSelectedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['TreeViewSelectedTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['TreeViewHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
$resources['IconFolderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FDB900')
$resources['IconFileBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['IconScopeBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#107C10')
$resources['HeaderBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
$resources['HeaderTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1F1F1F')

$window.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F5F5F5')
$themeToggleButton.Content = '🌙 Mode Sombre'

# Update header backgrounds
$treeHeaderBorder = $window.FindName("treeHeaderBorder")
$detailsHeaderBorder = $window.FindName("detailsHeaderBorder")
if ($treeHeaderBorder) {
$treeHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
}
if ($detailsHeaderBorder) {
$detailsHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
}
}
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested Set-Theme function is defined inside Show-GPOManager but lacks any documentation. The function takes a boolean parameter but doesn't document what it does or what the parameter means. Adding inline documentation would improve code maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +1098 to +1399
function Set-Theme {
param([bool]$dark)

$resources = $window.Resources

if ($dark) {
# Dark Theme - Improved colors
$resources['PrimaryBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0086F0')
$resources['PrimaryHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#3399FF')
$resources['PrimaryPressedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0066CC')
$resources['AccentBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#00D4FF')
$resources['BackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E1E1E')
$resources['SecondaryBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#2D2D30')
$resources['BorderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#3F3F46')
$resources['TextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['SecondaryTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#CCCCCC')
$resources['TreeViewSelectedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0086F0')
$resources['TreeViewSelectedTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['TreeViewHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#2D2D30')
$resources['IconFolderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFD700')
$resources['IconFileBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#64B5F6')
$resources['IconScopeBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#81C784')
$resources['HeaderBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
$resources['HeaderTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')

$window.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E1E1E')
$themeToggleButton.Content = '☀️ Mode Clair'

# Update header backgrounds
$treeHeaderBorder = $window.FindName("treeHeaderBorder")
$detailsHeaderBorder = $window.FindName("detailsHeaderBorder")
if ($treeHeaderBorder) {
$treeHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
}
if ($detailsHeaderBorder) {
$detailsHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
}
} else {
# Light Theme
$resources['PrimaryBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['PrimaryHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#106EBE')
$resources['PrimaryPressedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#005A9E')
$resources['AccentBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#00BCF2')
$resources['BackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['SecondaryBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F5F5F5')
$resources['BorderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#E1E1E1')
$resources['TextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1F1F1F')
$resources['SecondaryTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#605E5C')
$resources['TreeViewSelectedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['TreeViewSelectedTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['TreeViewHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
$resources['IconFolderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FDB900')
$resources['IconFileBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['IconScopeBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#107C10')
$resources['HeaderBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
$resources['HeaderTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1F1F1F')

$window.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F5F5F5')
$themeToggleButton.Content = '🌙 Mode Sombre'

# Update header backgrounds
$treeHeaderBorder = $window.FindName("treeHeaderBorder")
$detailsHeaderBorder = $window.FindName("detailsHeaderBorder")
if ($treeHeaderBorder) {
$treeHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
}
if ($detailsHeaderBorder) {
$detailsHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
}
}
}

$themeToggleButton.Add_Click({
$script:isDarkTheme = -not $script:isDarkTheme
Set-Theme -dark $script:isDarkTheme
})

# Get context menu items from XAML
$enableMenuItem = $window.FindName("enableMenuItem")
$disableMenuItem = $window.FindName("disableMenuItem")
$createGpoMenuItem = $window.FindName("createGpoMenuItem")

# Add click handlers to menu items
if ($enableMenuItem) {
$enableMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Enable: $($selectedItem.Tag.DisplayName)"
}
})
}

if ($disableMenuItem) {
$disableMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Disable: $($selectedItem.Tag.DisplayName)"
}
})
}

if ($createGpoMenuItem) {
$createGpoMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Create GPO for: $($selectedItem.Tag.DisplayName)"
}
})
}

$displayNameTextBox = $window.FindName("displayNameTextBox")
$descriptionTextBox = $window.FindName("descriptionTextBox")
$scopeTextBox = $window.FindName("scopeTextBox")
$registryPathTextBox = $window.FindName("registryPathTextBox")
$registryKeyTextBox = $window.FindName("registryKeyTextBox")
$registryValuesGrid = $window.FindName("registryValuesGrid")
$searchTextBox = $window.FindName("searchTextBox")
$searchButton = $window.FindName("searchButton")
$breadcrumbTextBlock = $window.FindName("breadcrumbTextBlock")

$treeView.add_SelectedItemChanged({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -is [Windows.Controls.TreeViewItem]) {
$policy = $selectedItem.Tag

# Build breadcrumb path
$breadcrumbPath = ""
$currentItem = $selectedItem
$pathParts = @()

while ($currentItem -ne $null) {
if ($currentItem.Tag -is [GPOToolsPolicy]) {
$pathParts = @($currentItem.Tag.DisplayName) + $pathParts
} elseif ($currentItem.Tag -is [GPOToolsCategory]) {
$pathParts = @($currentItem.Tag.DisplayName) + $pathParts
} elseif ($currentItem.Tag -is [ScopePolicy]) {
$pathParts = @($currentItem.Tag.ToString()) + $pathParts
}
$currentItem = $currentItem.Parent
if ($currentItem -isnot [Windows.Controls.TreeViewItem]) {
break
}
}

$breadcrumbPath = $pathParts -join ' > '
if ($breadcrumbPath) {
$breadcrumbTextBlock.Text = "📍 $breadcrumbPath"
} else {
$breadcrumbTextBlock.Text = "Select a policy to see its path"
}

if ($policy -is [GPOToolsPolicy]) {
$displayNameTextBox.Text = $policy.DisplayName
$descriptionTextBox.Text = if ($policy.Description) { $policy.Description } else { "(Aucune description disponible)" }
$scopeTextBox.Text = $policy.Scope.ToString()

# Handle registry information with null checks
if ($policy.Registry) {
$registryPathTextBox.Text = if ($policy.Registry.Path) { $policy.Registry.Path } else { "(Aucun chemin de registre défini)" }
$registryKeyTextBox.Text = if ($policy.Registry.Key) { $policy.Registry.Key } else { "(Aucune clé de registre définie)" }

# Populate registry values in the DataGrid
if ($policy.Registry.Value -and $policy.Registry.Value.Keys.Count -gt 0) {
$registryValuesGrid.ItemsSource = @(
foreach ($key in $policy.Registry.Value.Keys) {
[PSCustomObject]@{
Key = $key
Value = if ($policy.Registry.Value[$key]) { $policy.Registry.Value[$key] } else { "(Valeur non définie)" }
}
}
)
} else {
$registryValuesGrid.ItemsSource = @(
[PSCustomObject]@{
Key = "Information"
Value = "(Aucune valeur de registre disponible pour cette GPO)"
}
)
}
} else {
$registryPathTextBox.Text = "(Cette GPO ne nécessite pas de configuration de registre)"
$registryKeyTextBox.Text = "(Aucune clé de registre)"
$registryValuesGrid.ItemsSource = @(
[PSCustomObject]@{
Key = "Information"
Value = "(Cette policy ne stocke pas de données dans le registre)"
}
)
}
} else {
$displayNameTextBox.Text = ""
$descriptionTextBox.Text = ""
$scopeTextBox.Text = ""
$registryPathTextBox.Text = ""
$registryKeyTextBox.Text = ""
$registryValuesGrid.ItemsSource = $null
}
}
})

function Populate-TreeView {
param(
[string]$Filter = ''
)
$treeView.Items.Clear()
$scopes = [Enum]::GetValues([ScopePolicy])
foreach ($scope in $scopes) {
$scopeItem = New-Object Windows.Controls.TreeViewItem

# Create StackPanel for icon + text
$scopePanel = New-Object Windows.Controls.StackPanel
$scopePanel.Orientation = 'Horizontal'

$scopeIcon = New-Object Windows.Controls.TextBlock
$scopeIcon.Text = '📋 '
$scopeIcon.FontSize = 14
$scopeIcon.Foreground = $window.Resources['IconScopeBrush']

$scopeText = New-Object Windows.Controls.TextBlock
$scopeText.Text = $scope.ToString()
$scopeText.FontWeight = 'SemiBold'

$scopePanel.Children.Add($scopeIcon) | Out-Null
$scopePanel.Children.Add($scopeText) | Out-Null
$scopeItem.Header = $scopePanel
$scopeItem.Tag = $scope

$policies = Get-PSGPOPolicy -Scope $scope
if ($Filter -and $Filter -ne '' -and $Filter -ne 'Rechercher une GPO...') {
$policies = $policies | Where-Object { $_.DisplayName -like "*$Filter*" }
}

foreach ($policy in $policies) {
$category = $policy.Category
$parentCategory = $category.ParentCategory
$currentItem = $scopeItem

while ($null -ne $parentCategory) {
# Check if category already exists
$existingItem = $null
foreach ($item in $currentItem.Items) {
if ($item.Tag -and $item.Tag.DisplayName -eq $parentCategory.DisplayName) {
$existingItem = $item
break
}
}

if (-not $existingItem) {
$newItem = New-Object Windows.Controls.TreeViewItem

# Create StackPanel for folder icon + text
$folderPanel = New-Object Windows.Controls.StackPanel
$folderPanel.Orientation = 'Horizontal'

$folderIcon = New-Object Windows.Controls.TextBlock
$folderIcon.Text = '📂 '
$folderIcon.FontSize = 13
$folderIcon.Foreground = $window.Resources['IconFolderBrush']

$folderText = New-Object Windows.Controls.TextBlock
$folderText.Text = $parentCategory.DisplayName

$folderPanel.Children.Add($folderIcon) | Out-Null
$folderPanel.Children.Add($folderText) | Out-Null
$newItem.Header = $folderPanel
$newItem.Tag = $parentCategory

$currentItem.Items.Add($newItem) | Out-Null
$currentItem = $newItem
} else {
$currentItem = $existingItem
}
$parentCategory = $parentCategory.ParentCategory
}

$policyItem = New-Object Windows.Controls.TreeViewItem

# Create StackPanel for file icon + text
$policyPanel = New-Object Windows.Controls.StackPanel
$policyPanel.Orientation = 'Horizontal'

$policyIcon = New-Object Windows.Controls.TextBlock
$policyIcon.Text = '📄 '
$policyIcon.FontSize = 13
$policyIcon.Foreground = $window.Resources['IconFileBrush']

$policyText = New-Object Windows.Controls.TextBlock
$policyText.Text = $policy.DisplayName

$policyPanel.Children.Add($policyIcon) | Out-Null
$policyPanel.Children.Add($policyText) | Out-Null
$policyItem.Header = $policyPanel
$policyItem.Tag = $policy

$currentItem.Items.Add($policyItem) | Out-Null
}

if ($scopeItem.Items.Count -gt 0) {
$treeView.Items.Add($scopeItem) | Out-Null
}
}
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function Populate-TreeView and the theme toggle functionality access and modify variables from the outer scope without proper scoping. The click handlers and nested functions rely on closure over variables like $treeView, $window, etc. While PowerShell supports this, explicitly documenting or structuring this dependency would improve maintainability. Consider using a more structured approach with a class or explicitly passing required references.

Copilot uses AI. Check for mistakes.
# Sequential processing to load ADMX and ADML files
[GPOToolsUtility]::InitiateAdmxAdml($Item, $UICulture)
}

Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Show-GPOManager function lacks any documentation. There is no comment-based help, no description of what the function does, its purpose, or usage examples. This is especially important for a complex GUI function that introduces significant new functionality. Consider adding proper PowerShell comment-based help with .SYNOPSIS, .DESCRIPTION, .EXAMPLE, etc.

Suggested change
<#
.SYNOPSIS
Displays the PSGPOTools GPO Manager graphical user interface.
.DESCRIPTION
Show-GPOManager opens a WPF-based graphical user interface that allows you to
browse, search, and inspect Group Policy settings and related information
loaded by Initialize-PSGPOAdmx. The GUI uses the ADMX/ADML data parsed and
cached by GPOToolsUtility and provides an interactive way to explore policies,
categories, and supported platforms.
This command requires a Windows environment with GUI capabilities because it
relies on WPF assemblies (PresentationFramework, PresentationCore, WindowsBase).
For accurate data, run Initialize-PSGPOAdmx prior to launching the manager so
that ADMX/ADML definitions are loaded.
.EXAMPLE
PS C:\> Show-GPOManager
Launches the GPO Manager UI using the ADMX/ADML data previously initialized
with Initialize-PSGPOAdmx.
.EXAMPLE
PS C:\> Initialize-PSGPOAdmx -Path "C:\Windows\PolicyDefinitions"
PS C:\> Show-GPOManager
Initializes the ADMX/ADML definitions from the specified PolicyDefinitions
directory and then opens the GPO Manager GUI to browse those settings.
.NOTES
The window is created using Windows Presentation Foundation (WPF). Ensure that
you are running in a PowerShell session that supports WPF (e.g. PowerShell
running on Windows with access to a desktop session).
#>

Copilot uses AI. Check for mistakes.
Comment on lines +1298 to +1301
function Populate-TreeView {
param(
[string]$Filter = ''
)
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Populate-TreeView function lacks documentation explaining its purpose, the Filter parameter, and its behavior. This is a complex function with significant logic that would benefit from clear documentation.

Copilot uses AI. Check for mistakes.
Comment on lines +927 to +1283
Content='🌙 Mode Sombre'
Height='36'
Width='140'
Style='{StaticResource ModernButton}'
Margin='0,0,12,0'/>
<TextBox Name='searchTextBox'
Width='320'
Height='36'
Style='{StaticResource ModernTextBox}'
VerticalContentAlignment='Center'
Margin='0,0,12,0'
Text='Rechercher une GPO...'
Foreground='#999999'/>
<Button Name='searchButton'
Content='🔍 Rechercher'
Height='36'
Width='130'
Style='{StaticResource ModernButton}'/>
</StackPanel>
</Grid>
</Border>

<!-- Main Content Area with Splitter -->
<Grid Grid.Row='1' Margin='0'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='2*' MinWidth='250'/>
<ColumnDefinition Width='Auto'/>
<ColumnDefinition Width='3*' MinWidth='300'/>
</Grid.ColumnDefinitions>

<!-- TreeView Panel -->
<Border Grid.Column='0'
Background='{DynamicResource BackgroundBrush}'
Margin='16,16,8,16'
CornerRadius='8'
BorderThickness='1'
BorderBrush='{DynamicResource BorderBrush}'>
<Border.Effect>
<DropShadowEffect BlurRadius='10' ShadowDepth='2' Opacity='0.1' Color='Black'/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height='Auto'/>
<RowDefinition Height='*'/>
</Grid.RowDefinitions>

<Border Grid.Row='0'
Name='treeHeaderBorder'
Background='{DynamicResource HeaderBackgroundBrush}'
Padding='16,12'
CornerRadius='8,8,0,0'>
<StackPanel>
<TextBlock Name='treeTitleTextBlock'
Text='📁 Policy Structure'
FontSize='15'
FontWeight='SemiBold'
Foreground='{DynamicResource HeaderTextBrush}'/>
<TextBlock Name='breadcrumbTextBlock'
Text='Select a policy to see its path'
FontSize='11'
Foreground='{DynamicResource SecondaryTextBrush}'
Margin='0,4,0,0'
TextTrimming='CharacterEllipsis'/>
</StackPanel>
</Border>

<TreeView Name='treeView' Grid.Row='1' Margin='8'>
<TreeView.ContextMenu>
<ContextMenu Name='treeViewContextMenu'>
<MenuItem Name='enableMenuItem' Header='✅ Enable this parameter'/>
<MenuItem Name='disableMenuItem' Header='❌ Disable this parameter'/>
<Separator/>
<MenuItem Name='createGpoMenuItem' Header='➕ Create a GPO'/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</Grid>
</Border>

<!-- Splitter -->
<GridSplitter Grid.Column='1'
Width='6'
HorizontalAlignment='Center'
VerticalAlignment='Stretch'
Background='Transparent'
ShowsPreview='True'/>

<!-- Details Panel -->
<Border Grid.Column='2'
Background='{DynamicResource BackgroundBrush}'
Margin='8,16,16,16'
CornerRadius='8'
BorderThickness='1'
BorderBrush='{DynamicResource BorderBrush}'>
<Border.Effect>
<DropShadowEffect BlurRadius='10' ShadowDepth='2' Opacity='0.1' Color='Black'/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height='Auto'/>
<RowDefinition Height='*'/>
</Grid.RowDefinitions>

<Border Grid.Row='0'
Name='detailsHeaderBorder'
Background='{DynamicResource HeaderBackgroundBrush}'
Padding='16,12'
CornerRadius='8,8,0,0'>
<TextBlock Name='detailsTitleTextBlock'
Text='ℹ️ Policy Details'
FontSize='15'
FontWeight='SemiBold'
Foreground='{DynamicResource HeaderTextBrush}'/>
</Border>

<ScrollViewer Grid.Row='1'
VerticalScrollBarVisibility='Auto'
Padding='16'>
<StackPanel Name='detailsPanel'>
<TextBlock Text='Display Name' Style='{StaticResource LabelStyle}'/>
<TextBox Name='displayNameTextBox' Style='{StaticResource ReadOnlyTextBox}'/>

<TextBlock Text='Description' Style='{StaticResource LabelStyle}' Margin='0,16,0,4'/>
<TextBox Name='descriptionTextBox'
Style='{StaticResource ReadOnlyTextBox}'
MinHeight='60'
MaxHeight='120'
VerticalScrollBarVisibility='Auto'/>

<TextBlock Text='Scope' Style='{StaticResource LabelStyle}' Margin='0,16,0,4'/>
<TextBox Name='scopeTextBox' Style='{StaticResource ReadOnlyTextBox}'/>

<TextBlock Text='Registry Path' Style='{StaticResource LabelStyle}' Margin='0,16,0,4'/>
<TextBox Name='registryPathTextBox' Style='{StaticResource ReadOnlyTextBox}'/>

<TextBlock Text='Registry Key' Style='{StaticResource LabelStyle}' Margin='0,16,0,4'/>
<TextBox Name='registryKeyTextBox' Style='{StaticResource ReadOnlyTextBox}'/>

<TextBlock Text='Registry Values' Style='{StaticResource LabelStyle}' Margin='0,16,0,4'/>
<DataGrid Name='registryValuesGrid'
AutoGenerateColumns='False'
IsReadOnly='True'
MaxHeight='200'
Margin='0,0,0,16'>
<DataGrid.Columns>
<DataGridTextColumn Header='Name' Binding='{Binding Key}' Width='*'/>
<DataGridTextColumn Header='Value' Binding='{Binding Value}' Width='2*'/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</Grid>
</Window>
"@

$xmlDocument = New-Object System.Xml.XmlDocument
$xmlDocument.LoadXml($xaml)
$reader = New-Object System.Xml.XmlNodeReader $xmlDocument
$window = [Windows.Markup.XamlReader]::Load($reader)

$treeView = $window.FindName("treeView")
$themeToggleButton = $window.FindName("themeToggleButton")
$treeTitleTextBlock = $window.FindName("treeTitleTextBlock")
$detailsTitleTextBlock = $window.FindName("detailsTitleTextBlock")

# Theme management
$isDarkTheme = $false

function Set-Theme {
param([bool]$dark)

$resources = $window.Resources

if ($dark) {
# Dark Theme - Improved colors
$resources['PrimaryBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0086F0')
$resources['PrimaryHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#3399FF')
$resources['PrimaryPressedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0066CC')
$resources['AccentBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#00D4FF')
$resources['BackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E1E1E')
$resources['SecondaryBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#2D2D30')
$resources['BorderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#3F3F46')
$resources['TextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['SecondaryTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#CCCCCC')
$resources['TreeViewSelectedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0086F0')
$resources['TreeViewSelectedTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['TreeViewHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#2D2D30')
$resources['IconFolderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFD700')
$resources['IconFileBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#64B5F6')
$resources['IconScopeBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#81C784')
$resources['HeaderBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
$resources['HeaderTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')

$window.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E1E1E')
$themeToggleButton.Content = '☀️ Mode Clair'

# Update header backgrounds
$treeHeaderBorder = $window.FindName("treeHeaderBorder")
$detailsHeaderBorder = $window.FindName("detailsHeaderBorder")
if ($treeHeaderBorder) {
$treeHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
}
if ($detailsHeaderBorder) {
$detailsHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#252526')
}
} else {
# Light Theme
$resources['PrimaryBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['PrimaryHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#106EBE')
$resources['PrimaryPressedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#005A9E')
$resources['AccentBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#00BCF2')
$resources['BackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['SecondaryBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F5F5F5')
$resources['BorderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#E1E1E1')
$resources['TextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1F1F1F')
$resources['SecondaryTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#605E5C')
$resources['TreeViewSelectedBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['TreeViewSelectedTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FFFFFF')
$resources['TreeViewHoverBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
$resources['IconFolderBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#FDB900')
$resources['IconFileBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#0078D4')
$resources['IconScopeBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#107C10')
$resources['HeaderBackgroundBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
$resources['HeaderTextBrush'] = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1F1F1F')

$window.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F5F5F5')
$themeToggleButton.Content = '🌙 Mode Sombre'

# Update header backgrounds
$treeHeaderBorder = $window.FindName("treeHeaderBorder")
$detailsHeaderBorder = $window.FindName("detailsHeaderBorder")
if ($treeHeaderBorder) {
$treeHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
}
if ($detailsHeaderBorder) {
$detailsHeaderBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#F3F2F1')
}
}
}

$themeToggleButton.Add_Click({
$script:isDarkTheme = -not $script:isDarkTheme
Set-Theme -dark $script:isDarkTheme
})

# Get context menu items from XAML
$enableMenuItem = $window.FindName("enableMenuItem")
$disableMenuItem = $window.FindName("disableMenuItem")
$createGpoMenuItem = $window.FindName("createGpoMenuItem")

# Add click handlers to menu items
if ($enableMenuItem) {
$enableMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Enable: $($selectedItem.Tag.DisplayName)"
}
})
}

if ($disableMenuItem) {
$disableMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Disable: $($selectedItem.Tag.DisplayName)"
}
})
}

if ($createGpoMenuItem) {
$createGpoMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Create GPO for: $($selectedItem.Tag.DisplayName)"
}
})
}

$displayNameTextBox = $window.FindName("displayNameTextBox")
$descriptionTextBox = $window.FindName("descriptionTextBox")
$scopeTextBox = $window.FindName("scopeTextBox")
$registryPathTextBox = $window.FindName("registryPathTextBox")
$registryKeyTextBox = $window.FindName("registryKeyTextBox")
$registryValuesGrid = $window.FindName("registryValuesGrid")
$searchTextBox = $window.FindName("searchTextBox")
$searchButton = $window.FindName("searchButton")
$breadcrumbTextBlock = $window.FindName("breadcrumbTextBlock")

$treeView.add_SelectedItemChanged({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -is [Windows.Controls.TreeViewItem]) {
$policy = $selectedItem.Tag

# Build breadcrumb path
$breadcrumbPath = ""
$currentItem = $selectedItem
$pathParts = @()

while ($currentItem -ne $null) {
if ($currentItem.Tag -is [GPOToolsPolicy]) {
$pathParts = @($currentItem.Tag.DisplayName) + $pathParts
} elseif ($currentItem.Tag -is [GPOToolsCategory]) {
$pathParts = @($currentItem.Tag.DisplayName) + $pathParts
} elseif ($currentItem.Tag -is [ScopePolicy]) {
$pathParts = @($currentItem.Tag.ToString()) + $pathParts
}
$currentItem = $currentItem.Parent
if ($currentItem -isnot [Windows.Controls.TreeViewItem]) {
break
}
}

$breadcrumbPath = $pathParts -join ' > '
if ($breadcrumbPath) {
$breadcrumbTextBlock.Text = "📍 $breadcrumbPath"
} else {
$breadcrumbTextBlock.Text = "Select a policy to see its path"
}

if ($policy -is [GPOToolsPolicy]) {
$displayNameTextBox.Text = $policy.DisplayName
$descriptionTextBox.Text = if ($policy.Description) { $policy.Description } else { "(Aucune description disponible)" }
$scopeTextBox.Text = $policy.Scope.ToString()

# Handle registry information with null checks
if ($policy.Registry) {
$registryPathTextBox.Text = if ($policy.Registry.Path) { $policy.Registry.Path } else { "(Aucun chemin de registre défini)" }
$registryKeyTextBox.Text = if ($policy.Registry.Key) { $policy.Registry.Key } else { "(Aucune clé de registre définie)" }

# Populate registry values in the DataGrid
if ($policy.Registry.Value -and $policy.Registry.Value.Keys.Count -gt 0) {
$registryValuesGrid.ItemsSource = @(
foreach ($key in $policy.Registry.Value.Keys) {
[PSCustomObject]@{
Key = $key
Value = if ($policy.Registry.Value[$key]) { $policy.Registry.Value[$key] } else { "(Valeur non définie)" }
}
}
)
} else {
$registryValuesGrid.ItemsSource = @(
[PSCustomObject]@{
Key = "Information"
Value = "(Aucune valeur de registre disponible pour cette GPO)"
}
)
}
} else {
$registryPathTextBox.Text = "(Cette GPO ne nécessite pas de configuration de registre)"
$registryKeyTextBox.Text = "(Aucune clé de registre)"
$registryValuesGrid.ItemsSource = @(
[PSCustomObject]@{
Key = "Information"
Value = "(Cette policy ne stocke pas de données dans le registre)"
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains French text mixed with English. Text strings in lines 927, 938, 941, and throughout the GUI use French ("Mode Sombre", "Mode Clair", "Rechercher une GPO...", etc.), including error messages and placeholders (lines 1251, 1256-1258, 1265, 1273, 1278-1283). For consistency and maintainability, the codebase should use a single language consistently, or implement proper localization/internationalization support.

Copilot uses AI. Check for mistakes.
Comment on lines +1335 to +1371
while ($null -ne $parentCategory) {
# Check if category already exists
$existingItem = $null
foreach ($item in $currentItem.Items) {
if ($item.Tag -and $item.Tag.DisplayName -eq $parentCategory.DisplayName) {
$existingItem = $item
break
}
}

if (-not $existingItem) {
$newItem = New-Object Windows.Controls.TreeViewItem

# Create StackPanel for folder icon + text
$folderPanel = New-Object Windows.Controls.StackPanel
$folderPanel.Orientation = 'Horizontal'

$folderIcon = New-Object Windows.Controls.TextBlock
$folderIcon.Text = '📂 '
$folderIcon.FontSize = 13
$folderIcon.Foreground = $window.Resources['IconFolderBrush']

$folderText = New-Object Windows.Controls.TextBlock
$folderText.Text = $parentCategory.DisplayName

$folderPanel.Children.Add($folderIcon) | Out-Null
$folderPanel.Children.Add($folderText) | Out-Null
$newItem.Header = $folderPanel
$newItem.Tag = $parentCategory

$currentItem.Items.Add($newItem) | Out-Null
$currentItem = $newItem
} else {
$currentItem = $existingItem
}
$parentCategory = $parentCategory.ParentCategory
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested loops iterate through parent categories and tree items to build hierarchical structure. This could result in poor performance with a large number of policies or deep category hierarchies (O(n*m) complexity). Consider using a dictionary/hashtable to cache category items by their DisplayName for faster lookups instead of iterating through all items each time.

Copilot uses AI. Check for mistakes.
Comment on lines +1181 to +1206
if ($enableMenuItem) {
$enableMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Enable: $($selectedItem.Tag.DisplayName)"
}
})
}

if ($disableMenuItem) {
$disableMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Disable: $($selectedItem.Tag.DisplayName)"
}
})
}

if ($createGpoMenuItem) {
$createGpoMenuItem.Add_Click({
$selectedItem = $treeView.SelectedItem
if ($selectedItem -and $selectedItem.Tag -is [GPOToolsPolicy]) {
Write-Host "Create GPO for: $($selectedItem.Tag.DisplayName)"
}
})
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The menu item click handlers only output to Write-Host without implementing any actual functionality. These handlers appear to be placeholder implementations that don't perform the actions their names suggest (enabling/disabling parameters, creating GPOs). This could confuse users who expect these menu items to work.

Copilot uses AI. Check for mistakes.
Comment on lines +1443 to +1446
try {
$window.Icon = [System.Windows.Media.Imaging.BitmapImage]::new([Uri]::new("pack://application:,,,/icon.ico"))
} catch {
# Icon not found, continue without it
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded icon path "pack://application:,,,/icon.ico" will likely fail in most installations since no icon.ico file is included in the module. The try-catch silently ignores the error, which is good for graceful degradation, but this hardcoded path should either be removed, made configurable, or point to an actual bundled resource.

Suggested change
try {
$window.Icon = [System.Windows.Media.Imaging.BitmapImage]::new([Uri]::new("pack://application:,,,/icon.ico"))
} catch {
# Icon not found, continue without it
$iconPath = Join-Path -Path $PSScriptRoot -ChildPath 'icon.ico'
if (Test-Path -LiteralPath $iconPath) {
try {
$iconUri = [System.Uri]::new((Resolve-Path -LiteralPath $iconPath))
$window.Icon = [System.Windows.Media.Imaging.BitmapImage]::new($iconUri)
} catch {
# Icon failed to load, continue without it
}

Copilot uses AI. Check for mistakes.
$detailsTitleTextBlock = $window.FindName("detailsTitleTextBlock")

# Theme management
$isDarkTheme = $false
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function uses a script-scoped variable $script:isDarkTheme but initializes a local variable $isDarkTheme on line 1096. This inconsistency could cause issues - the Add_Click handler references $script:isDarkTheme while the initial assignment is to a local variable. The initial assignment should be $script:isDarkTheme = $false to match the reference in the click handler.

Suggested change
$isDarkTheme = $false
$script:isDarkTheme = $false

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants