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
6 changes: 6 additions & 0 deletions Documents/Changelog/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@

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

* Implemented [#2379](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2379), RTL support for **all** `Krypton.Toolkit` controls
* Set `RightToLeft` and `RightToLeftLayout` on the host `KryptonForm` (or on the control). Both flags together mirror docks, splitters, group/header captions, spin buttons, and list reading order; `RightToLeft` alone still controls text Near/Far.
* `KryptonPropertyGrid` forwards `RightToLeft` into the inner grid (help pane, toolbar, reading order). Label/value columns stay native LTR: WinForms `PropertyGrid` paints left-aligned cells and does not support a mirrored list.
* Month calendar day-name and date columns pack from the start edge when both flags are set (first weekday on the right).
* TestForm **Toolkit RTL Gallery** hosts every visual Toolkit control (plus CommandLink) under Dual RTL.
* Implemented [#4372](https://github.com/Krypton-Suite/Standard-Toolkit/issues/4372), Display the commit ID in Discord notifications
* 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
105 changes: 105 additions & 0 deletions Scripts/UnitTests/Invoke-PropertyGridRtlScreenshot.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<#
.SYNOPSIS
Hosts ToolkitRtlGalleryDemo on the PropertyGrid tab and captures Dual RTL.

.DESCRIPTION
Loads Debug TestForm in-process (STA), shows ToolkitRtlGalleryDemo with Dual RTL,
selects the PropertyGrid page, and writes Documents/PR/2379-toolkit-rtl-propertygrid.png.

# UnitTest-CI: exclude

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

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

$repoRoot = Get-UnitTestRepoRoot
$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration $Configuration -TargetFramework $TargetFramework -BinDir $BinDir
if (-not $OutputPath) {
$OutputPath = Join-Path $repoRoot 'Documents\PR\2379-toolkit-rtl-propertygrid.png'
}

$outDir = Split-Path -Parent $OutputPath
if (-not (Test-Path -LiteralPath $outDir)) {
New-Item -ItemType Directory -Path $outDir | Out-Null
}

Register-UnitTestAssemblyResolver -BinDir $bin
Initialize-UnitTestNativeInput
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms

Get-ChildItem -LiteralPath $bin -Filter '*.dll' | ForEach-Object {
try {
[void][System.Reflection.Assembly]::LoadFrom($_.FullName)
}
catch {
# Some satellite / native binaries are not managed assemblies.
}
}

$testFormPath = Join-Path $bin 'TestForm.dll'
if (-not (Test-Path -LiteralPath $testFormPath)) {
$testFormPath = Join-Path $bin 'TestForm.exe'
}
if (-not (Test-Path -LiteralPath $testFormPath)) {
throw "TestForm was not found in $bin."
}

$asm = [System.Reflection.Assembly]::LoadFrom($testFormPath)
[System.Windows.Forms.Application]::EnableVisualStyles()
if ([System.Windows.Forms.Application]::RenderWithVisualStyles) { }

$formType = $asm.GetType('TestForm.ToolkitRtlGalleryDemo')
if (-not $formType) {
throw 'Type TestForm.ToolkitRtlGalleryDemo was not found.'
}

$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()
[void][UnitTestNative]::SetForegroundWindow($form.Handle)
[System.Windows.Forms.Application]::DoEvents()

$navField = $formType.GetField('kryptonNavigator', [System.Reflection.BindingFlags]'Instance,NonPublic')
$nav = $navField.GetValue($form)
foreach ($page in $nav.Pages) {
if ($page.Text -eq 'PropertyGrid') {
$nav.SelectedPage = $page
break
}
}

Write-Host "Hosted $($form.Text); selected $($nav.SelectedPage.Text)"
[System.Windows.Forms.Application]::DoEvents()
Start-Sleep -Milliseconds 1200
[System.Windows.Forms.Application]::DoEvents()

$bounds = $form.Bounds
$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$g = [System.Drawing.Graphics]::FromImage($bmp)
try {
$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$bmp.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$g.Dispose()
$bmp.Dispose()
$form.Close()
$form.Dispose()
}

Write-Host "Wrote $OutputPath"
184 changes: 184 additions & 0 deletions Scripts/UnitTests/Invoke-ToolkitRtlScreenshot.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<#
.SYNOPSIS
Hosts RTLControlsTest and captures LTR / RTL PNGs for issue #2379.

.DESCRIPTION
Launches Debug TestForm with --demo RTLControlsTest (process host, not LoadFrom).
Prefers TestForm.exe; falls back to `dotnet TestForm.dll` when Application Control
blocks the exe. Captures LTR, then relaunches with --rtl (both flags) and captures RTL.
Writes Documents/PR/2379-toolkit-rtl-ltr.png and 2379-toolkit-rtl-rtl.png (or -OutputDir).

# UnitTest-CI: exclude

.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -STA -File .\Scripts\UnitTests\Invoke-ToolkitRtlScreenshot.ps1
#>
[CmdletBinding()]
param(
[string]$Configuration = 'Debug',
[string]$TargetFramework = 'net472',
[string]$BinDir,
[string]$OutputDir,
[string]$Demo = 'RTLControlsTest',
[string]$WindowTitle = 'Toolkit RTL Test (#2379)',
[string]$OutputStem = '2379-toolkit-rtl',
[switch]$SingleCapture
)

$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
}

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Initialize-UnitTestNativeInput

if (-not ('ToolkitRtlCaptureNative' -as [type])) {
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class ToolkitRtlCaptureNative
{
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr h, int X, int Y, int cx, int cy, uint flags);
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOZORDER = 0x0004;
}
"@
}

