diff --git a/.github/workflows/powershell-xaml-designer.yml b/.github/workflows/powershell-xaml-designer.yml index 07e51d8..977aa7f 100644 --- a/.github/workflows/powershell-xaml-designer.yml +++ b/.github/workflows/powershell-xaml-designer.yml @@ -9,22 +9,36 @@ jobs: validate: runs-on: windows-latest steps: - - uses: actions/checkout@v4 - - name: Parse PowerShell and XAML - shell: pwsh - run: .\Tests\Test-Repository.ps1 - - name: Verify WPF assemblies can load - shell: pwsh - run: | - Add-Type -AssemblyName WindowsBase - Add-Type -AssemblyName PresentationCore - Add-Type -AssemblyName PresentationFramework - [System.Windows.Window] | Out-String | Write-Host - - - name: Test generated PowerShell code - shell: pwsh - run: .\Tests\Test-CodeGeneration.ps1 - - - name: Smoke test designer startup in STA - shell: cmd - run: powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\Tests\Test-DesignerStartup.ps1 + - uses: actions/checkout@v5 + + - name: Parse repository with PowerShell 7 + shell: cmd + run: pwsh.exe -NoProfile -STA -File .\Tests\Test-Repository.ps1 + + - name: Parse repository with Windows PowerShell 5.1 + shell: cmd + run: powershell.exe -NoProfile -STA -File .\Tests\Test-Repository.ps1 + + - name: Core behavior tests with PowerShell 7 + shell: cmd + run: pwsh.exe -NoProfile -STA -File .\Tests\Test-DesignerCore.ps1 + + - name: Core behavior tests with Windows PowerShell 5.1 + shell: cmd + run: powershell.exe -NoProfile -STA -File .\Tests\Test-DesignerCore.ps1 + + - name: Generated code tests with PowerShell 7 + shell: cmd + run: pwsh.exe -NoProfile -STA -File .\Tests\Test-CodeGeneration.ps1 + + - name: Generated code tests with Windows PowerShell 5.1 + shell: cmd + run: powershell.exe -NoProfile -STA -File .\Tests\Test-CodeGeneration.ps1 + + - name: Designer startup smoke test with PowerShell 7 + shell: cmd + run: pwsh.exe -NoProfile -STA -File .\Tests\Test-DesignerStartup.ps1 + + - name: Designer startup smoke test with Windows PowerShell 5.1 + shell: cmd + run: powershell.exe -NoProfile -STA -File .\Tests\Test-DesignerStartup.ps1 diff --git a/README.md b/README.md index 2fa695e..991bf55 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,14 @@ instead of depending on `Click="Button1_Click"` in XAML. - PowerShell + WPF only; no Visual Studio and no third-party module required at runtime. - New / Open / Save / Save As for paired `.xaml` and `.ps1` files. - Runtime-discovered toolbox for public, instantiable WPF visual element types. -- Toolbox search and category filtering. +- Toolbox search plus a beginner-oriented Common category with short control descriptions. +- Drag-and-drop, double-click, or an explicit Add selected control button for inserting controls. - Drag and drop from the toolbox to Canvas/Grid/StackPanel/DockPanel/WrapPanel/UniformGrid layouts and supported empty single-child containers such as Border and GroupBox. - Direct mouse movement for controls whose parent is a `Canvas`. - Arrow-key Canvas movement (1 px, or 10 px with Shift) plus optional 10-pixel mouse snap-to-grid. - XAML document Outline, including selection/editing of the root Window. - Selection of controls on the preview surface. -- Undo/redo for designer-side XAML changes, while text editors keep their native text undo/redo. +- Undo/redo for designer-side changes restores XAML and generated PowerShell together, while text editors keep native text undo/redo. - Reflection-based property browser and property editing, including Canvas/Grid/DockPanel attached properties. - Reflection-based event browser. - Double-click an event to generate PowerShell event code. @@ -52,23 +53,28 @@ instead of depending on `Click="Button1_Click"` in XAML. - Automatic synchronization of named XAML controls into a generated control-reference region in the paired `.ps1` file. - Generated event handlers are placed in a dedicated region before `$Window.ShowDialog()`, so handlers are registered before the UI runs. - Existing user event code is not overwritten when control references are refreshed. -- Save As warns before replacing an already-existing paired `.ps1` file. +- Deleting controls archives their generated event handlers as comments instead of leaving broken references or silently discarding the code. +- Safe preview blocks risky loose-XAML constructs/custom CLR elements and prevents automatic external resource navigation. +- Save As warns before replacing an already-existing paired `.ps1` file; paired saves use temporary files/rollback and UTF-8 BOM for Windows PowerShell 5.1/Japanese compatibility. ### Start the designer -Windows PowerShell 5.1: +From a PowerShell prompt: -```powershell -powershell.exe -STA -ExecutionPolicy Bypass -File .\XamlDesigner\Start-XamlDesigner.ps1 -``` +~~~powershell +.\XamlDesigner\Start-XamlDesigner.ps1 +~~~ -PowerShell 7 on Windows: +If the current process is not STA, the launcher attempts to relaunch the same PowerShell executable in STA mode automatically. It does not bypass the machine's execution policy. -```powershell -pwsh.exe -STA -File .\XamlDesigner\Start-XamlDesigner.ps1 -``` +Explicit launch examples: + +~~~powershell +powershell.exe -NoProfile -STA -File .\XamlDesigner\Start-XamlDesigner.ps1 +pwsh.exe -NoProfile -STA -File .\XamlDesigner\Start-XamlDesigner.ps1 +~~~ -WPF requires Windows and an STA thread. +For a Japanese beginner walkthrough, see [XamlDesigner/GETTING_STARTED.ja.md](./XamlDesigner/GETTING_STARTED.ja.md). The requested 50 simulated persona reviews are recorded in [XamlDesigner/REVIEW_50_PERSONAS.md](./XamlDesigner/REVIEW_50_PERSONAS.md). ## Design direction diff --git a/Tests/Test-CodeGeneration.ps1 b/Tests/Test-CodeGeneration.ps1 index 84617c0..29a5929 100644 --- a/Tests/Test-CodeGeneration.ps1 +++ b/Tests/Test-CodeGeneration.ps1 @@ -20,6 +20,7 @@ $coreDirectory = Join-Path $designerDirectory 'Core' . (Join-Path $coreDirectory 'State.ps1') . (Join-Path $coreDirectory 'Xml.ps1') . (Join-Path $coreDirectory 'CodeBehind.ps1') +. (Join-Path $coreDirectory 'History.ps1') . (Join-Path $coreDirectory 'Events.ps1') $script:State.BaseDirectory = $designerDirectory @@ -40,7 +41,7 @@ if ($referenceLines -notcontains $expectedReference) { throw "Expected generated control reference was not found. Actual: $($referenceLines -join '; ')" } -$code = Get-Content -LiteralPath (Join-Path $designerDirectory 'Templates\BlankWindow.ps1') -Raw +$code = [System.IO.File]::ReadAllText((Join-Path $designerDirectory 'Templates\BlankWindow.ps1')) $script:State.Ui = @{ CodeEditor = [pscustomobject]@{ Text = $code } MainTabs = [pscustomobject]@{ SelectedIndex = 0 } diff --git a/Tests/Test-DesignerCore.ps1 b/Tests/Test-DesignerCore.ps1 new file mode 100644 index 0000000..79e8e0a --- /dev/null +++ b/Tests/Test-DesignerCore.ps1 @@ -0,0 +1,201 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($env:OS -ne 'Windows_NT') { + Write-Host 'Designer core behavior test skipped: WPF requires Windows.' + return +} + +Add-Type -AssemblyName WindowsBase +Add-Type -AssemblyName PresentationCore +Add-Type -AssemblyName PresentationFramework + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$designerDirectory = Join-Path $repositoryRoot 'XamlDesigner' +$coreDirectory = Join-Path $designerDirectory 'Core' + +foreach ($part in @( + 'State.ps1', + 'Xml.ps1', + 'CodeBehind.ps1', + 'Documents.ps1', + 'History.ps1', + 'ToolboxCatalog.ps1', + 'ToolboxEditing.ps1', + 'Events.ps1' +)) { + . (Join-Path $coreDirectory $part) +} + +$script:State.BaseDirectory = $designerDirectory +$script:State.ToolboxItems = @(Get-WpfControlCatalog) + +$safeDocument = New-XmlDocumentFromText -Text @' + + + +'@ +if (-not (Test-XamlPreviewSafety -Document $safeDocument)) { + throw 'Safe standard XAML was unexpectedly rejected.' +} + +$dangerousDocument = New-XmlDocumentFromText -Text @' + + + + + + +'@ +$blocked = $false +try { + [void](Test-XamlPreviewSafety -Document $dangerousDocument) +} +catch { + $blocked = $true +} +if (-not $blocked) { + throw 'ObjectDataProvider was not blocked by safe preview.' +} + +$customDocument = New-XmlDocumentFromText -Text @' + + + +'@ +$blocked = $false +try { + [void](Test-XamlPreviewSafety -Document $customDocument) +} +catch { + $blocked = $true +} +if (-not $blocked) { + throw 'Custom CLR element was not blocked by safe preview.' +} + +$script:State.XamlDocument = New-XmlDocumentFromText -Text @' + + + + + + + +