YellowBox.Modeling.psm1
#region Types enum ParseState { Start Model Element } class ScriptTimer { ScriptTimer() { $this.modelTimer = [YellowBox.Provider.ModelTimer]::new() $this.sourceIdentifier = "_YB_TimerElapsed{0:D8}" -f [ScriptTimer]::timerId++ [ScriptTimer]::instances[$this.sourceIdentifier] = $this Register-ObjectEvent -InputObject $this.modelTimer -EventName "Elapsed" -SourceIdentifier $this.sourceIdentifier } [void] Dispose() { if (!$this.disposed) { $this.modelTimer.Dispose() Unregister-Event -SourceIdentifier $this.sourceIdentifier $this.disposed = $true } } [void] SetInterval($interval) { $this.modelTimer.Interval = $interval } [void] Start() { $this.modelTimer.Enabled = $true } [void] Stop() { $this.modelTimer.Enabled = $false } [scriptblock] $Elapsed hidden $modelTimer hidden [string] $sourceIdentifier hidden [bool] $disposed static [void] DisposeAllInstances() { foreach ($instance in [ScriptTimer]::instances.Values) { $instance.Dispose() } } static [void] OnElapsed($sourceIdentifier) { if ([ScriptTimer]::instances.ContainsKey($sourceIdentifier)) { [ScriptTimer]::instances[$sourceIdentifier].Elapsed.Invoke() } } static hidden [uint32] $timerId static hidden $instances = @{} } #endregion #region Global Variables $ScriptBaseName = (ls $MyInvocation.MyCommand.Path).BaseName $ScriptDirectoryName = (ls $MyInvocation.MyCommand.Path).DirectoryName #endregion #region Helper methods function Assert($Condition, $Message = "assert") { if (!$Condition) { throw $Message } } <# .SYNOPSIS Parses a rectangle from a text representation of rectangle coordinates. .PARAMETER RectString Text representation of rectangle coordinates.. .OUTPUT Rectangle. #> function Parse-Rect([string] $RectString) { $parts = $RectString.Split(',') [double] $x = [double]::Parse($parts[0]) [double] $y = [double]::Parse($parts[1]) [double] $width = [double]::Parse($parts[2]) [double] $height = [double]::Parse($parts[3]) New-Object System.Windows.Rect -ArgumentList $x, $y, $width, $height } <# .SYNOPSIS Parses width or height information from a text representation. .PARAMETER WidthOrHeightString Text representation of a width or height value followed by a unit. Examples: "40%" Forty percent of the width/height of the container. "20px" Twenty pixels. "*" Equal part of the width/height remaining after pixel or percentage claims have been assigned. .OUTPUT Pair of quantity and unit values. #> function Parse-WidthOrHeight([string] $WidthOrHeightString) { switch -Regex ($WidthOrHeightString) { "^(\d+(?:\.\d+)?)%$" { ([double]::Parse($Matches[1])/100), ([YellowBox.Provider.LayoutUnit]::Percent) } "^(\d+(?:\.\d+)?)px$" { [double]::Parse($Matches[1]), ([YellowBox.Provider.LayoutUnit]::Pixel)} "^\*$" { 0, ([YellowBox.Provider.LayoutUnit]::ShareRemainder) } } } <# .SYNOPSIS Parses row/column count and heigth/width information from a textual representation. .PARAMETER Dimension By reference. List to which the function appends height/width information for rows/columns. .PARAMETER DimensionString Textual representation of row/column count and heigth/width. Examples: "3" Three rows/columns, each with "ShareRemainder" height/width. "*,*,*" Three rows/columns, each with "ShareRemainder" height/width. "*, 40%, *" Three rows/columns, the 2nd occupying 40% of the height/width, the others have "ShareRemainder" height/width. "20px, *, *" Three rows/columns, the first is 20 pixels high/wide, the others have "ShareRemainder" height/width. .OUTPUT None (function modifies 'Dimension' argument). #> function Parse-TableDimension([System.Collections.Generic.List`1[System.Tuple`2[double, YellowBox.Provider.LayoutUnit]]] $Dimension, [string] $DimensionString) { switch -Regex ($DimensionString) { "^\d+$" { for ($i = 0; $i -lt [int]::Parse($DimensionString); ++$i) { $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList 0, ([YellowBox.Provider.LayoutUnit]::ShareRemainder))) } } default { foreach ($part in ($DimensionString -split ",")) { switch -Regex ($part.Trim()) { "^\*$" { $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList 0, ([YellowBox.Provider.LayoutUnit]::ShareRemainder))) } "^(\d+)%$" { [double] $value = [double]::Parse($Matches[1]) $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList $value, ([YellowBox.Provider.LayoutUnit]::Percent))) } "^(\d+)px$" { [double] $value = [double]::Parse($Matches[1]) $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList $value, ([YellowBox.Provider.LayoutUnit]::Pixel))) } } } } } } <# .SYNOPSIS Gets the closest ancestral Grid pattern. .PARAMETER CurrentElement Element to start the search at. .OUTPUT Grid pattern if the ancestor chain contains an element supporting the Grid pattern. Otherwise an exception is being thrown. #> function ClosestAncestralGridPattern($CurrentElement) { for ($CurrentElement = $CurrentElement.Parent; $CurrentElement -ne $null; $CurrentElement = $CurrentElement.Parent) { $gridPattern = $null if ($CurrentElement.Patterns.TryGetValue(([YellowBox.PatternId]::Grid), ([ref]$gridPattern))) { return $gridPattern } } throw "no ancestral Grid pattern" } <# .SYNOPSIS Parses numeric RuntimeId from a textual representation. .PARAMETER RuntimeIdString Textual representation of RuntimeId. .OUTPUT Numeric representation of RuntimeId. .DESCRIPTION To avoid collisions with explicitly assigned RuntimeId value, we're using the upper half of the positive integer range. #> function Parse-RuntimeId([string] $RuntimeIdString) { $id = [int]::Parse($RuntimeIdString) if ($id -gt [int]::MaxValue / 2) { throw "RuntimeId $RuntimeIdString encroaches on range reserved for implicitly assigned RuntimeIds" } $id } [int] $__CurrentImplicitRuntimeId_value = [int]::MaxValue <# .SYNOPSIS Gets a value for an implicitly assigned RuntimeId. .OUTPUT Value for an implicitly assigned RuntimeId. .DESCRIPTION To simplify AccML authoring, the Id attribute on Element nodes is optional. If absent, the RuntimeId value returned from this function is being used. To avoid collisions with explicitly assigned RuntimeId value, we're using the upper half of the positive integer range. #> function CurrentImplicitRuntimeId() { if ($script:__CurrentImplicitRuntimeId_value -le [int]::MaxValue / 2) { throw "exhausted range reserved for implicitly assigned RuntimeIds" } ($script:__CurrentImplicitRuntimeId_value--) } function ConvertToAutomationType ([YellowBox.AutomationType] $Type, [string] $Value) { switch($Type) { # in AutomationType enumeration order ([YellowBox.AutomationType]::Int) { return [int]::Parse($Value) } ([YellowBox.AutomationType]::Bool) { return [bool]::Parse($Value) } ([YellowBox.AutomationType]::String) { return $Value } ([YellowBox.AutomationType]::Double) { return [double]::Parse($Value) } default { throw "conversion to $Type not yet implemented" } } } function CreateTimer() { return [ScriptTimer]::new() } #endregion function Show-UiaModel( [parameter(Mandatory = $true)][string] $ModelFile, [switch] $Render = $true ) { # Hash to map element identifiers to element references. $IdElementMap = @{} # Hash to map custom annotation names to identifiers $CustomAnnotationTypes = @{} # List of operations to be executed after all the XML data has been read (e.g. patching forward # references). [ScriptBlock[]] $PatchOperations = @() # Custom properties. Key is the markup property name, value a tuple containing type information # and the custom property identifier resulting from custom property registration. $CustomPropertyMap = @{} [YellowBox.Provider.ElementProvider] $rootElement = $null [YellowBox.Provider.ElementProvider] $currentElement = $null [string] $closedHandler = $null [string] $closingHandler = $null [string] $keyPressHandler = $null [string] $shownHandler = $null [xml] $xml = Get-Content $ModelFile $currXml = $xml.FirstChild [ParseState] $state = [ParseState]::Start function AssertStates([ParseState[]] $ExpectedStates) { foreach ($expectedState in $ExpectedStates) { if ($state -eq $expectedState) { return } } throw "Expected states ($($ExpectedStates -join ', ')), actual state $state" } $cont = $true while ($cont) { # skip over comments, white space etc. Is there a navigation mode that can be used to do this? if ($currXml.NodeType -eq ([System.Xml.XmlNodeType]::Element)) { switch ($currXml.LocalName) { "Annotation" { # Creates an Annotation pattern. # # Parents: # Element: The element supporting the annotation pattern. # Attributes: # Target: Id of the element that is targeted by the annotation. # TypeId: Annotation type identifier, represented as the name of a member of the AnnotationTypes enum. # Author: Name of the annotation author. # Children: # (none) $annotation = [YellowBox.Provider.AnnotationProvider]::new() $annotation.TypeId = [YellowBox.AnnotationType]::Unknown foreach ($attribute in $currXml.Attributes) { switch ($attribute.Name) { "Author" { $annotation.Author = $attribute.Value; break } "Target" { [string] $targetId <# something we can capture by value #> = $attribute.Value # delayed execution to enable forward references $PatchOperations += { $annotation.Target = $IdElementMap[$targetId] }.GetNewClosure() break } "TypeId" { $annotationType = $attribute.Value -as [YellowBox.AnnotationType] if ($annotationType) { $annotation.TypeId = $annotationType } elseif ($CustomAnnotationTypes.ContainsKey($attribute.Value)) { $annotation.TypeId = $CustomAnnotationTypes[$attribute.Value] } else { throw "unknown annotation type '$($attribute.Value)'" } break } "TypeName" { $annotation.TypeName = $attribute.Value; break } default { throw "unexpected Annotation attribute '$($attribute.Name)'"} } } $currentElement.Patterns.Add(([YellowBox.PatternId]::Annotation), $annotation) break } "AnnotationProperty" { # Creates an Annotation property of an Element. # # An Element can have multiple AnnotationProperty children. Each one contributes to the arrays returned # via the UIA_AnnotationTypesPropertyId and UIA_AnnotationObjectsPropertyId properties. # # Parents: # Element: Element to which the annotation property is assigned. An Element can have 0 or more # annotation properties. # Attributes: # Type: Annotation type, represented as the name of a member of the AnnotationTypes enum. # Element: Optional. ID of the element representing the annotation. # Children: # (none) AssertStates ([ParseState]::Element) # Note that the 'System.Tuple.ItemX' accessors are read-only, meaning the respective values need to be passed # to the tuple constructor. The code below is structured to account for this. $annotation = $null if ($currXml.Type) { $annotationType = $currXml.Type -as [YellowBox.AnnotationType] if ($annotationType) { $annotation = [Tuple[int, WeakReference[YellowBox.Provider.ElementProvider]]]::new($annotationType, [WeakReference[YellowBox.Provider.ElementProvider]]::new($null)) } elseif ($CustomAnnotationTypes.ContainsKey($currXml.Type)) { $annotation = [Tuple[int, WeakReference[YellowBox.Provider.ElementProvider]]]::new($CustomAnnotationTypes[$currXml.Type], [WeakReference[YellowBox.Provider.ElementProvider]]::new($null)) } else { throw "unknown annotation type '$($currXml.Type)'" } } foreach ($attribute in $currXml.Attributes) { switch ($attribute.Name) { "Type" { <# handled above #> } "Element" { # in case there is no 'Type' attribute that prompted the creation above if ($null -eq $annotation) { $annotation = [Tuple[int, WeakReference[YellowBox.Provider.ElementProvider]]]::new([YellowBox.AnnotationType]::Unknown, [WeakReference[YellowBox.Provider.ElementProvider]]::new($null)) } [string] $annotatorId = $attribute.Value # value for lambda capture below # delayed execution to enable forward references $PatchOperations += { $annotation.Item2.SetTarget($IdElementMap[$annotatorId]) }.GetNewClosure() break } default { throw "unexpected 'AnnotationProperty' attribute '$($attribute.Name)'" } } } if ($null -eq $annotation) { throw "'AnnotationProperty' element needs at least one 'Type' or one 'Element' attribute" } $currentElement.Annotations.Add($annotation) } "CustomAnnotationType" { # Registers a custom annotation type. # # The registration of a custom annotation type results in a name that can be used in places where annotation types can be specified. # # Parents: # Model: Custom annotation types must be defined at the top level of the model. # Attributes: # Guid: GUID of the custom annotation type. # Name: Symbolic name that can subsequently be used in places where annotation types can be specified. # None of the existing annotation type names (see YellowBox.AnnotationType) can be used. # Children: # (none) AssertStates ([ParseState]::Model) $guid = $null $name = $null foreach ($attribute in $currXml.Attributes) { switch ($attribute.Name) { "Guid" { $guid = [System.Guid]::Parse($attribute.Value); break } "Name" { $name = $attribute.Value if ($name -as [YellowBox.AnnotationType]) { throw "custom annotation type name '$name' matches the name of a standard annotation type"} if ($CustomAnnotationTypes.ContainsKey($name)) { throw "a custom annotation type with the name '$name' has already been defined" } break } default {throw "unexpected 'CustomAnnotationType' attribute '$($attribute.Name)'"} } } if ($guid -eq $null) { throw "'CustomAnnotationType' element is missing 'Guid' attribute" } if ($name -eq $null) { throw "'CustomAnnotationType' element is missing 'Name' attribute" } $id = (Register-UiaCustomAnnotationType -Guid $guid).LocalId $CustomAnnotationTypes[$name] = $id break } "CustomProperty" { # Registers a custom property. # # Custom property registration results in a name that subsequently can be used as an attribute # name on Element elements. # # Parents: # Model # Attributes: # Guid: Custom property GUID. # Name: Programmatic name of the custom property (passed on to UIA, irrelevant for the purposes of this script). # Type: Custom property type, expressed as the name of an YellowBox.AutomationType enum member. # MarkupName: Name of the attribute through which the custom property can be assigned to Element elements. # Children: # (none) AssertStates ([ParseState]::Model) $guid = $null $programmaticName = $null $type = $null $markupName = $null foreach ($attribute in $currXml.Attributes) { switch ($attribute.Name) { "Guid" { $guid = [System.Guid]::Parse($attribute.Value); break } "Name" { $programmaticName = $attribute.Value; break } "Type" { $type = [System.Enum]::Parse([YellowBox.AutomationType], $attribute.Value); break } "MarkupName" { $markupName = $attribute.Value; break } default {throw "unexpected 'CustomProperty' attribute '$($attribute.Name)'"} } } if ($guid -eq $null) { throw "'CustomProperty' element is missing 'Guid' attribute" } if ($programmaticName -eq $null) { throw "'CustomProperty' element is missing 'Name' attribute" } if ($type -eq $null) { throw "'CustomProperty' element is missing 'Type' attribute" } if ($markupName -eq $null) { $markupName = $programmaticName } $id = Register-UiaCustomProperty -Guid $guid -ProgrammaticName $programmaticName -Type $type $CustomPropertyMap[$markupName] = @{ Id = $id; Type = $type } break } "CustomNavigation" { $customNavigation = New-Object YellowBox.Provider.CustomNavigationProvider foreach($attribute in $currXml.Attributes) { [string] $targetElementId = $attribute.Value $PatchOperations += switch ($attribute.Name) { "Parent" { { $customNavigation.Parent = $IdElementMap[$targetElementId] }.GetNewClosure(); break } "PreviousSibling" { { $customNavigation.PreviousSibling = $IdElementMap[$targetElementId] }.GetNewClosure(); break } "NextSibling" { { $customNavigation.NextSibling = $IdElementMap[$targetElementId] }.GetNewClosure(); break } "FirstChild" { { $customNavigation.FirstChild = $IdElementMap[$targetElementId] }.GetNewClosure(); break } "LastChild" { { $customNavigation.LastChild = $IdElementMap[$targetElementId] }.GetNewClosure(); break } default { throw "invalid attribute '$($attribute.Name)'" } } } $currentElement.Patterns.Add(([YellowBox.PatternId]::CustomNavigation), $customNavigation) break } "Element" { # UI Element. # # Attributes: # AcceleratorKey: # AccessKey: # AutomationId: # ... # Children: # AnnotationProperty # ... AssertStates ([ParseState]::Model), ([ParseState]::Element) $state = ([ParseState]::Element) $newElement = $null [int] $runtimeId = if ($currXml.HasAttribute("RuntimeId")) { Parse-RuntimeId $currXml.Id } else { CurrentImplicitRuntimeId } if ($rootElement -eq $null) { $newElement = [YellowBox.Provider.RootProvider]::new($runtimeId) $rootElement = $newElement } else { $newElement = [YellowBox.Provider.ElementProvider]::new($runtimeId) } if ($currentElement -ne $null) { $currentElement.Children.Add($newElement) } foreach ($attribute in $currXml.Attributes) { switch ($attribute.Name) { "AcceleratorKey" { $newElement.AcceleratorKey = $attribute.Value; break } "AccessKey" { $newElement.AccessKey = $attribute.Value; break } "AutomationId" { $newElement.AutomationId = $attribute.Value; break } "BoundingRect" { $newElement.BoundingRectangle = Parse-Rect $attribute.Value; break } "ClassName" { $newElement.ClassName = $attribute.Value; break } "ControlType" { $newElement.ControlType = [System.Enum]::Parse([YellowBox.ControlType], $attribute.Value, $true); break } "HasKeyboardFocus" { $newElement.HasKeyboardFocus = [bool]::Parse($attribute.Value); break } "Id" { $id = $attribute.Value if ($id -notmatch "[a-zA-Z_]\w*") { throw "'$id' is not a valid identifier" } if ((Get-Variable -Scope "Script" -Name $id -ErrorAction Ignore) -ne $null) { throw "an object with identifier '$id' already exists" } Set-Variable -Scope "Script" -Name $id -Value $newElement $IdElementMap[$id] = $newElement } "IsContentElement" { $newElement.IsContentElement = [bool]::Parse($attribute.Value); break } "IsControlElement" { $newElement.IsControlElement = [bool]::Parse($attribute.Value); break } "IsEnabled" { $newElement.IsEnabled = [bool]::Parse($attribute.Value); break } "IsKeyboardFocusable" { $newElement.IsKeyboardFocusable = [bool]::Parse($attribute.Value); break } "LandmarkType" { $newElement.LandmarkType = [System.Enum]::Parse([YellowBox.LandmarkType], $attribute.Value, $true); break } "Layout" # alternatively, "Layout.Type" { switch ($attribute.Value) { "HorizontalStack" { $newElement.Layout = New-Object YellowBox.Provider.HorizontalStackLayout } "VerticalStack" { $newElement.Layout = New-Object YellowBox.Provider.VerticalStackLayout } "Table" { $table = New-Object YellowBox.Provider.TableLayout Parse-TableDimension $table.RowSpecs $currXml.'Layout.Rows' Parse-TableDimension $table.ColSpecs $currXml.'Layout.Columns' $newElement.Layout = $table } } } "Layout.Column" { [UInt32] $col = [UInt32]::Parse($attribute.Value) if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout])) { throw "'Column' attribute requires 'Table' parent layout" } if ($col -ge $newElement.Parent.Layout.ColSpecs.Count) { throw "'Column' attribute value must be less than the column count indicated by parent's 'Columns' attribute" } if ($newElement.LayoutItem -eq $null) { $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem } $newElement.LayoutItem.Column = $col } "Layout.ColumnSpan" { [UInt32] $colSpan = [UInt32]::Parse($attribute.Value) if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout])) { throw "'ColumnSpan' attribute requires 'Table' parent layout" } if ($colSpan -ge $newElement.Parent.Layout.ColSpecs.Count) { throw "'ColumnSpan' attribute value must be less than the column count indicated by parent's 'Columns' attribute" } if ($newElement.LayoutItem -eq $null) { $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem } $newElement.LayoutItem.ColumnSpan = $colSpan } "Layout.Height" { if (!($newElement.Parent.Layout -is [YellowBox.Provider.VerticalStackLayout])) { throw "'Height' attribute requires 'VerticalStack' parent layout" } $newElement.LayoutItem = New-Object YellowBox.Provider.VerticalStackLayoutItem -ArgumentList (Parse-WidthOrHeight $attribute.Value) } "Layout.Row" { [UInt32] $row = [UInt32]::Parse($attribute.Value) if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout])) { throw "'Row' attribute requires 'Table' parent layout" } if ($row -ge $newElement.Parent.Layout.RowSpecs.Count) { throw "'Row' attribute value must be less than the row count indicated by parent's 'Rows' attribute" } if ($newElement.LayoutItem -eq $null) { $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem } $newElement.LayoutItem.Row = $row } "Layout.RowSpan" { [UInt32] $rowSpan = [UInt32]::Parse($attribute.Value) if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout])) { throw "'RowSpan' attribute requires 'Table' parent layout" } if ($rowSpan -ge $newElement.Parent.Layout.RowSpecs.Count) { throw "'RowSpan' attribute value must be less than the row count indicated by parent's 'Rows' attribute" } if ($newElement.LayoutItem -eq $null) { $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem } $newElement.LayoutItem.RowSpan = $rowSpan } "Layout.Width" { if (!($newElement.Parent.Layout -is [YellowBox.Provider.HorizontalStackLayout])) { throw "'Width' attribute requires 'HorizontalStack' parent layout" } $newElement.LayoutItem = New-Object YellowBox.Provider.HorizontalStackLayoutItem -ArgumentList (Parse-WidthOrHeight $attribute.Value) } "Level" { $newElement.Level = [int]::Parse($attribute.Value); break } "LocalizedLandmarkType" { $newElement.LocalizedLandmarkType = $attribute.Value; break } "Name" { $newElement.Name = $attribute.Value; break } "NameRequested" { Register-ObjectEvent -InputObject $newElement -EventName 'NameRequested' -SourceIdentifier '_YB_MethodCalled' -MessageData "$($attribute.Value); `$Event.SourceEventArgs.Return()" break } "OnClosed" { if ($newElement -ne $rootElement) { throw "'OnClosed' attribute only supported on root element" } $closedHandler = $attribute.Value break } "OnClosing" { if ($newElement -ne $rootElement) { throw "'OnClosing' attribute only supported on root element" } $closingHandler = $attribute.Value break } "OnKeyPress" { if ($newElement -ne $rootElement) { throw "'OnKeyPress' attribute only supported on root element" } $keyPressHandler = $attribute.Value break } "OnShown" { if ($newElement -ne $rootElement) { throw "'OnShown' attribute only supported on root element" } $shownHandler = $attribute.Value break } "PositionInSet" { $newElement.PositionInSet = [int]::Parse($attribute.Value); break } "SizeOfSet" { $newElement.SizeOfSet = [int]::Parse($attribute.Value); break } default { if ($CustomPropertyMap.ContainsKey($attribute.Name)) { $propertyInfo = $CustomPropertyMap[$attribute.Name] $newElement.SetProperty($propertyInfo.Id, (ConvertToAutomationType $propertyInfo.Type $attribute.Value)) } else { throw "Unknown 'Element' attribute '$($attribute.Name)'" } } } } break } "ExpandCollapse" { $expandCollapse = New-Object YellowBox.Provider.ExpandCollapseProvider if ($currXml.ExpandCollapseState -eq $null) { throw "<ExpandCollapse/> element must have at least a 'ExpandCollapseState' attribute" } else { $expandCollapse.ExpandCollapseState = $currXml.ExpandCollapseState } $currentElement.Patterns.Add(([YellowBox.PatternId]::ExpandCollapse), $expandCollapse) break } "Grid" { $grid = New-Object YellowBox.Provider.GridProvider $grid.RowCount = $currXml.RowCount $grid.ColumnCount = $currXml.ColumnCount $currentElement.Patterns.Add(([YellowBox.PatternId]::Grid), $grid) break } "GridItem" { $gridItem = New-Object YellowBox.Provider.GridItemProvider $gridItem.Row = $currXml.Row $gridItem.Column = $currXml.Column if($currXml.RowSpan -ne $null) { $gridItem.RowSpan = $currXml.RowSpan } if($currXml.ColumnSpan -ne $null) { $gridItem.ColumnSpan = $currXml.ColumnSpan } $currentElement.Patterns.Add(([YellowBox.PatternId]::GridItem), $gridItem) break } "Invoke" { $invoke = New-Object YellowBox.Provider.InvokeProvider $currentElement.Patterns.Add(([YellowBox.PatternId]::Invoke), $invoke) break } "Model" { AssertStates ([ParseState]::Start) # outermost XML element, allows us to have non-UI-elements $state = [ParseState]::Model break } "Script" { # dot-source instead to allow <Script/> blocks to define functions that can be called # later? Invoke-Expression $currXml.InnerText break } "Selection" { $selection = New-Object YellowBox.Provider.SelectionProvider $selection.CanSelectMultiple = $currXml.CanSelectMultiple $selection.IsSelectionRequired = $currXml.IsSelectionRequired $currentElement.Patterns.Add(([YellowBox.PatternId]::Selection), $selection) break } "SelectionItem" { $selectionItem = New-Object YellowBox.Provider.SelectionItemProvider $currentElement.Patterns.Add(([YellowBox.PatternId]::SelectionItem), $selectionItem) if ([bool]::Parse($currXml.IsSelected)) { $selectionItem.AddToSelection() } break } "Spreadsheet" { $spreadsheet = New-Object YellowBox.Provider.SpreadsheetProvider $currentElement.Patterns.Add(([YellowBox.PatternId]::Spreadsheet), $spreadsheet) break } "SpreadsheetItem" { $spreadsheetItem = New-Object YellowBox.Provider.SpreadsheetItemProvider if ($currXml.HasAttribute("Formula")) { $spreadsheetItem.Formula = $currXml.Formula } $currentElement.Patterns.Add(([YellowBox.PatternId]::SpreadsheetItem), $spreadsheetItem) break } "Table" { $table = New-Object YellowBox.Provider.TableProvider # TODO: parse and pass on RowOrColumnMajor value if ($currXml.RowHeaders -eq $null -and $currXml.ColumnHeaders -eq $null) { throw "<Table/> element must have at least one of the 'RowHeaders' or 'ColumnHeaders' attributes" } if ($currXml.RowHeaders -ne $null) { if ($currXml.RowHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$") { # interpret RowHeaders attribute as a list of (rowIndex, columnIndex) # coordinate pairs $currXml.RowHeaders.Trim("()") -split "\)\s*\(" | %{ $arr = $_ -split "," $row = [uint32]::Parse($arr[0]) $col = [uint32]::Parse($arr[1]) $PatchOperations += { $table.RowHeaders.Add($currentElement.Patterns[([YellowBox.PatternId]::Grid)].GetItem($row, $col)) }.GetNewClosure() } } else { # interpret RowHeaders attribute as a list element IDs $currXml.RowHeaders -split "," | %{ $_.Trim() } | %{ $id = $_ # delayed execution to enable forward references $PatchOperations += { $table.RowHeaders.Add($IdElementMap[$id]) }.GetNewClosure() } } } if ($currXml.ColumnHeaders -ne $null) { if ($currXml.ColumnHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$") { # interpret ColumnHeaders attribute as a list of (rowIndex, columnIndex) # coordinate pairs $currXml.ColumnHeaders.Trim("()") -split "\)\s*\(" | %{ $arr = $_ -split "," $row = [uint32]::Parse($arr[0]) $col = [uint32]::Parse($arr[1]) $PatchOperations += { $table.ColumnHeaders.Add($currentElement.Patterns[([YellowBox.PatternId]::Grid)].GetItem($row, $col)) }.GetNewClosure() } } else { # interpret RowHeaders attribute as a list element IDs $currXml.ColumnHeaders -split "," | %{ $_.Trim() } | %{ $id = $_ # delayed execution to enable forward references $PatchOperations += { $table.ColumnHeaders.Add($IdElementMap[$id]) }.GetNewClosure() } } } $currentElement.Patterns.Add(([YellowBox.PatternId]::Table), $table) break } "TableItem" { $tableItem = New-Object YellowBox.Provider.TableItemProvider if ($currXml.RowHeaders -eq $null -and $currXml.ColumnHeaders -eq $null) { throw "<TableItem/> element must have at least one of the 'RowHeaders' or 'ColumnHeaders' attributes" } if($currXml.RowHeaders -ne $null) { if ($currXml.RowHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$") { # interpret RowHeaders attribute as a list of (rowIndex, columnIndex) # coordinate pairs $currXml.RowHeaders.Trim("()") -split "\)\s*\(" | %{ $arr = $_ -split "," $row = [uint32]::Parse($arr[0]) $col = [uint32]::Parse($arr[1]) $gridPattern = ClosestAncestralGridPattern $currentElement $PatchOperations += { $tableItem.RowHeaderItems.Add($gridPattern.GetItem($row, $col)) }.GetNewClosure() } } else { # interpret RowHeaders attribute as a list element IDs $currXml.RowHeaders -split "," | %{ $_.Trim() } | %{ $id = $_ # delayed execution to enable forward references $PatchOperations += { $tableItem.RowHeaderItems.Add($IdElementMap[$id]) }.GetNewClosure() } } } if($currXml.ColumnHeaders -ne $null) { if ($currXml.ColumnHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$") { # interpret ColumnHeaders attribute as a list of (rowIndex, columnIndex) # coordinate pairs $currXml.ColumnHeaders.Trim("()") -split "\)\s*\(" | %{ $arr = $_ -split "," $row = [uint32]::Parse($arr[0]) $col = [uint32]::Parse($arr[1]) $gridPattern = ClosestAncestralGridPattern $currentElement $PatchOperations += { $tableItem.ColumnHeaderItems.Add($gridPattern.GetItem($row, $col)) }.GetNewClosure() } } else { # interpret ColumnHeaders attribute as a list element IDs $currXml.ColumnHeaders -split "," | %{ $_.Trim() } | %{ $id = $_ # delayed execution to enable forward references $PatchOperations += { $tableItem.ColumnHeaderItems.Add($IdElementMap[$id]) }.GetNewClosure() } } } $currentElement.Patterns.Add(([YellowBox.PatternId]::TableItem), $tableItem) break } "Text" { $text = New-Object YellowBox.Provider.TextProvider -ArgumentList $currXml.InnerText $currentElement.Patterns.Add(([YellowBox.PatternId]::Text), $text) break } "Toggle" { $toggle = New-Object YellowBox.Provider.ToggleProvider if ($currXml.ToggleState -ne $null) { $toggle.ToggleState = $currXml.ToggleState } if ($currXml.OnToggle -ne $null) { Register-ObjectEvent -InputObject $toggle -EventName 'ToggleCalled' -SourceIdentifier 'MethodCalled' -MessageData $currXml.OnToggle } $currentElement.Patterns.Add(([YellowBox.PatternId]::Toggle), $toggle) break } "Value" { $value = New-Object YellowBox.Provider.ValueProvider $value.IsReadOnly = $currXml.IsReadOnly $value.Value = $currXml.InnerText $currentElement.Patterns.Add(([YellowBox.PatternId]::Value), $value) break } } } # tree iteration logic $nextXml = $currXml.PSBase.FirstChild if ($nextXml -ne $null) { if ($currXml.LocalName -eq "Element") { if ($currentElement -ne $null) { $currentElement = $currentElement.Children[$currentElement.Children.Count - 1] } else { $currentElement = $rootElement } } $currXml = $nextXml continue } $nextXml = $currXml.PSBase.NextSibling if ($nextXml -ne $null) { $currXml = $nextXml continue } $ascend = $true while ($ascend) { $nextXml = $currXml.ParentNode if ($nextXml.NodeType -eq ([System.Xml.XmlNodeType]::Document)) { # we're back at the doc node $cont = $false # break out of outer loop break # break out of inner loop } if ($nextXml.LocalName -eq "Element") { $currentElement = $currentElement.Parent } $currXml = $nextXml $nextXml = $currXml.PSBase.NextSibling if ($nextXml -ne $null) { $currXml = $nextXml $ascend = $false } } } foreach ($patchOperation in $PatchOperations) { $patchOperation.Invoke() } $model = [YellowBox.Provider.Model]::new($rootElement, $Render) Register-ObjectEvent -InputObject $model.Form -EventName "Closed" -SourceIdentifier "_YB_ModelFormClosed" Register-ObjectEvent -InputObject $model.Form -EventName "Closing" -SourceIdentifier "_YB_ModelFormClosing" Register-ObjectEvent -InputObject $model.Form -EventName "KeyPress" -SourceIdentifier "_YB_ModelFormKeyPress" if ($shownHandler) { & $shownHandler $model } [bool] $cont = $true while ($cont) { $event = Wait-Event switch ($event.SourceIdentifier) { "_YB_ModelFormClosing" { # TODO: Accomodate CancelEventArgs # maybe via $event.SourceEventArgs.Return() if ($closingHandler) { & $closingHandler $event.SourceArgs } break } "_YB_ModelFormClosed" { if ($closedHandler) { & $closedHandler $event.SourceArgs } $cont = $false break } "_YB_ModelFormKeyPress" { if ($keyPressHandler) { & $keyPressHandler $event.SourceArgs } break } "MethodCalled" { Invoke-Expression $event.MessageData; break } default { if ($event.SourceIdentifier.StartsWith("_YB_TimerElapsed")) { [ScriptTimer]::OnElapsed($event.SourceIdentifier) } } } Remove-Event -EventIdentifier $event.EventIdentifier } Unregister-Event -SourceIdentifier "_YB_ModelFormClosed" Unregister-Event -SourceIdentifier "_YB_ModelFormClosing" Unregister-Event -SourceIdentifier "_YB_ModelFormKeyPress" Unregister-Event -SourceIdentifier "_YB_MethodCalled" -ErrorAction Ignore [ScriptTimer]::DisposeAllInstances() $model.Dispose() } Export-ModuleMember -Function Show-UiaModel |