function Get-ToolkitRtlHost {
param([string]$BinDir)

$exe = Join-Path $BinDir 'TestForm.exe'
$dll = Join-Path $BinDir 'TestForm.dll'
if (Test-Path -LiteralPath $exe) {
try {
$probe = Start-Process -FilePath $exe -ArgumentList @('--help') -WorkingDirectory $BinDir -PassThru -WindowStyle Hidden -ErrorAction Stop
Start-Sleep -Milliseconds 200
if ($probe -and -not $probe.HasExited) {
$probe.Kill()
}
return @{ FilePath = $exe; WorkingDirectory = $BinDir }
}
catch {
# WDAC / Application Control often blocks the net472 exe; dotnet is trusted.
}
}

$dotnetBins = @(
$BinDir,
(Join-Path (Split-Path -Parent $BinDir) 'net8.0-windows')
)
foreach ($candidate in $dotnetBins) {
$candidateDll = Join-Path $candidate 'TestForm.dll'
if (Test-Path -LiteralPath $candidateDll) {
return @{ FilePath = 'dotnet'; ArgumentsPrefix = @($candidateDll); WorkingDirectory = $candidate }
}
}

throw "TestForm.exe/dll not found or blocked in $BinDir. Build TestForm first."
}

function Wait-AutomationWindow {
param(
[string]$Name,
[int]$TimeoutMs = 25000
)

$root = [System.Windows.Automation.AutomationElement]::RootElement
$cond = New-Object System.Windows.Automation.PropertyCondition (
[System.Windows.Automation.AutomationElement]::NameProperty, $Name)
$sw = [System.Diagnostics.Stopwatch]::StartNew()
while ($sw.ElapsedMilliseconds -lt $TimeoutMs) {
$el = $root.FindFirst([System.Windows.Automation.TreeScope]::Children, $cond)
if ($el) {
$rect = $el.Current.BoundingRectangle
if ($rect.Width -ge 400 -and $rect.Height -ge 300) {
return $el
}
}
Start-Sleep -Milliseconds 200
}

throw "Window '$Name' was not found within ${TimeoutMs}ms."
}

function Save-ElementShot {
param(
[System.Windows.Automation.AutomationElement]$Window,
[string]$Path
)

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

$rect = $Window.Current.BoundingRectangle
$bounds = [System.Drawing.Rectangle]::new([int]$rect.X, [int]$rect.Y, [int]$rect.Width, [int]$rect.Height)
$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$g = [System.Drawing.Graphics]::FromImage($bmp)
try {
$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$g.Dispose()
$bmp.Dispose()
}

Write-Host "Wrote $Path"
}

