Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Documents/Changelog/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@

## 2026-11-xx - Build 2611 (V110 Nightly) - November 2026

* Implemented [#2381](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2381), logical `RightToLeftLayout` on `KryptonNavigator`
* Header ButtonSpecs (close, context, and workspace maximize) dock to the left when both `RightToLeft` and `RightToLeftLayout` are set. The navigator copies the layout flag from the host form, matching Ribbon and Workspace. Does not enable `WS_EX_LAYOUTRTL`.
* Implemented [#2383](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2383), RTL support for **all** `Krypton.Workspace` controls
* Horizontal sequences pack from the right when `RightToLeft` and `RightToLeftLayout` are both set (same two-flag contract as `KryptonForm` / `KryptonRibbon`). Vertical stacks stay top-to-bottom. Saved XML / `Children` order is unchanged. Set the flags on the host form; `KryptonWorkspace` syncs the layout flag automatically.
* Cell header buttons (maximize, context, close) dock to the left because `KryptonNavigator` now has the same logical `RightToLeftLayout` flag and form sync ([#2381](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2381)).
* Implemented [#3870](https://github.com/Krypton-Suite/Standard-Toolkit/issues/3870), Theme previews for custom themes
* `KryptonThemeListView` lists builtin and registered custom themes with preview images in Large Icon, Tile, Small Icon, and Details views. Stored `KryptonCustomPaletteBase.Thumbnail` (base64 PNG in `.kthemex`) is shown with the Stable Kr overlay; palettes without a thumbnail use the Kr tile. Builtin themes use a generated window mock-up (`KryptonThemePreview`). Palette Designer and Theme Browser Export write the mock-up into `Thumbnail` on save.
* Turn previews off with `ShowThemePreviews` (Kr tile for every row). Extra palettes follow `ShowExtraThemes` like `KryptonThemeListBox`.
Expand Down
92 changes: 92 additions & 0 deletions Scripts/UnitTests/Invoke-WorkspaceRtlScreenshot.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<#
.SYNOPSIS
Hosts WorkspaceRtlDemo and captures LTR / RTL PNGs for issue #2383.

.DESCRIPTION
Loads TestForm assemblies in-process (STA), shows WorkspaceRtlDemo, captures the
window in LTR then after enabling RightToLeft + RightToLeftLayout, and writes
Documents/PR/2383-workspace-rtl-ltr.png and 2383-workspace-rtl-rtl.png (or -OutputDir).

# UnitTest-CI: exclude

.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -STA -File .\Scripts\UnitTests\Invoke-WorkspaceRtlScreenshot.ps1
#>
[CmdletBinding()]
param(
[string]$Configuration = 'Debug',
[string]$TargetFramework = 'net472',
[string]$BinDir,
[string]$OutputDir
)

$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'UnitTestCommon.ps1')

$repoRoot = Get-UnitTestRepoRoot
$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration $Configuration -TargetFramework $TargetFramework -BinDir $BinDir
if (-not $OutputDir) {
$OutputDir = Join-Path $repoRoot 'Documents\PR'
}
if (-not (Test-Path -LiteralPath $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir | Out-Null
}

Register-UnitTestAssemblyResolver -BinDir $bin
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Navigator.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Workspace.dll'))
$asm = [System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'TestForm.exe'))

[System.Windows.Forms.Application]::EnableVisualStyles()
$formType = $asm.GetType('TestForm.WorkspaceRtlDemo')
if (-not $formType) {
throw 'Type TestForm.WorkspaceRtlDemo was not found in TestForm.exe.'
}

function Save-FormCapture {
param(
[System.Windows.Forms.Form]$Form,
[string]$Path
)

[System.Windows.Forms.Application]::DoEvents()
Start-Sleep -Milliseconds 400
[System.Windows.Forms.Application]::DoEvents()

$bounds = $Form.Bounds
$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$g.Dispose()
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
Write-Host "Wrote $Path"
}

$form = [System.Activator]::CreateInstance($formType)
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
$form.Location = New-Object System.Drawing.Point 80, 80
$form.TopMost = $true
$form.Show()
$form.Activate()
$form.BringToFront()
[System.Windows.Forms.Application]::DoEvents()
Start-Sleep -Milliseconds 700

Save-FormCapture -Form $form -Path (Join-Path $OutputDir '2383-workspace-rtl-ltr.png')

$chkField = $formType.GetField('chkRtl', [System.Reflection.BindingFlags]'Instance,NonPublic,Public')
if (-not $chkField) {
throw 'Field chkRtl was not found on WorkspaceRtlDemo.'
}
$chk = $chkField.GetValue($form)
$chk.Checked = $true
$form.PerformLayout()
$form.Refresh()
Save-FormCapture -Form $form -Path (Join-Path $OutputDir '2383-workspace-rtl-rtl.png')

$form.Close()
$form.Dispose()
2 changes: 2 additions & 0 deletions Scripts/UnitTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Default output folder: `Bin\Debug\net472`.
| `UnitTest-CustomPaletteBasePaletteMode.ps1` | #1870 `KryptonCustomPaletteBase.BasePaletteMode` inherits the builtin colour table; builtin `BasePalette` keeps catalog mode | `include` |
| `UnitTest-PaletteBinary.ps1` | #2117 custom palette `.kthemex` / `.ktheme` round-trip, `Convert`, `UpgradeXmlToKthemex` / `ConvertFile` (file and `KryptonCustomPaletteBase`), collections, `AddToCollection` / `RemoveFromCollection`, directory collections, and Utilities `FromDirectory` | `include` |
| `UnitTest-KryptonFormRtl.ps1` | #2103 `KryptonForm` RTL: `ScreenToWindow` stays physical; Close hit-tests on the right in LTR and the left with `RightToLeftLayout`; window region includes both physical left and right chrome | `include` |
| `UnitTest-WorkspaceRtlLayout.ps1` | #2383 `KryptonWorkspace` logical RTL: horizontal cells pack from the right when both flags are set; nested vertical order is unchanged; XML Children order is stable | `include` |
| `UnitTest-ContextMenuSubMenuImage.ps1` | #4252 Light Gray Office 2007/2010/Microsoft 365 `GetContextMenuSubMenuImage` returns an image; all catalog palettes must not throw | `include` |
| `UnitTest-TagInput.ps1` | `KryptonTagInputControl` API: defaults, add/remove, duplicates, max tags, events, category colours | `include` |
| `Start-AsyncFormsDemoHost.ps1` | Hosts `Feature4177AsyncFormsDemo` | n/a |
Expand All @@ -91,6 +92,7 @@ Default output folder: `Bin\Debug\net472`.
| `Invoke-TreeViewMultiSelectScreenshot.ps1` | Hosts `Bug4326TreeViewMultiSelectDemo` and writes `Documents/PR/4326-treeview-multiselect-false.png` | `exclude` |
| `Invoke-SchemeStripTextScreenshot.ps1` | Hosts `SchemeStripTextDemo` (#1100) and writes default/contrast PNGs under `Documents/PR/` | `exclude` |
| `Invoke-RibbonRtlScreenshot.ps1` | Hosts `RibbonRtlDemo` (#2382) and writes LTR/RTL PNGs under `Documents/PR/` | `exclude` |
| `Invoke-WorkspaceRtlScreenshot.ps1` | Hosts `WorkspaceRtlDemo` (#2383) and writes LTR/RTL PNGs under `Documents/PR/` | `exclude` |
| `Invoke-RibbonCaptionPaletteScreenshot.ps1` | Hosts `Bug4061RibbonCaptionIconThemeDemo` (#3859 / #4061) and writes Office 2007 / Microsoft 365 PNGs under `Documents/PR/` | `exclude` |
| `Invoke-PaletteBinaryScreenshot.ps1` | Hosts `PaletteBinaryDemo` (#2117) and writes `Documents/PR/2117-bulk-xml-upgrade-demo.png` | `exclude` |
| `Invoke-PaletteCollectionEditorScreenshot.ps1` | Hosts `KryptonPaletteCollectionEditor` (#2117) and writes `Documents/PR/2117-pack-editor-demo.png` | `exclude` |
Expand Down
209 changes: 209 additions & 0 deletions Scripts/UnitTests/UnitTest-WorkspaceRtlLayout.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<#
.SYNOPSIS
Asserts #2383 KryptonWorkspace logical RTL packing (horizontal reverse, vertical unchanged).

.DESCRIPTION
Builds a KryptonWorkspace with a horizontal root (A | vertical[Top,Bottom] | C),
then checks LTR left-to-right packing, RTL packing from the right with Top still
above Bottom, and XML save/load keeping Children order.

.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -STA -File .\Scripts\UnitTests\UnitTest-WorkspaceRtlLayout.ps1
#>
# UnitTest-CI: include
[CmdletBinding()]
param(
[string]$Configuration = 'Debug',
[string]$TargetFramework = 'net472',
[string]$BinDir
)

$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'UnitTestCommon.ps1')

$repoRoot = Get-UnitTestRepoRoot
$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration $Configuration -TargetFramework $TargetFramework -BinDir $BinDir
Register-UnitTestAssemblyResolver -BinDir $bin

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms

[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Interop.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Navigator.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Workspace.dll'))

$failed = New-Object System.Collections.Generic.List[string]

function Assert-True {
param([bool]$Condition, [string]$Message)
if (-not $Condition) {
$failed.Add($Message)
Write-Host "FAIL: $Message" -ForegroundColor Red
}
else {
Write-Host "PASS: $Message" -ForegroundColor Green
}
}

function New-RtlTestCell {
param([string]$UniqueName, [string]$Title)
$cell = New-Object Krypton.Workspace.KryptonWorkspaceCell
$cell.UniqueName = $UniqueName
$cell.StarSize = '50*,50*'
$cell.Button.CloseButtonDisplay = [Krypton.Navigator.ButtonDisplay]::ShowEnabled
$page = New-Object Krypton.Navigator.KryptonPage
$page.Text = $Title
$page.UniqueName = $UniqueName + 'Page'
[void]$cell.Pages.Add($page)
return $cell
}

function Get-RootChildNames {
param($Workspace)
$names = New-Object System.Collections.Generic.List[string]
foreach ($child in $Workspace.Root.Children) {
if ($child -is [Krypton.Workspace.KryptonWorkspaceCell]) {
$names.Add($child.UniqueName)
}
elseif ($child -is [Krypton.Workspace.KryptonWorkspaceSequence]) {
$nested = New-Object System.Collections.Generic.List[string]
foreach ($inner in $child.Children) {
if ($inner -is [Krypton.Workspace.KryptonWorkspaceCell]) {
$nested.Add($inner.UniqueName)
}
}
$names.Add(('[' + ($nested -join ',') + ']'))
}
}
return ($names -join ',')
}

function Get-NavButtonRect {
param($Navigator, $Spec)
$flags = [System.Reflection.BindingFlags]'Instance,NonPublic'
$vbProp = [Krypton.Navigator.KryptonNavigator].GetProperty('ViewBuilder', $flags)
$vb = $vbProp.GetValue($Navigator)
if (-not $vb) {
return [System.Drawing.Rectangle]::Empty
}

$type = $vb.GetType()
$mgr = $null
while ($type) {
$field = $type.GetField('_buttonManager', $flags)
if ($field) {
$mgr = $field.GetValue($vb)
break
}
$type = $type.BaseType
}

if (-not $mgr) {
return [System.Drawing.Rectangle]::Empty
}

return $mgr.GetButtonRectangle($Spec)
}

$form = New-Object Krypton.Toolkit.KryptonForm
$ws = $null
try {
$form.Text = 'Workspace RTL unit test'
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
$form.Location = New-Object System.Drawing.Point 40, 40
$form.Size = New-Object System.Drawing.Size 900, 520
$form.RightToLeft = [System.Windows.Forms.RightToLeft]::No
$form.RightToLeftLayout = $false

$ws = New-Object Krypton.Workspace.KryptonWorkspace
$ws.Dock = [System.Windows.Forms.DockStyle]::Fill
[void]$form.Controls.Add($ws)

$cellA = New-RtlTestCell -UniqueName 'RtlCellA' -Title 'A'
$cellTop = New-RtlTestCell -UniqueName 'RtlCellTop' -Title 'Top'
$cellBottom = New-RtlTestCell -UniqueName 'RtlCellBottom' -Title 'Bottom'
$cellC = New-RtlTestCell -UniqueName 'RtlCellC' -Title 'C'

$stacked = New-Object Krypton.Workspace.KryptonWorkspaceSequence ([System.Windows.Forms.Orientation]::Vertical)
[void]$stacked.Children.Add($cellTop)
[void]$stacked.Children.Add($cellBottom)

$ws.Root.Orientation = [System.Windows.Forms.Orientation]::Horizontal
[void]$ws.Root.Children.Add($cellA)
[void]$ws.Root.Children.Add($stacked)
[void]$ws.Root.Children.Add($cellC)

$form.Show()
$form.Activate()
[System.Windows.Forms.Application]::DoEvents()
$ws.PerformLayout()
[System.Windows.Forms.Application]::DoEvents()

$ltrOrder = Get-RootChildNames -Workspace $ws
Assert-True ($ltrOrder -eq 'RtlCellA,[RtlCellTop,RtlCellBottom],RtlCellC') "LTR children order is A, [Top,Bottom], C (actual='$ltrOrder')"
Assert-True ($cellA.Left -lt $cellC.Left) "LTR A is left of C (A.Left=$($cellA.Left) C.Left=$($cellC.Left))"
Assert-True ($cellTop.Top -lt $cellBottom.Top) "LTR Top is above Bottom (Top.Y=$($cellTop.Top) Bottom.Y=$($cellBottom.Top))"
Assert-True (-not $ws.RightToLeftLayout) 'Workspace RightToLeftLayout is false in LTR'
Assert-True (-not $cellA.RightToLeftLayout) 'Cell RightToLeftLayout is false in LTR'

$ltrClose = Get-NavButtonRect -Navigator $cellA -Spec $cellA.Button.CloseButton
Assert-True ($ltrClose.Width -gt 0) "LTR close button has a layout rect (actual=$ltrClose)"
Assert-True (($ltrClose.X + [int]($ltrClose.Width / 2)) -gt ([int]($cellA.Width / 2))) "LTR close button is on the right of the cell (close=$ltrClose cellWidth=$($cellA.Width))"

$saved = $ws.SaveLayoutToArray()
Assert-True ($saved.Length -gt 0) 'SaveLayoutToArray returns bytes'

$form.RightToLeft = [System.Windows.Forms.RightToLeft]::Yes
$form.RightToLeftLayout = $true
[System.Windows.Forms.Application]::DoEvents()
$ws.PerformLayout()
[System.Windows.Forms.Application]::DoEvents()

Assert-True $ws.RightToLeftLayout 'Workspace RightToLeftLayout syncs from the form'
Assert-True $cellA.RightToLeftLayout 'Cell RightToLeftLayout syncs from the form'
Assert-True ($ws.RightToLeft -eq [System.Windows.Forms.RightToLeft]::Yes) 'Workspace RightToLeft inherits Yes'
Assert-True ($cellA.Left -gt $cellC.Left) "RTL A is right of C (A.Left=$($cellA.Left) C.Left=$($cellC.Left))"
Assert-True ($cellTop.Top -lt $cellBottom.Top) "RTL Top stays above Bottom (Top.Y=$($cellTop.Top) Bottom.Y=$($cellBottom.Top))"

$cellA.PerformLayout()
[System.Windows.Forms.Application]::DoEvents()
$rtlClose = Get-NavButtonRect -Navigator $cellA -Spec $cellA.Button.CloseButton
Assert-True ($rtlClose.Width -gt 0) "RTL close button has a layout rect (actual=$rtlClose)"
Assert-True (($rtlClose.X + [int]($rtlClose.Width / 2)) -lt ([int]($cellA.Width / 2))) "RTL close button is on the left of the cell (close=$rtlClose cellWidth=$($cellA.Width))"

$rtlOrder = Get-RootChildNames -Workspace $ws
Assert-True ($rtlOrder -eq 'RtlCellA,[RtlCellTop,RtlCellBottom],RtlCellC') "RTL children order is unchanged (actual='$rtlOrder')"

$form.RightToLeft = [System.Windows.Forms.RightToLeft]::Yes
$form.RightToLeftLayout = $false
[System.Windows.Forms.Application]::DoEvents()
$ws.PerformLayout()
[System.Windows.Forms.Application]::DoEvents()
Assert-True ($cellA.Left -lt $cellC.Left) "RightToLeft alone does not reverse columns (A.Left=$($cellA.Left) C.Left=$($cellC.Left))"

$form.RightToLeft = [System.Windows.Forms.RightToLeft]::Yes
$form.RightToLeftLayout = $true
[System.Windows.Forms.Application]::DoEvents()
$ws.LoadLayoutFromArray($saved)
$ws.PerformLayout()
[System.Windows.Forms.Application]::DoEvents()
$loadedOrder = Get-RootChildNames -Workspace $ws
Assert-True ($loadedOrder -eq 'RtlCellA,[RtlCellTop,RtlCellBottom],RtlCellC') "LoadLayoutFromArray keeps Children order (actual='$loadedOrder')"
}
finally {
if ($form) {
$form.Close()
$form.Dispose()
}
}

if ($failed.Count -gt 0) {
Write-Host ""
Write-Host "$($failed.Count) assertion(s) failed." -ForegroundColor Red
exit 1
}

Write-Host ''
Write-Host 'All Workspace RTL layout assertions passed.' -ForegroundColor Green
exit 0
Loading
Loading