1 Commits
Author SHA1 Message Date
kawaandClaude Haiku 4.5 a366ff6ee4 feat: UI improvements and robustness fixes
- Use System.IO.Path.GetFileName for safer path handling (handles UNC paths, trailing slashes)
- Increase minimum window size (860x600, was 760x560) for better control visibility
- Widen folder list box and reposition buttons to match
- Enable multi-select mode on folder list
- Add keyboard accelerators to Ajouter/Supprimer buttons (&)
- Disable Supprimer button when no folder selected

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-24 12:25:42 +02:00
+224 -115
View File
@@ -564,7 +564,7 @@ function Get-FolderNode {
[int]$Depth = 0
)
$name = Split-Path -Path $Path -Leaf
$name = [System.IO.Path]::GetFileName($Path.TrimEnd('\'))
if ([string]::IsNullOrEmpty($name)) { $name = $Path } # e.g. a drive root
$node = [ordered]@{
@@ -2052,7 +2052,7 @@ $form = New-Object System.Windows.Forms.Form
$form.Text = 'Filer Manager'
$form.Size = New-Object System.Drawing.Size(960, 720)
$form.StartPosition = 'CenterScreen'
$form.MinimumSize = New-Object System.Drawing.Size(760, 560)
$form.MinimumSize = New-Object System.Drawing.Size(860, 600)
# ---- top: folder list + add/remove ----
$lblFolders = New-Object System.Windows.Forms.Label
@@ -2061,19 +2061,20 @@ $lblFolders.Location = '12,12'; $lblFolders.AutoSize = $true
$form.Controls.Add($lblFolders)
$lstFolders = New-Object System.Windows.Forms.ListBox
$lstFolders.Location = '12,32'; $lstFolders.Size = '700,84'
$lstFolders.Location = '12,32'; $lstFolders.Size = '800,84'
$lstFolders.Anchor = 'Top,Left,Right'
$lstFolders.HorizontalScrollbar = $true
$lstFolders.SelectionMode = 'MultiExtended'
$form.Controls.Add($lstFolders)
$btnAdd = New-Object System.Windows.Forms.Button
$btnAdd.Text = 'Ajouter...'; $btnAdd.Location = '724,32'; $btnAdd.Size = '110,28'
$btnAdd.Text = 'A&jouter...'; $btnAdd.Location = '824,32'; $btnAdd.Size = '110,28'
$btnAdd.Anchor = 'Top,Right'
$form.Controls.Add($btnAdd)
$btnRemove = New-Object System.Windows.Forms.Button
$btnRemove.Text = 'Supprimer'; $btnRemove.Location = '724,66'; $btnRemove.Size = '110,28'
$btnRemove.Anchor = 'Top,Right'
$btnRemove.Text = '&Supprimer'; $btnRemove.Location = '824,66'; $btnRemove.Size = '110,28'
$btnRemove.Anchor = 'Top,Right'; $btnRemove.Enabled = $false
$form.Controls.Add($btnRemove)
# ---- settings row ----
@@ -2136,21 +2137,47 @@ $form.Controls.Add($chkRevert)
$chkGrant.Add_CheckedChanged({ $chkRevert.Enabled = $chkGrant.Checked })
$btnScan = New-Object System.Windows.Forms.Button
$btnScan.Text = 'Analyser'; $btnScan.Location = '600,124'; $btnScan.Size = '110,30'
$btnScan.Text = '&Analyser'; $btnScan.Location = '700,124'; $btnScan.Size = '110,30'
$btnScan.Anchor = 'Top,Right'
$btnScan.BackColor = [System.Drawing.Color]::FromArgb(79,140,255)
$btnScan.ForeColor = [System.Drawing.Color]::White
$btnScan.FlatStyle = 'Flat'
$btnScan.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnScan.FlatAppearance.BorderSize = 0
$btnScan.FlatAppearance.MouseOverBackColor = [System.Drawing.Color]::FromArgb(104, 158, 255)
$btnScan.FlatAppearance.MouseDownBackColor = [System.Drawing.Color]::FromArgb(56, 114, 220)
# A flat button keeps its custom BackColor when disabled, which reads as "still
# clickable". Dim it explicitly instead.
$btnScan.Add_EnabledChanged({
$btnScan.BackColor = if ($btnScan.Enabled) { [System.Drawing.Color]::FromArgb(79, 140, 255) }
else { [System.Drawing.Color]::FromArgb(176, 186, 200) }
})
$form.Controls.Add($btnScan)
$btnExport = New-Object System.Windows.Forms.Button
$btnExport.Text = 'Exporter...'; $btnExport.Location = '724,124'; $btnExport.Size = '110,30'
$btnExport.Text = '&Exporter...'; $btnExport.Location = '824,124'; $btnExport.Size = '110,30'
$btnExport.Anchor = 'Top,Right'; $btnExport.Enabled = $false
$form.Controls.Add($btnExport)
# Defined here because the list views below hook it on Resize, which can fire
# while the tab pages are still being built.
function Update-FilerListFill {
<# Gives one column whatever width the other columns leave, so the long
path/folder column follows the window instead of staying at its design
width with a horizontal scrollbar. #>
param($List, [int]$Col)
if ($null -eq $List -or $List.Columns.Count -le $Col) { return }
$other = 0
for ($i = 0; $i -lt $List.Columns.Count; $i++) {
if ($i -ne $Col) { $other += $List.Columns[$i].Width }
}
$w = $List.ClientSize.Width - $other - 4
if ($w -gt 140) { $List.Columns[$Col].Width = $w }
}
# ---- tabs ----
$tabs = New-Object System.Windows.Forms.TabControl
$tabs.Location = '12,220'; $tabs.Size = '922,424'
$tabs.Location = '12,220'; $tabs.Size = '922,437'
$tabs.Anchor = 'Top,Bottom,Left,Right'
$form.Controls.Add($tabs)
@@ -2172,6 +2199,16 @@ $tabTree.Controls.Add($tree)
# the GUI responsive no matter how large the scanned tree is).
$tree.Add_BeforeExpand({ param($s, $e) Expand-FilerTreeNode -TreeNode $e.Node })
# DrawNode fires for every visible row on every repaint, so the brushes and the
# pen are built once here instead of three GDI allocations per row per paint.
$script:BarTrack = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(228, 230, 235))
$script:BarFill = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(59, 111, 212))
$script:BarEdge = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(170, 175, 185))
$script:BarSel = New-Object System.Drawing.SolidBrush ([System.Drawing.SystemColors]::Highlight)
$script:TreeTextFlags = [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor `
[System.Windows.Forms.TextFormatFlags]::Left -bor `
[System.Windows.Forms.TextFormatFlags]::NoPrefix
# Render each tree node: the label, then a bar showing the folder's share of its
# parent's size (roots = 100%), then the percentage - matching the HTML report.
$tree.Add_DrawNode({
@@ -2185,14 +2222,9 @@ $tree.Add_DrawNode({
$selected = ($e.State -band [System.Windows.Forms.TreeNodeStates]::Selected) -ne 0
$foreColor = if ($selected) { [System.Drawing.SystemColors]::HighlightText } else { $tree.ForeColor }
if ($selected) {
$hl = New-Object System.Drawing.SolidBrush ([System.Drawing.SystemColors]::Highlight)
$e.Graphics.FillRectangle($hl, $e.Bounds); $hl.Dispose()
}
if ($selected) { $e.Graphics.FillRectangle($script:BarSel, $e.Bounds) }
$flags = [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor `
[System.Windows.Forms.TextFormatFlags]::Left -bor `
[System.Windows.Forms.TextFormatFlags]::NoPrefix
$flags = $script:TreeTextFlags
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.Node.Text, $tree.Font, $e.Bounds, $foreColor, $flags)
# Share of the parent folder's size (top-level roots have no parent = 100%).
@@ -2204,21 +2236,17 @@ $tree.Add_DrawNode({
}
if ($pct -lt 0) { $pct = 0 } elseif ($pct -gt 100) { $pct = 100 }
# Bar just to the right of the label.
# Bar just to the right of the label - skipped entirely when a long label has
# already pushed it out of view, so deep trees do not pay for invisible paint.
$barW = 120; $barH = 9
$barX = $e.Bounds.Right + 10
$barY = $e.Bounds.Top + [int](($e.Bounds.Height - $barH) / 2)
if ($barX -ge $tree.ClientSize.Width) { return }
$track = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(228, 230, 235))
$e.Graphics.FillRectangle($track, $barX, $barY, $barW, $barH); $track.Dispose()
$e.Graphics.FillRectangle($script:BarTrack, $barX, $barY, $barW, $barH)
$fillW = [int][math]::Round($barW * $pct / 100)
if ($fillW -gt 0) {
$fill = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(59, 111, 212))
$e.Graphics.FillRectangle($fill, $barX, $barY, $fillW, $barH); $fill.Dispose()
}
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(170, 175, 185))
$e.Graphics.DrawRectangle($pen, $barX, $barY, $barW, $barH); $pen.Dispose()
if ($fillW -gt 0) { $e.Graphics.FillRectangle($script:BarFill, $barX, $barY, $fillW, $barH) }
$e.Graphics.DrawRectangle($script:BarEdge, $barX, $barY, $barW, $barH)
$pctRect = New-Object System.Drawing.Rectangle (($barX + $barW + 6), $e.Bounds.Top, 52, $e.Bounds.Height)
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, ('{0:N1}%' -f $pct), $tree.Font, $pctRect, $tree.ForeColor, $flags)
@@ -2243,36 +2271,44 @@ $lvGrant.Dock = 'Fill'; $lvGrant.View = 'Details'; $lvGrant.FullRowSelect = $tru
$tabGrant.Controls.Add($lvGrant)
# Filter toolbar for the permissions tab (docked above the list).
$tabW = 914 # baseline width the right-aligned tab controls are anchored to
$permBar = New-Object System.Windows.Forms.Panel
$permBar.Dock = 'Top'; $permBar.Height = 30
$permBar.Dock = 'Top'; $permBar.Height = 30; $permBar.Width = $tabW
$lblPermHint = New-Object System.Windows.Forms.Label
$lblPermHint.Text = 'Cliquez sur un en-tête de colonne pour filtrer ou trier.'
$lblPermHint.AutoSize = $true; $lblPermHint.Location = '6,7'
# Sized rather than auto-sized: it shares the row with the two check boxes, so
# it has to give ground (and ellipsize) as the window narrows.
$lblPermHint.AutoSize = $false; $lblPermHint.Location = '6,8'; $lblPermHint.Size = '330,18'
$lblPermHint.Anchor = 'Top,Left,Right'; $lblPermHint.AutoEllipsis = $true
$lblPermHint.ForeColor = [System.Drawing.Color]::DimGray
$chkPermHideInh = New-Object System.Windows.Forms.CheckBox
$chkPermHideInh.Text = 'Masquer les héritées (enfants)'
$chkPermHideInh.Size = '220,20'; $chkPermHideInh.Location = '342,6'; $chkPermHideInh.Anchor = 'Top,Right'
$chkPermHideSys = New-Object System.Windows.Forms.CheckBox
$chkPermHideSys.Text = 'Masquer les comptes système'
$chkPermHideSys.Size = '200,20'; $chkPermHideSys.Location = '570,6'; $chkPermHideSys.Anchor = 'Top,Right'
$btnPermClear = New-Object System.Windows.Forms.Button
$btnPermClear.Text = 'Effacer les filtres'; $btnPermClear.Size = '130,24'
$btnPermClear.Dock = 'Right'; $btnPermClear.Enabled = $false
$chkPermHideInh = New-Object System.Windows.Forms.CheckBox
$chkPermHideInh.Text = 'Masquer les permissions héritées (enfants)'
$chkPermHideInh.AutoSize = $true; $chkPermHideInh.Dock = 'Right'
$chkPermHideInh.Padding = '0,4,8,0'
$chkPermHideSys = New-Object System.Windows.Forms.CheckBox
$chkPermHideSys.Text = 'Masquer les comptes/groupes système'
$chkPermHideSys.AutoSize = $true; $chkPermHideSys.Dock = 'Right'
$chkPermHideSys.Padding = '0,4,8,0'
$permBar.Controls.Add($lblPermHint)
$permBar.Controls.Add($chkPermHideInh)
$permBar.Controls.Add($chkPermHideSys)
$permBar.Controls.Add($btnPermClear)
$btnPermClear.Location = '778,3'; $btnPermClear.Anchor = 'Top,Right'; $btnPermClear.Enabled = $false
$permBar.Controls.AddRange(@($lblPermHint, $chkPermHideInh, $chkPermHideSys, $btnPermClear))
$tabPerm.Controls.Add($permBar)
$tip.SetToolTip($chkPermHideInh, "Masquer les permissions héritées portées par les dossiers enfants.`nCelles des dossiers racines sont conservées : leur parent est hors analyse.")
$tip.SetToolTip($chkPermHideSys, "Masquer les entrées détenues par les comptes et groupes système bien connus`n(SYSTEM, Administrators, CREATOR OWNER, NT SERVICE\*) pour ne garder que les identités métier.")
$tip.SetToolTip($btnPermClear, 'Supprimer tous les filtres de colonne actifs.')
$lvLong.Add_Resize({ Update-FilerListFill -List $lvLong -Col 2 })
$lvPerm.Add_Resize({ Update-FilerListFill -List $lvPerm -Col 0 })
$lvGrant.Add_Resize({ Update-FilerListFill -List $lvGrant -Col 0 })
$lvPerm.Add_ColumnClick({ param($s, $e) Show-PermColumnMenu -ColumnIndex $e.Column })
$btnPermClear.Add_Click({ $script:PermFilters = @{}; Update-PermView })
$chkPermHideInh.Add_CheckedChanged({ $script:PermHideInherited = $chkPermHideInh.Checked; Update-PermView })
$chkPermHideSys.Add_CheckedChanged({ $script:PermHideSystem = $chkPermHideSys.Checked; Update-PermView })
# ---- Renommer tab: batch rename with find/replace, regex, case, affixes, numbering ----
$rnW = 914 # baseline width used to anchor the right-aligned controls
$rnW = $tabW # baseline width used to anchor the right-aligned controls
# Preview grid (added first so the docked panels below claim their edges and the
# grid fills the remaining space - same ordering trick used by the perm filter bar).
@@ -2292,59 +2328,59 @@ $rnTop.Dock = 'Top'; $rnTop.Height = 200; $rnTop.Width = $rnW
$lblRnPath = New-Object System.Windows.Forms.Label
$lblRnPath.Text = 'Dossier :'; $lblRnPath.Location = '6,13'; $lblRnPath.AutoSize = $true
$rnPath = New-Object System.Windows.Forms.TextBox
$rnPath.Location = '70,10'; $rnPath.Width = ($rnW - 70 - 188); $rnPath.Anchor = 'Top,Left,Right'
$rnPath.Location = '90,10'; $rnPath.Width = ($rnW - 90 - 198); $rnPath.Anchor = 'Top,Left,Right'
$rnBrowse = New-Object System.Windows.Forms.Button
$rnBrowse.Text = 'Parcourir...'; $rnBrowse.Size = '88,24'; $rnBrowse.Location = "$($rnW - 184),9"; $rnBrowse.Anchor = 'Top,Right'
$rnBrowse.Text = 'Parcourir...'; $rnBrowse.Size = '88,24'; $rnBrowse.Location = "$($rnW - 190),9"; $rnBrowse.Anchor = 'Top,Right'
$rnList = New-Object System.Windows.Forms.Button
$rnList.Text = 'Lister'; $rnList.Size = '88,24'; $rnList.Location = "$($rnW - 92),9"; $rnList.Anchor = 'Top,Right'
$rnList.Text = 'Lister'; $rnList.Size = '88,24'; $rnList.Location = "$($rnW - 94),9"; $rnList.Anchor = 'Top,Right'
$rnTop.Controls.AddRange(@($lblRnPath, $rnPath, $rnBrowse, $rnList))
# Row 2: scope options.
$chkRnRecurse = New-Object System.Windows.Forms.CheckBox
$chkRnRecurse.Text = 'Inclure les sous-dossiers'; $chkRnRecurse.Location = '70,40'; $chkRnRecurse.AutoSize = $true
$chkRnRecurse.Text = 'Inclure les sous-dossiers'; $chkRnRecurse.Location = '90,40'; $chkRnRecurse.AutoSize = $true
$chkRnFiles = New-Object System.Windows.Forms.CheckBox
$chkRnFiles.Text = 'Fichiers'; $chkRnFiles.Location = '250,40'; $chkRnFiles.AutoSize = $true; $chkRnFiles.Checked = $true
$chkRnFiles.Text = 'Fichiers'; $chkRnFiles.Location = '270,40'; $chkRnFiles.AutoSize = $true; $chkRnFiles.Checked = $true
$chkRnFolders = New-Object System.Windows.Forms.CheckBox
$chkRnFolders.Text = 'Dossiers'; $chkRnFolders.Location = '345,40'; $chkRnFolders.AutoSize = $true; $chkRnFolders.Checked = $true
$chkRnFolders.Text = 'Dossiers'; $chkRnFolders.Location = '365,40'; $chkRnFolders.AutoSize = $true; $chkRnFolders.Checked = $true
$chkRnHidden = New-Object System.Windows.Forms.CheckBox
$chkRnHidden.Text = 'Exclure les dossiers masqués'; $chkRnHidden.Location = '450,40'; $chkRnHidden.AutoSize = $true; $chkRnHidden.Checked = $true
$chkRnHidden.Text = 'Exclure les dossiers masqués'; $chkRnHidden.Location = '470,40'; $chkRnHidden.AutoSize = $true; $chkRnHidden.Checked = $true
$rnTop.Controls.AddRange(@($chkRnRecurse, $chkRnFiles, $chkRnFolders, $chkRnHidden))
# Row 3: find.
$lblRnFind = New-Object System.Windows.Forms.Label
$lblRnFind.Text = 'Rechercher :'; $lblRnFind.Location = '6,75'; $lblRnFind.AutoSize = $true
$rnFind = New-Object System.Windows.Forms.TextBox
$rnFind.Location = '90,72'; $rnFind.Width = ($rnW - 90 - 270); $rnFind.Anchor = 'Top,Left,Right'
$rnFind.Location = '90,72'; $rnFind.Width = ($rnW - 90 - 222); $rnFind.Anchor = 'Top,Left,Right'
$chkRnRegex = New-Object System.Windows.Forms.CheckBox
$chkRnRegex.Text = 'Regex'; $chkRnRegex.Size = '70,22'; $chkRnRegex.Location = "$($rnW - 172),74"; $chkRnRegex.Anchor = 'Top,Right'
$chkRnRegex.Text = 'Regex'; $chkRnRegex.Size = '70,22'; $chkRnRegex.Location = "$($rnW - 214),74"; $chkRnRegex.Anchor = 'Top,Right'
$chkRnIgnoreCase = New-Object System.Windows.Forms.CheckBox
$chkRnIgnoreCase.Text = 'Ignorer la casse'; $chkRnIgnoreCase.Size = '130,22'; $chkRnIgnoreCase.Location = "$($rnW - 100),74"; $chkRnIgnoreCase.Anchor = 'Top,Right'
$chkRnIgnoreCase.Text = 'Ignorer la casse'; $chkRnIgnoreCase.Size = '130,22'; $chkRnIgnoreCase.Location = "$($rnW - 136),74"; $chkRnIgnoreCase.Anchor = 'Top,Right'
$rnTop.Controls.AddRange(@($lblRnFind, $rnFind, $chkRnRegex, $chkRnIgnoreCase))
# Row 4: replace.
$lblRnReplace = New-Object System.Windows.Forms.Label
$lblRnReplace.Text = 'Remplacer :'; $lblRnReplace.Location = '6,105'; $lblRnReplace.AutoSize = $true
$rnReplace = New-Object System.Windows.Forms.TextBox
$rnReplace.Location = '90,102'; $rnReplace.Width = ($rnW - 90 - 270); $rnReplace.Anchor = 'Top,Left,Right'
$rnReplace.Location = '90,102'; $rnReplace.Width = ($rnW - 90 - 222); $rnReplace.Anchor = 'Top,Left,Right'
$chkRnIgnoreExt = New-Object System.Windows.Forms.CheckBox
$chkRnIgnoreExt.Text = "Ignorer l'extension"; $chkRnIgnoreExt.Size = '160,22'; $chkRnIgnoreExt.Location = "$($rnW - 172),104"; $chkRnIgnoreExt.Anchor = 'Top,Right'; $chkRnIgnoreExt.Checked = $true
$chkRnIgnoreExt.Text = "Ignorer l'extension"; $chkRnIgnoreExt.Size = '160,22'; $chkRnIgnoreExt.Location = "$($rnW - 166),104"; $chkRnIgnoreExt.Anchor = 'Top,Right'; $chkRnIgnoreExt.Checked = $true
$rnTop.Controls.AddRange(@($lblRnReplace, $rnReplace, $chkRnIgnoreExt))
# Row 5: case + affixes.
$lblRnCase = New-Object System.Windows.Forms.Label
$lblRnCase.Text = 'Casse :'; $lblRnCase.Location = '6,137'; $lblRnCase.AutoSize = $true
$rnCase = New-Object System.Windows.Forms.ComboBox
$rnCase.DropDownStyle = 'DropDownList'; $rnCase.Location = '62,134'; $rnCase.Width = 150
$rnCase.DropDownStyle = 'DropDownList'; $rnCase.Location = '90,134'; $rnCase.Width = 150
[void]$rnCase.Items.AddRange(@('Conserver', 'MAJUSCULES', 'minuscules', 'Première Lettre Des Mots', 'Première lettre'))
$rnCase.SelectedIndex = 0
$lblRnPrefix = New-Object System.Windows.Forms.Label
$lblRnPrefix.Text = 'Préfixe :'; $lblRnPrefix.Location = '228,137'; $lblRnPrefix.AutoSize = $true
$lblRnPrefix.Text = 'Préfixe :'; $lblRnPrefix.Location = '256,137'; $lblRnPrefix.AutoSize = $true
$rnPrefix = New-Object System.Windows.Forms.TextBox
$rnPrefix.Location = '282,134'; $rnPrefix.Width = 150
$rnPrefix.Location = '310,134'; $rnPrefix.Width = 150
$lblRnSuffix = New-Object System.Windows.Forms.Label
$lblRnSuffix.Text = 'Suffixe :'; $lblRnSuffix.Location = '444,137'; $lblRnSuffix.AutoSize = $true
$lblRnSuffix.Text = 'Suffixe :'; $lblRnSuffix.Location = '476,137'; $lblRnSuffix.AutoSize = $true
$rnSuffix = New-Object System.Windows.Forms.TextBox
$rnSuffix.Location = '498,134'; $rnSuffix.Width = 150
$rnSuffix.Location = '530,134'; $rnSuffix.Width = 150
$rnTop.Controls.AddRange(@($lblRnCase, $rnCase, $lblRnPrefix, $rnPrefix, $lblRnSuffix, $rnSuffix))
# Row 6: sequential numbering + preview.
@@ -2390,6 +2426,7 @@ $rnBrowse.Add_Click({
-InitialPath $rnPath.Text)
if ($picked.Count -gt 0) { $rnPath.Text = [string]$picked[0] }
})
$lvRename.Add_Resize({ Update-FilerListFill -List $lvRename -Col 4 })
$rnList.Add_Click({ Invoke-RenameListing })
$rnPreview.Add_Click({ Update-RenamePreview })
# Recompute the preview live as the rename options change.
@@ -2445,6 +2482,7 @@ $script:PermCols = @('Folder', 'Identity', 'Rights', 'Type', 'Inherited') # c
# Display labels for the permission columns (property name -> French header).
$script:PermColLabels = @{ Folder = 'Dossier'; Identity = 'Identité'; Rights = 'Droits'; Type = 'Type'; Inherited = 'Hérité' }
$script:PermFilters = @{} # property -> System.Collections.Generic.HashSet[string] of allowed values
$script:PermMenu = $null # last column menu, disposed when the next one opens
$script:PermSort = @{ Prop = 'Folder'; Asc = $true }
# When $true, inherited ACEs on child folders are hidden (scan roots keep theirs).
$script:PermHideInherited = $false
@@ -2492,7 +2530,7 @@ function Expand-FilerTreeNode {
$node = $TreeNode.Tag
$TreeNode.TreeView.BeginUpdate()
$TreeNode.Nodes.Clear()
$kids = @($node.Children | Where-Object { $_ } | Sort-Object @{E={$_.Size}} -Descending)
$kids = @($node.Children | Where-Object { $_ } | Sort-Object -Property Size -Descending)
foreach ($c in $kids) { [void]$TreeNode.Nodes.Add((New-FilerTreeNode -Node $c)) }
$TreeNode.TreeView.EndUpdate()
}
@@ -2506,13 +2544,18 @@ function Show-Results {
if ($tree.Nodes.Count -gt 0) { $tree.Nodes[0].Expand() } # loads first level lazily
$tree.EndUpdate()
$lvLong.BeginUpdate(); $lvLong.Items.Clear()
# One AddRange instead of a per-row Add: a scan of a big share can report
# thousands of long paths and each Add re-lays the control out.
$longItems = New-Object 'System.Collections.Generic.List[System.Windows.Forms.ListViewItem]'
foreach ($lp in $Scan.LongPaths) {
$it = New-Object System.Windows.Forms.ListViewItem([string]$lp.Length)
[void]$it.SubItems.Add($lp.Type); [void]$it.SubItems.Add($lp.Path)
[void]$lvLong.Items.Add($it)
$longItems.Add($it)
}
$lvLong.BeginUpdate(); $lvLong.Items.Clear()
if ($longItems.Count -gt 0) { $lvLong.Items.AddRange($longItems.ToArray()) }
$lvLong.EndUpdate()
$tabLong.Text = if ($longItems.Count -gt 0) { "Chemins trop longs ($($longItems.Count))" } else { 'Chemins trop longs' }
$script:AllPerms = @($Scan.Permissions)
$script:PermRootPaths = Get-FilerRootPathSet -Scan $Scan
@@ -2520,7 +2563,7 @@ function Show-Results {
$script:PermSort = @{ Prop = 'Folder'; Asc = $true }
Update-PermView
$lvGrant.BeginUpdate(); $lvGrant.Items.Clear()
$grantItems = New-Object 'System.Collections.Generic.List[System.Windows.Forms.ListViewItem]'
foreach ($g in @($Scan.Grants | Where-Object { $_.Success })) {
$it = New-Object System.Windows.Forms.ListViewItem([string]$g.Path)
[void]$it.SubItems.Add([string]$g.Reason)
@@ -2530,10 +2573,15 @@ function Show-Results {
elseif ($Scan.Settings.RevertGrants) { $state = 'NON annulé'; $it.ForeColor = [System.Drawing.Color]::Firebrick }
else { $state = 'conservé'; $it.ForeColor = [System.Drawing.Color]::DarkGoldenrod }
[void]$it.SubItems.Add($state)
[void]$lvGrant.Items.Add($it)
$grantItems.Add($it)
}
$lvGrant.BeginUpdate(); $lvGrant.Items.Clear()
if ($grantItems.Count -gt 0) { $lvGrant.Items.AddRange($grantItems.ToArray()) }
$lvGrant.EndUpdate()
$tabGrant.Text = if ($lvGrant.Items.Count -gt 0) { "Accès accordé ($($lvGrant.Items.Count))" } else { 'Accès accordé' }
$tabGrant.Text = if ($grantItems.Count -gt 0) { "Accès accordé ($($grantItems.Count))" } else { 'Accès accordé' }
Update-FilerListFill -List $lvLong -Col 2
Update-FilerListFill -List $lvGrant -Col 0
$statusLbl.Text = ("Terminé. {0} au total, {1} dossiers, {2} fichiers, {3} chemins trop longs, {4} erreurs." -f `
(Format-Bytes $Scan.Stats.TotalSize), $Scan.Stats.TotalFolders, $Scan.Stats.TotalFiles,
@@ -2550,30 +2598,39 @@ function Get-PermCellValue {
function Update-PermView {
# Re-renders the permissions ListView from $script:AllPerms applying the
# active per-column filters and the current sort.
$rows = $script:AllPerms
if ($script:PermHideInherited) {
$rows = @($rows | Where-Object { (-not $_.Inherited) -or $script:PermRootPaths.Contains([string]$_.Folder) })
}
if ($script:PermHideSystem) {
$rows = @($rows | Where-Object { -not (Test-IsSystemPrincipal -Identity $_.Identity -Sid $_.Sid) })
}
foreach ($prop in $script:PermFilters.Keys) {
$allowed = $script:PermFilters[$prop]
$rows = @($rows | Where-Object { $allowed.Contains((Get-PermCellValue $_ $prop)) })
# Every active filter in a single pass (the old code rebuilt the whole array
# once per filter), and the sort keyed on the property name rather than on a
# script block evaluated for each comparison.
$rows = $script:AllPerms
$hideInh = $script:PermHideInherited
$hideSys = $script:PermHideSystem
$filters = $script:PermFilters
$filterKey = @($filters.Keys)
$roots = $script:PermRootPaths
if ($hideInh -or $hideSys -or $filterKey.Count -gt 0) {
$rows = @($rows | Where-Object {
if ($hideInh -and $_.Inherited -and -not $roots.Contains([string]$_.Folder)) { return $false }
if ($hideSys -and (Test-IsSystemPrincipal -Identity $_.Identity -Sid $_.Sid)) { return $false }
foreach ($k in $filterKey) { if (-not $filters[$k].Contains([string]$_.$k)) { return $false } }
return $true
})
}
$sortProp = $script:PermSort.Prop
$rows = @($rows | Sort-Object @{ E = { Get-PermCellValue $_ $sortProp } })
$rows = @($rows | Sort-Object -Property $sortProp)
if (-not $script:PermSort.Asc) { [array]::Reverse($rows) }
$lvPerm.BeginUpdate(); $lvPerm.Items.Clear()
$deny = [System.Drawing.Color]::Firebrick
$items = New-Object 'System.Collections.Generic.List[System.Windows.Forms.ListViewItem]'
foreach ($ace in $rows) {
$it = New-Object System.Windows.Forms.ListViewItem($ace.Folder)
[void]$it.SubItems.Add($ace.Identity); [void]$it.SubItems.Add($ace.Rights)
[void]$it.SubItems.Add($ace.Type); [void]$it.SubItems.Add([string]$ace.Inherited)
if ($ace.Type -eq 'Deny') { $it.ForeColor = [System.Drawing.Color]::Firebrick }
[void]$lvPerm.Items.Add($it)
if ($ace.Type -eq 'Deny') { $it.ForeColor = $deny }
$items.Add($it)
}
$lvPerm.BeginUpdate(); $lvPerm.Items.Clear()
if ($items.Count -gt 0) { $lvPerm.Items.AddRange($items.ToArray()) }
$lvPerm.EndUpdate()
# Update column headers to show sort arrow and filter funnel.
@@ -2588,6 +2645,7 @@ function Update-PermView {
$shown = $lvPerm.Items.Count; $total = $script:AllPerms.Count
$nFilt = $script:PermFilters.Count
$btnPermClear.Enabled = ($nFilt -gt 0)
$tabPerm.Text = if ($total -gt 0) { "Permissions ($shown)" } else { 'Permissions' }
if ($script:AllPerms.Count -gt 0) {
$msg = "Permissions : $shown sur $total affichées"
if ($nFilt -gt 0) { $msg += " ($nFilt filtre$(if($nFilt -gt 1){'s'}) de colonne) - cliquez sur un en-tête pour filtrer/trier" }
@@ -2601,6 +2659,10 @@ function Show-PermColumnMenu {
if ($script:AllPerms.Count -eq 0) { return }
$prop = $script:PermCols[$ColumnIndex]
# The previous menu is only safe to drop once a new click has replaced it -
# disposing it from its own Closed handler would tear it down mid-event.
if ($script:PermMenu) { $script:PermMenu.Dispose(); $script:PermMenu = $null }
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$menu.ShowImageMargin = $false
@@ -2662,6 +2724,7 @@ function Show-PermColumnMenu {
# Show just under the clicked column header.
$x = 0
for ($i = 0; $i -lt $ColumnIndex; $i++) { $x += $lvPerm.Columns[$i].Width }
$script:PermMenu = $menu
$menu.Show($lvPerm, $x, 4)
}
@@ -2689,7 +2752,7 @@ function Show-ExportDialog {
$dlg.FormBorderStyle = 'FixedDialog'
$dlg.StartPosition = 'CenterParent'
$dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false
$dlg.ClientSize = New-Object System.Drawing.Size(320, 330)
$dlg.ClientSize = New-Object System.Drawing.Size(320, 330) # height fixed up below
$lbl = New-Object System.Windows.Forms.Label
$lbl.Text = 'Inclure ces catégories dans le rapport :'
@@ -2741,6 +2804,8 @@ function Show-ExportDialog {
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$dlg.Controls.Add($btnCancel); $dlg.CancelButton = $btnCancel
$dlg.ClientSize = New-Object System.Drawing.Size(320, ($y + 42))
# Enable Export only when at least one category AND at least one format is ticked.
$sync = {
$anyCat = @($boxes | Where-Object { $_.Checked }).Count -gt 0
@@ -2861,7 +2926,7 @@ function Invoke-RenameListing {
if ($wantDir) {
[void]$items.Add([pscustomobject]@{
Name = $c.Name; FullPath = $c.FullName; IsFile = $false
Dir = (Split-Path -LiteralPath $c.FullName -Parent); Depth = ($cur.Depth + 1)
Dir = ([System.IO.Path]::GetDirectoryName($c.FullName)); Depth = ($cur.Depth + 1)
})
}
# A junction / symlink can be renamed, but descending into one
@@ -2873,7 +2938,7 @@ function Invoke-RenameListing {
elseif ($wantFile) {
[void]$items.Add([pscustomobject]@{
Name = $c.Name; FullPath = $c.FullName; IsFile = $true
Dir = (Split-Path -LiteralPath $c.FullName -Parent); Depth = ($cur.Depth + 1)
Dir = ([System.IO.Path]::GetDirectoryName($c.FullName)); Depth = ($cur.Depth + 1)
})
}
}
@@ -2881,7 +2946,7 @@ function Invoke-RenameListing {
}
finally { $form.Cursor = [System.Windows.Forms.Cursors]::Default }
$script:RenameItems = @($items | Sort-Object @{ E = { $_.FullPath } })
$script:RenameItems = @($items | Sort-Object -Property FullPath)
Update-RenamePreview
}
@@ -2929,23 +2994,34 @@ function Update-RenamePreview {
}
else { $seen[$key] = $row }
}
# Current name -> row, so "is the occupant one of the items that moves
# away?" is a hashtable hit instead of a scan of the whole group. The row
# object is stored, not a flag, so its live .Changed state is still read.
$movers = @{}
foreach ($r in $grp.Group) {
if ($r.Changed) { $movers[$r.Item.Name.ToLowerInvariant()] = $r }
}
foreach ($row in $grp.Group) {
if (-not $row.Changed) { continue }
$target = Join-Path $row.Item.Dir $row.New
# Skip a case-only rename of the item onto itself (-ne is case-insensitive).
if ($target -ne $row.Item.FullPath -and (Test-Path -LiteralPath $target)) {
# OK only if the current occupant is another listed item that will move away.
$occupantMoves = $grp.Group | Where-Object {
$_.Changed -and $_.Item.Name.ToLowerInvariant() -eq $row.New.ToLowerInvariant()
$occupant = $movers[$row.New.ToLowerInvariant()]
if (-not ($occupant -and $occupant.Changed)) {
$row.Status = 'conflit (existe déjà)'; $row.Changed = $false
[void]$movers.Remove($row.Item.Name.ToLowerInvariant())
}
if (-not $occupantMoves) { $row.Status = 'conflit (existe déjà)'; $row.Changed = $false }
}
}
}
$script:RenameRows = @($rows)
$lvRename.BeginUpdate(); $lvRename.Items.Clear()
# Built off-control then inserted in one go: this runs on every keystroke in
# the find/replace boxes, so a per-row Add would make typing feel sticky.
$bad = [System.Drawing.Color]::Firebrick
$dim = [System.Drawing.Color]::Gray
$view = New-Object 'System.Collections.Generic.List[System.Windows.Forms.ListViewItem]'
foreach ($row in $rows) {
$it = New-Object System.Windows.Forms.ListViewItem($(if ($row.Item.IsFile) { 'Fichier' } else { 'Dossier' }))
[void]$it.SubItems.Add($row.Item.Name)
@@ -2955,11 +3031,13 @@ function Update-RenamePreview {
$it.Tag = $row
if ($row.Changed) { $it.Checked = $true }
elseif ($row.Status -like 'conflit*' -or $row.Status -like 'erreur*' -or $row.Status -eq 'caractères invalides' -or $row.Status -eq 'nom vide') {
$it.ForeColor = [System.Drawing.Color]::Firebrick
$it.ForeColor = $bad
}
else { $it.ForeColor = [System.Drawing.Color]::Gray } # inchangé
[void]$lvRename.Items.Add($it)
else { $it.ForeColor = $dim } # inchangé
$view.Add($it)
}
$lvRename.BeginUpdate(); $lvRename.Items.Clear()
if ($view.Count -gt 0) { $lvRename.Items.AddRange($view.ToArray()) }
$lvRename.EndUpdate()
$nChange = @($rows | Where-Object { $_.Changed }).Count
@@ -3026,7 +3104,7 @@ $timer.Add_Tick({
if ($script:Handle -and $script:Handle.IsCompleted) {
$timer.Stop()
try {
$result = $script:PowerShell.EndInvoke($script:Handle)
$null = $script:PowerShell.EndInvoke($script:Handle)
$scan = $script:Shared.Result
if ($scan) { Show-Results -Scan $scan }
else { $statusLbl.Text = 'Analyse terminée mais aucune donnée renvoyée.' }
@@ -3042,7 +3120,8 @@ $timer.Add_Tick({
$script:PowerShell = $null; $script:Runspace = $null; $script:Handle = $null
$script:ScanGrantsPending = $false
$progressBar.Visible = $false
$btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true
$btnScan.Enabled = $true; $btnAdd.Enabled = $true
$btnRemove.Enabled = ($lstFolders.SelectedIndices.Count -gt 0)
}
}
}
@@ -3067,7 +3146,8 @@ $exportTimer.Add_Tick({
if ($script:ExportRunspace) { $script:ExportRunspace.Close(); $script:ExportRunspace.Dispose() }
$script:ExportPowerShell = $null; $script:ExportRunspace = $null; $script:ExportHandle = $null
$progressBar.Visible = $false
$btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true
$btnScan.Enabled = $true; $btnAdd.Enabled = $true
$btnRemove.Enabled = ($lstFolders.SelectedIndices.Count -gt 0)
$btnExport.Enabled = ($null -ne $script:LastScan)
}
@@ -3293,7 +3373,7 @@ function Expand-FilerPickerNode {
elseif ($r.Error) { $err = $r.Error }
else {
$children = @($r.Folders | ForEach-Object {
[pscustomobject]@{ Text = (Split-Path -Path $_ -Leaf); Path = $_; Kind = 'folder' }
[pscustomobject]@{ Text = ([System.IO.Path]::GetFileName($_)); Path = $_; Kind = 'folder' }
})
}
}
@@ -3425,47 +3505,47 @@ function Show-FilerFolderPicker {
$dlg = New-Object System.Windows.Forms.Form
$dlg.Text = if ($SingleSelection) { 'Sélectionner un dossier' } else { 'Sélectionner des dossiers' }
$dlg.StartPosition = 'CenterParent'
$dlg.ClientSize = New-Object System.Drawing.Size(760, 592)
$dlg.MinimumSize = New-Object System.Drawing.Size(640, 500)
$dlg.ClientSize = New-Object System.Drawing.Size(780, 620)
$dlg.MinimumSize = New-Object System.Drawing.Size(740, 560)
$dlg.ShowInTaskbar = $false
$lblDesc = New-Object System.Windows.Forms.Label
$lblDesc.Text = $Description; $lblDesc.Location = '12,10'; $lblDesc.AutoSize = $true
$txtPath = New-Object System.Windows.Forms.TextBox
$txtPath.Location = '12,34'; $txtPath.Size = '618,24'; $txtPath.Anchor = 'Top,Left,Right'
$txtPath.Location = '12,34'; $txtPath.Size = '638,24'; $txtPath.Anchor = 'Top,Left,Right'
if ($InitialPath) { $txtPath.Text = $InitialPath }
$btnGo = New-Object System.Windows.Forms.Button
$btnGo.Text = 'Ouvrir'; $btnGo.Location = '638,33'; $btnGo.Size = '110,26'; $btnGo.Anchor = 'Top,Right'
$btnGo.Text = 'Ouvrir'; $btnGo.Location = '658,33'; $btnGo.Size = '110,26'; $btnGo.Anchor = 'Top,Right'
$tree = New-Object System.Windows.Forms.TreeView
$tree.Location = '12,68'; $tree.Size = '736,436'
$tree.Location = '12,68'; $tree.Size = '756,448'
$tree.Anchor = 'Top,Bottom,Left,Right'
$tree.CheckBoxes = (-not $SingleSelection)
$tree.HideSelection = $false
$tree.ShowLines = $true
$btnRefresh = New-Object System.Windows.Forms.Button
$btnRefresh.Text = 'Actualiser'; $btnRefresh.Location = '12,512'; $btnRefresh.Size = '110,28'; $btnRefresh.Anchor = 'Bottom,Left'
$btnServer = New-Object System.Windows.Forms.Button
$btnServer.Text = 'Ajouter un serveur...'; $btnServer.Location = '130,512'; $btnServer.Size = '160,28'; $btnServer.Anchor = 'Bottom,Left'
$btnConnect = New-Object System.Windows.Forms.Button
$btnConnect.Text = 'Connecter un partage...'; $btnConnect.Location = '298,512'; $btnConnect.Size = '170,28'; $btnConnect.Anchor = 'Bottom,Left'
$lblStatus = New-Object System.Windows.Forms.Label
$lblStatus.Location = '12,524'; $lblStatus.Size = '756,18'; $lblStatus.Anchor = 'Bottom,Left,Right'
$lblStatus.ForeColor = [System.Drawing.Color]::DimGray
$chkUnc = New-Object System.Windows.Forms.CheckBox
$chkUnc.Text = 'Utiliser les chemins UNC pour les lecteurs réseau (recommandé en mode Administrateur)'
$chkUnc.Location = '12,544'; $chkUnc.Size = '560,22'; $chkUnc.Checked = $true; $chkUnc.Anchor = 'Bottom,Left'
$chkUnc.Location = '12,548'; $chkUnc.AutoSize = $true; $chkUnc.Checked = $true; $chkUnc.Anchor = 'Bottom,Left'
$lblStatus = New-Object System.Windows.Forms.Label
$lblStatus.Location = '12,568'; $lblStatus.Size = '520,18'; $lblStatus.Anchor = 'Bottom,Left'
$lblStatus.ForeColor = [System.Drawing.Color]::DimGray
$btnRefresh = New-Object System.Windows.Forms.Button
$btnRefresh.Text = 'Actualiser'; $btnRefresh.Location = '12,578'; $btnRefresh.Size = '110,30'; $btnRefresh.Anchor = 'Bottom,Left'
$btnServer = New-Object System.Windows.Forms.Button
$btnServer.Text = 'Ajouter un serveur...'; $btnServer.Location = '130,578'; $btnServer.Size = '160,30'; $btnServer.Anchor = 'Bottom,Left'
$btnConnect = New-Object System.Windows.Forms.Button
$btnConnect.Text = 'Connecter un partage...'; $btnConnect.Location = '298,578'; $btnConnect.Size = '170,30'; $btnConnect.Anchor = 'Bottom,Left'
$btnOk = New-Object System.Windows.Forms.Button
$btnOk.Text = 'Valider'; $btnOk.Size = '104,30'; $btnOk.Location = '542,552'; $btnOk.Anchor = 'Bottom,Right'
$btnOk.Text = 'Valider'; $btnOk.Size = '104,30'; $btnOk.Location = '552,578'; $btnOk.Anchor = 'Bottom,Right'
$btnOk.DialogResult = [System.Windows.Forms.DialogResult]::OK
$btnCancel = New-Object System.Windows.Forms.Button
$btnCancel.Text = 'Annuler'; $btnCancel.Size = '104,30'; $btnCancel.Location = '654,552'; $btnCancel.Anchor = 'Bottom,Right'
$btnCancel.Text = 'Annuler'; $btnCancel.Size = '104,30'; $btnCancel.Location = '664,578'; $btnCancel.Anchor = 'Bottom,Right'
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$dlg.Controls.AddRange(@($lblDesc, $txtPath, $btnGo, $tree, $btnRefresh, $btnServer,
@@ -3612,7 +3692,19 @@ function Show-FilerFolderPicker {
$r = Resolve-FilerScanPath -Path $p -PreferUnc:$preferUnc -Inventory $inv
if ($r.Path -and $seen.Add($r.Path)) { [void]$out.Add([string]$r.Path) }
}
return $out.ToArray()
# Ticking a folder and one of its parents would walk the same tree twice and
# count its bytes twice in the totals. Shortest paths first, then drop every
# path already covered by one that was kept.
$keep = New-Object System.Collections.ArrayList
foreach ($p in @($out | Sort-Object -Property Length)) {
$nested = $false
foreach ($k in $keep) {
if ($p.StartsWith(($k.TrimEnd('\') + '\'), [System.StringComparison]::OrdinalIgnoreCase)) { $nested = $true; break }
}
if (-not $nested) { [void]$keep.Add($p) }
}
return $keep.ToArray()
}
# ---- events ----
@@ -3632,8 +3724,19 @@ $btnAdd.Add_Click({
if ($added -gt 0) { $statusLbl.Text = "$added dossier(s) ajouté(s)." }
})
$btnRemove.Add_Click({
if ($lstFolders.SelectedIndex -ge 0) { $lstFolders.Items.RemoveAt($lstFolders.SelectedIndex) }
# Remove every selected row, highest index first so the earlier ones keep theirs.
$removeSelectedFolders = {
foreach ($i in @($lstFolders.SelectedIndices | Sort-Object -Descending)) { $lstFolders.Items.RemoveAt($i) }
}
$btnRemove.Add_Click($removeSelectedFolders)
$lstFolders.Add_KeyDown({
param($s, $e)
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Delete -and $btnRemove.Enabled) {
& $removeSelectedFolders
}
})
$lstFolders.Add_SelectedIndexChanged({
if ($btnAdd.Enabled) { $btnRemove.Enabled = ($lstFolders.SelectedIndices.Count -gt 0) }
})
$btnScan.Add_Click({
@@ -3797,4 +3900,10 @@ if ($pendingJournals.Count -gt 0) {
if ($Path) { foreach ($p in $Path) { [void]$lstFolders.Items.Add($p) } }
[void]$form.ShowDialog()
$timer.Stop(); $timer.Dispose()
$exportTimer.Stop(); $exportTimer.Dispose()
if ($script:PermMenu) { $script:PermMenu.Dispose(); $script:PermMenu = $null }
$script:BarTrack.Dispose(); $script:BarFill.Dispose(); $script:BarEdge.Dispose(); $script:BarSel.Dispose()
$tip.Dispose()
$form.Dispose()