Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/Wpf.Ui.FontMapper/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ await httpClient.GetFromJsonAsync<IEnumerable<GitTag>>(
)
?.Last()
?.Ref.Replace("refs/tags/", string.Empty)
.Trim() ?? throw new Exception("Unable to parse the version string");
.Trim()
?? throw new Exception("Unable to parse the version string");
}

string FormatIconName(string rawIconName)
Expand Down
4 changes: 2 additions & 2 deletions src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@
</ui:NavigationView.ContentOverlay>
</ui:NavigationView>

<ContentPresenter x:Name="RootContentDialog" Grid.Row="0" />

<ui:TitleBar
x:Name="TitleBar"
Title="{Binding ViewModel.ApplicationTitle}"
Expand All @@ -98,5 +96,7 @@
<ContextMenu DataContext="{Binding DataContext, Source={x:Reference NavigationView}}" ItemsSource="{Binding ViewModel.TrayMenuItems, Mode=OneWay}" />
</tray:NotifyIcon.Menu>
</tray:NotifyIcon>

<ui:ContentDialogHost x:Name="RootContentDialog" Grid.Row="0" />
</Grid>
</ui:FluentWindow>
8 changes: 5 additions & 3 deletions src/Wpf.Ui/Appearance/SystemThemeWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@ public static void UnWatch(Window? window)
/// </summary>
private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == (int)PInvoke.WM_DWMCOLORIZATIONCOLORCHANGED ||
msg == (int)PInvoke.WM_THEMECHANGED ||
msg == (int)PInvoke.WM_SYSCOLORCHANGE)
if (
msg == (int)PInvoke.WM_DWMCOLORIZATIONCOLORCHANGED
|| msg == (int)PInvoke.WM_THEMECHANGED
|| msg == (int)PInvoke.WM_SYSCOLORCHANGE
)
{
UpdateObservedWindow(hWnd);
}
Expand Down
206 changes: 206 additions & 0 deletions src/Wpf.Ui/AutomationPeers/ContentDialogAutomationPeer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.

using System.Windows.Automation;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Threading;
using Wpf.Ui.Controls;

namespace Wpf.Ui.AutomationPeers;

/// <summary>
/// Automation peer that exposes a <see cref="ContentDialog"/> as a standard modal window
/// for UI Automation clients.
/// </summary>
/// <remarks>
/// This peer maps dialog-specific behavior to the <see cref="IWindowProvider"/> pattern so
/// assistive technologies (screen readers, automation tools) perceive the <see cref="ContentDialog"/>
/// as a modal, non-resizable dialog window.
/// </remarks>
internal sealed class ContentDialogAutomationPeer : UIElementAutomationPeer, IWindowProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentDialogAutomationPeer"/> class.
/// </summary>
/// <param name="owner">The associated <see cref="ContentDialog"/>.</param>
public ContentDialogAutomationPeer(ContentDialog owner)
: base(owner) { }

/// <summary>
/// Gets a value indicating whether the window is modal.
/// Always <see langword="true"/> for <see cref="ContentDialog"/>.
/// </summary>
bool IWindowProvider.IsModal => true;

/// <summary>
/// Gets a value indicating whether the window is topmost.
/// <see cref="ContentDialog"/> are treated as topmost for automation.
/// </summary>
bool IWindowProvider.IsTopmost => true;

/// <summary>
/// Gets the current interaction state of the dialog window for UI Automation.
/// </summary>
public WindowInteractionState InteractionState
{
get
{
if (Owner is ContentDialog dialog)
{
if (
!dialog.IsLoaded
|| dialog.Dispatcher is { HasShutdownFinished: true } or { HasShutdownStarted: true }
)
{
return WindowInteractionState.Closing;
}
}

return WindowInteractionState.Running;
}
}

/// <summary>
/// Gets a value indicating whether the window can be maximized.
/// Always <see langword="false"/> for <see cref="ContentDialog"/>.
/// </summary>
public bool Maximizable => false;

