Classes/Classes.psm1

#region Classes and enums

# We define these enums to provide PS5 compatible tab completion and validation for the public functions.
# The Pester tests ensure that they correspond to the actual definitions.

enum EffectName {
    Animation
    ClientAreaAnimation
    ComboBoxAnimation
    CursorShadow
    DragFullWindows
    DropShadow
    EnableAeroPeek
    FontSmoothing
    IconShadow
    ListBoxSmoothScrolling
    MenuAnimation
    SelectionFade
    ShowThumbnails
    TaskbarAnimations
    TaskbarThumbnailCache
    ToolTipAnimation
    TranslucentSelect
}

enum EffectPreset {
    NoAnimation
    Performance
}

<# There are two types of effects: API-based and registry-based. We use a class for each type so we can handle
   get/set operations using a common interface.
#>


# Base class
class VisualEffect {
    # The common properties and methods that the child classes should have
    [string] $Name
    [bool]   $Enabled
    [string] $Description
    hidden [string] $Type

    [void] Refresh () {
        throw 'You must override this method'
    }

    [void] Set ([bool] $Enabled, [BroadcastMessage] $Broadcast) {
        throw 'You must override this method'
    }

    VisualEffect () {
        if ($this.GetType() -eq [VisualEffect]) {
            throw 'You cannot instantiate this base class'
        }
    }
}

# Child class that describes registry-based effects
class VisualEffectRegistry : VisualEffect {
    hidden [string]   $Key
    hidden [string]   $Value
    hidden [psobject] $DataOff   = 0
    hidden [psobject] $DataOn    = 1
    hidden [string]   $Broadcast = 'VisualEffects'

    [void] Refresh () {
        $data = (Get-ItemProperty $this.Key).($this.Value)
        $this.Enabled = ($data -eq $this.DataOn)
    }

    [void] Set ([bool] $Enabled, [BroadcastMessage] $Broadcast) {
        $data = if ($Enabled) {$this.DataOn} else {$this.DataOff}
        Set-ItemProperty -Path $this.Key -Name $this.Value -Value $data
        $Broadcast.AddBroadcast($this.Broadcast)
    }
}

# Child class that describes API-based effects
class VisualEffectSPI : VisualEffect {
    hidden [int] $SpiGet
    hidden [int] $SpiSet
    hidden [int] $WinIni = [Win32.User]::SPIF_UPDATEINIFILE -bor [Win32.User]::SPIF_SENDCHANGE

    [void] Refresh () {
        $featureEnabled = $false
        $ret = [Win32.User]::GetSystemParametersInfoBool($this.SPIGet, 0, [ref]$featureEnabled, 0)
        if (-not $ret) {
            throw "Failed to get System Parameter for $($this.Name)"
        }
        $this.Enabled = $featureEnabled
    }

    [void] Set ([bool] $Enabled, [BroadcastMessage] $Broadcast) {
        $ret = [Win32.User]::SetSystemParametersInfoBool($this.SpiSet, 0, $Enabled, $this.WinIni)
        if (-not $ret) {
            throw "Failed to set System Parameter $($this.Name) to $Enabled"
        }
    }
}

# Special case for the Animation effect
class VisualEffectSPIAnimation : VisualEffectSPI {
    [void] Refresh () {
        $animationInfo = [Win32.User+ANIMATIONINFO]::new(0)
        $ret = [Win32.User]::SystemParametersInfoAnimation(
            $this.SPIGet, $animationInfo.cbSize, [ref]$animationInfo, 0)
        if (-not $ret) {
            throw "Failed to get System Parameter for $($this.Name)"
        }
        $this.Enabled = $animationInfo.iMinAnimate
    }

    [void] Set ([bool] $Enabled, [BroadcastMessage] $Broadcast) {
        $animationInfo = [Win32.User+ANIMATIONINFO]::new($Enabled)
        $ret = [Win32.User]::SystemParametersInfoAnimation(
            $this.SpiSet, $animationInfo.cbSize, [ref]$animationInfo, $this.WinIni)
        if (-not $ret) {
            throw "Failed to set System Parameter $($this.Name) to $Enabled"
        }
    }
}

# Effects that make changes through the uiParam parameter
class VisualEffectSPIAltParam : VisualEffectSPI {
    [void] Set ([bool] $Enabled, [BroadcastMessage] $Broadcast) {
        # Oddly, setting these effects requires a different parameter order
        $ret = [Win32.User]::SetSystemParametersInfoBool($this.SpiSet, [int]$Enabled, $null, $this.WinIni)
        if (-not $ret) {
            throw "Failed to set System Parameter $($this.Name) to $Enabled"
        }
    }
}

# Class that tracks and sends broadcast messages
class BroadcastMessage {
    # The broadcasts that need to be sent after all settings have been updated
    [hashtable] $Broadcast = @{}

    # Add a broadcast message to the list
    [void] AddBroadcast ([string] $Message) {
        $this.Broadcast[$Message] = $true
    }

    # Send a broadcast
    [void] SendBroadcast ([string] $Message) {
        $hwndBroadcast   = [IntPtr] 0xffff
        $wmSettingChange = 0x1a
        $smToAbortIfHung = 0x0002
        $timeout         = 5000
        $result          = [UIntPtr]::Zero

        [Win32.User]::SendMessageTimeout($hwndBroadcast, $wmSettingChange, [UIntPtr]::Zero, $Message,
            $smToAbortIfHung, $timeout, [ref]$result) >$null
    }

    # Send the collected broadcasts so changes take effect immediately
    [void] SendAll () {
        # The "VisualEffects" message should always be sent
        $this.AddBroadcast('VisualEffects')
        $this.Broadcast.Keys | foreach {$this.SendBroadcast($_)}
    }
}

# Factory class that generates VisualEffect instances
class VisualEffectFactory {
    # The definitions of the visual effects, preloaded in the static constructor
    static [hashtable] $Definitions

    # Create a new class instance for the named effect
    static [VisualEffect] NewInstance ([string] $Name) {
        $definition = $([VisualEffectFactory]::Definitions[$Name])
        $instance = $definition -as $definition.Type
        $instance.Refresh()
        return $instance
    }

    # Static constructor to import the definitions (runs automatically before the creation of the first instance)
    static VisualEffectFactory () {
        $jsonData = Get-Content "$PSScriptRoot\..\Definitions\*.json" -Raw | ConvertFrom-Json
        [VisualEffectFactory]::Definitions = $jsonData.List | Group-Object Name -AsHashTable -AsString
    }
}
#endregion