function Invoke-ToolkitRtlCapture {
param(
[hashtable]$HostInfo,
[string[]]$ExtraArgs,
[string]$OutputPath
)

$argList = @()
if ($HostInfo.ArgumentsPrefix) {
$argList += $HostInfo.ArgumentsPrefix
}
$argList += @('--demo', $Demo)
if ($ExtraArgs) {
$argList += $ExtraArgs
}

$proc = Start-Process -FilePath $HostInfo.FilePath -ArgumentList $argList -WorkingDirectory $HostInfo.WorkingDirectory -PassThru
try {
$window = Wait-AutomationWindow -Name $WindowTitle
$hwnd = [IntPtr]$window.Current.NativeWindowHandle
[void][ToolkitRtlCaptureNative]::SetWindowPos($hwnd, [IntPtr]::Zero, 40, 40, 0, 0, 0x0005)
[void][UnitTestNative]::SetForegroundWindow($hwnd)
Start-Sleep -Milliseconds 700
$window = Wait-AutomationWindow -Name $WindowTitle
Save-ElementShot -Window $window -Path $OutputPath
}
finally {
if ($proc -and -not $proc.HasExited) {
$proc.Kill()
$proc.WaitForExit(5000) | Out-Null
}
}
}

$hostInfo = Get-ToolkitRtlHost -BinDir $bin
if ($SingleCapture) {
Invoke-ToolkitRtlCapture -HostInfo $hostInfo -OutputPath (Join-Path $OutputDir "$OutputStem.png")
}
else {
Invoke-ToolkitRtlCapture -HostInfo $hostInfo -OutputPath (Join-Path $OutputDir "$OutputStem-ltr.png")
Invoke-ToolkitRtlCapture -HostInfo $hostInfo -ExtraArgs @('--rtl') -OutputPath (Join-Path $OutputDir "$OutputStem-rtl.png")
}
Write-Host "Captured #2379 screenshots under $OutputDir"
2 changes: 2 additions & 0 deletions Scripts/UnitTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ 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-ToolkitRtlScreenshot.ps1` | Hosts `RTLControlsTest` (#2379) and writes LTR/RTL PNGs under `Documents/PR/`. Also: `-Demo ToolkitRtlGalleryDemo -WindowTitle 'Toolkit RTL Gallery (#2379)' -OutputStem 2379-toolkit-rtl-gallery -SingleCapture` | `exclude` |
| `Invoke-PropertyGridRtlScreenshot.ps1` | Hosts `ToolkitRtlGalleryDemo` on the PropertyGrid tab (#2379) and writes `Documents/PR/2379-toolkit-rtl-propertygrid.png` | `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
Original file line number Diff line number Diff line change
Expand Up @@ -1014,15 +1014,15 @@ public Size HideRibbonSize
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public bool RightToLeftLayout
public override bool RightToLeftLayout
{
get => _isRightToLeftLayout;
set
{
if (_isRightToLeftLayout != value)
{
_isRightToLeftLayout = value;
PerformNeedPaint(true);
OnRightToLeftLayoutChanged(EventArgs.Empty);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,20 @@ protected override void ContextMenuClosed()
_buttonController.RemoveFixed();
}

/// <inheritdoc />
protected override void OnRightToLeftChanged(EventArgs e)
{
CommandLinkTextValues.SyncDefaultArrow(CommonHelper.IsRightToLeftLayout(this));
base.OnRightToLeftChanged(e);
}

/// <inheritdoc />
protected override void OnRightToLeftLayoutChanged(EventArgs e)
{
CommandLinkTextValues.SyncDefaultArrow(CommonHelper.IsRightToLeftLayout(this));
base.OnRightToLeftLayoutChanged(e);
}

/// <inheritdoc />
protected override void OnPaint(PaintEventArgs? e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ public bool UseDefaultImage
}
public bool ShouldSerializeUseDefaultImage() => !UseDefaultImage;
public void ResetUseDefaultImage() => UseDefaultImage = true;

/// <summary>
/// Applies the default command-link arrow, mirrored when <paramref name="rightToLeft"/> is true.
/// </summary>
/// <param name="rightToLeft">True when both RTL layout flags are set on the owning button.</param>
public void SyncDefaultArrow(bool rightToLeft)
{
if (UseDefaultImage)
{
Image = CommandLinkArrowHelper.GetDefaultArrowImage(rightToLeft: rightToLeft);
}
}
#endregion

#region Protected
Expand Down
Loading
Loading