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 # Hash to map programmatic element identifiers to element references. $IdElementMap = @{} # 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 = @{} #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 ) { [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) { "Model" { AssertStates ([ParseState]::Start) # outermost XML element, allows us to have non-UI-elements $state = [ParseState]::Model break } "CustomProperty" { 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 } } } 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 } "Element" { 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 } "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 } "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 } "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 |