/// <summary>
/// Gets a value indicating whether the window can be minimized.
/// Always <see langword="false"/> for <see cref="ContentDialog"/>.
/// </summary>
public bool Minimizable => false;

/// <summary>
/// Gets the visual state of the window.
/// <see cref="ContentDialog"/> report <see cref="WindowVisualState.Normal"/>.
/// </summary>
public WindowVisualState VisualState => WindowVisualState.Normal;

/// <inheritdoc/>
protected override string GetClassNameCore()
{
// "Emulating WinUI3's ContentDialog ClassName"
return "Popup";
}

/// <inheritdoc/>
protected override string? GetNameCore()
{
if (Owner is ContentDialog dialog)
{
return dialog.Title as string ?? dialog.Title?.ToString();
}

return base.GetNameCore();
}

/// <inheritdoc/>
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Window;
}

#if NET48_OR_GREATER || NET5_0_OR_GREATER
/// <inheritdoc/>
protected override bool IsDialogCore()
{
return true;
}
#endif

/// <inheritdoc/>
protected override bool IsControlElementCore()
{
return true;
}

/// <inheritdoc/>
protected override bool IsContentElementCore()
{
return true;
}

/// <inheritdoc/>
protected override bool IsKeyboardFocusableCore()
{
return false;
}

/// <summary>
/// Returns whether the dialog is currently offscreen. A dialog is considered offscreen when not loaded or not visible.
/// </summary>
protected override bool IsOffscreenCore()
{
return Owner is ContentDialog { IsLoaded: false } or { IsVisible: false };
}

/// <summary>
/// Returns automation pattern implementations supported by this peer. Provides <see cref="IWindowProvider"/>.
/// </summary>
/// <param name="pattern">The requested automation pattern.</param>
/// <returns>An object implementing the requested pattern or <see langword="null"/> when not supported.</returns>
public override object? GetPattern(PatternInterface pattern)
{
// Include PatternInterface.ScrollItem to align with WinUI3 behavior: WinUI3 exposes this pattern
// for dialog-like popups, and exposing it here helps automation clients that rely on that behavior.
if (pattern is PatternInterface.Window or PatternInterface.ScrollItem)
{
return this;
}

return null;
}

/// <summary>
/// Closes the associated <see cref="ContentDialog"/>.
/// This is invoked by UI Automation clients through the <see cref="IWindowProvider"/> pattern.
/// </summary>
void IWindowProvider.Close()
{
if (Owner is ContentDialog dialog)
{
Dispatcher? dispatcher = dialog.Dispatcher;
if (dispatcher is { HasShutdownStarted: false, HasShutdownFinished: false })
{
dispatcher.BeginInvoke(
() =>
{
dialog.Hide();
},
DispatcherPriority.Normal
);
}
else
{
dialog.Hide();
}
}
}

/// <summary>
/// Sets the visual state of the window. Not supported for <see cref="ContentDialog"/>.
/// </summary>
void IWindowProvider.SetVisualState(WindowVisualState state)
{
// Not supported for this.
}

/// <summary>
/// Waits for the dialog to become idle.
/// Always returns <see langword="true"/> for <see cref="ContentDialog"/>.
/// </summary>
/// <param name="milliseconds">Maximum time to wait in milliseconds (ignored).</param>
/// <returns>
/// <see langword="true"/> if the dialog is idle or the operation completed;
/// otherwise <see langword="false"/>.
/// </returns>
public bool WaitForInputIdle(int milliseconds)
{
return true;
}
}
85 changes: 81 additions & 4 deletions src/Wpf.Ui/ContentDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Wpf.Ui;
/// </summary>
/// <example>
/// <code lang="xml">
/// &lt;ContentPresenter x:Name="RootContentDialogPresenter" Grid.Row="0" /&gt;
/// &lt;ContentDialogHost x:Name="RootContentDialogPresenter" Grid.Row="0" /&gt;
/// </code>
/// <code lang="csharp">
/// IContentDialogService contentDialogService = new ContentDialogService();
Expand All @@ -33,6 +33,7 @@ namespace Wpf.Ui;
public class ContentDialogService : IContentDialogService
{
private ContentPresenter? _dialogHost;
private ContentDialogHost? _dialogHostEx;

[Obsolete("Use SetDialogHost instead.")]
public void SetContentPresenter(ContentPresenter contentPresenter)
Expand All @@ -47,34 +48,110 @@ public void SetContentPresenter(ContentPresenter contentPresenter)
}

/// <inheritdoc/>
[Obsolete("Use SetDialogHost(ContentDialogHost) instead.")]
public void SetDialogHost(ContentPresenter contentPresenter)
{
if (contentPresenter == null)
{
throw new ArgumentNullException(nameof(contentPresenter));
}

if (_dialogHostEx != null)
{
throw new InvalidOperationException(
"Cannot set ContentPresenter: a ContentDialogHost host has already been set. "
+ "Only one host type is allowed per instance for compatibility."
);
}

_dialogHost = contentPresenter;
}

/// <inheritdoc/>
[Obsolete("Use GetDialogHostEx() instead.")]
public ContentPresenter? GetDialogHost()
{
return _dialogHost;
}

/// <inheritdoc/>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="dialogHost"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when a legacy dialog host (ContentPresenter) has already been set via
/// <see cref="SetDialogHost(ContentPresenter)"/>. Only one host type can be set per instance.
/// </exception>
/// <remarks>
/// <para>
/// This method sets the enhanced <see cref="ContentDialogHost"/> to contain and manage dialogs.
/// For compatibility reasons, an instance can have either a legacy host (set via
/// <see cref="SetDialogHost(ContentPresenter)"/>) or an enhanced host (set via this method),
/// but not both.
/// </para>
/// </remarks>
public void SetDialogHost(ContentDialogHost dialogHost)
{
if (dialogHost == null)
{
throw new ArgumentNullException(nameof(dialogHost));
}

// Defense mechanism: prevent mixed host types for compatibility
if (_dialogHost != null)
{
throw new InvalidOperationException(
"Cannot set ContentDialogHost: a legacy ContentPresenter host has already been set. "
+ "Only one host type is allowed per instance for compatibility."
);
}

_dialogHostEx = dialogHost;
}

/// <inheritdoc/>
public ContentDialogHost? GetDialogHostEx()
{
return _dialogHostEx;
}

/// <inheritdoc/>
public Task<ContentDialogResult> ShowAsync(ContentDialog dialog, CancellationToken cancellationToken)
{
if (_dialogHost == null)
#pragma warning disable CS0618 // (Warning: Obsolete) To maintain compatibility

if (dialog == null)
{
throw new ArgumentNullException(nameof(dialog));
}

if (_dialogHostEx == null && _dialogHost == null)
{
throw new InvalidOperationException("The DialogHost was never set.");
}

if (dialog.DialogHost != null && _dialogHost != dialog.DialogHost)
object? svcHost = _dialogHostEx is not null ? _dialogHostEx : _dialogHost;

object? dlgHost = dialog.DialogHostEx is not null ? dialog.DialogHostEx : dialog.DialogHost;

if (dlgHost != null && !ReferenceEquals(dlgHost, svcHost))
{
throw new InvalidOperationException(
"The DialogHost is not the same as the one that was previously set."
);
}

dialog.DialogHost = _dialogHost;
if (_dialogHostEx != null)
{
dialog.DialogHostEx = _dialogHostEx;
}
else
{
dialog.DialogHost = _dialogHost;
}

return dialog.ShowAsync(cancellationToken);

#pragma warning restore CS0618 // (Warning: Obsolete)
}
}
Loading
Loading