1+ // This Source Code Form is subject to the terms of the MIT License.
2+ // If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
3+ // Copyright (C) Leszek Pomianowski and WPF UI Contributors.
4+ // All Rights Reserved.
5+
6+ using System . Windows . Automation ;
7+ using System . Windows . Automation . Peers ;
8+ using System . Windows . Automation . Provider ;
9+ using System . Windows . Threading ;
10+ using Wpf . Ui . Controls ;
11+
12+ namespace Wpf . Ui . AutomationPeers ;
13+
14+ /// <summary>
15+ /// Automation peer that exposes a <see cref="ContentDialog"/> as a standard modal window
16+ /// for UI Automation clients.
17+ /// </summary>
18+ /// <remarks>
19+ /// This peer maps dialog-specific behavior to the <see cref="IWindowProvider"/> pattern so
20+ /// assistive technologies (screen readers, automation tools) perceive the <see cref="ContentDialog"/>
21+ /// as a modal, non-resizable dialog window.
22+ /// </remarks>
23+ internal sealed class ContentDialogAutomationPeer : UIElementAutomationPeer , IWindowProvider
24+ {
25+ /// <summary>
26+ /// Initializes a new instance of the <see cref="ContentDialogAutomationPeer"/> class.
27+ /// </summary>
28+ /// <param name="owner">The associated <see cref="ContentDialog"/>.</param>
29+ public ContentDialogAutomationPeer ( ContentDialog owner )
30+ : base ( owner )
31+ {
32+ }
33+
34+ /// <summary>
35+ /// Gets a value indicating whether the window is modal.
36+ /// Always <see langword="true"/> for <see cref="ContentDialog"/>.
37+ /// </summary>
38+ bool IWindowProvider . IsModal => true ;
39+
40+ /// <summary>
41+ /// Gets a value indicating whether the window is topmost.
42+ /// <see cref="ContentDialog"/> are treated as topmost for automation.
43+ /// </summary>
44+ bool IWindowProvider . IsTopmost => true ;
45+
46+ /// <summary>
47+ /// Gets the current interaction state of the dialog window for UI Automation.
48+ /// </summary>
49+ public WindowInteractionState InteractionState
50+ {
51+ get
52+ {
53+ if ( Owner is ContentDialog dialog )
54+ {
55+ if ( ! dialog . IsLoaded || dialog . Dispatcher is { HasShutdownFinished : true } or { HasShutdownStarted : true } )
56+ {
57+ return WindowInteractionState . Closing ;
58+ }
59+ }
60+
61+ return WindowInteractionState . Running ;
62+ }
63+ }
64+
65+ /// <summary>
66+ /// Gets a value indicating whether the window can be maximized.
67+ /// Always <see langword="false"/> for <see cref="ContentDialog"/>.
68+ /// </summary>
69+ public bool Maximizable => false ;
70+
71+ /// <summary>
72+ /// Gets a value indicating whether the window can be minimized.
73+ /// Always <see langword="false"/> for <see cref="ContentDialog"/>.
74+ /// </summary>
75+ public bool Minimizable => false ;
76+
77+ /// <summary>
78+ /// Gets the visual state of the window.
79+ /// <see cref="ContentDialog"/> report <see cref="WindowVisualState.Normal"/>.
80+ /// </summary>
81+ public WindowVisualState VisualState => WindowVisualState . Normal ;
82+
83+ /// <inheritdoc/>
84+ protected override string GetClassNameCore ( )
85+ {
86+ // "Emulating WinUI3's ContentDialog ClassName"
87+ return "Popup" ;
88+ }
89+
90+ /// <inheritdoc/>
91+ protected override string ? GetNameCore ( )
92+ {
93+ if ( Owner is ContentDialog dialog )
94+ {
95+ return dialog . Title as string ?? dialog . Title ? . ToString ( ) ;
96+ }
97+
98+ return base . GetNameCore ( ) ;
99+ }
100+
101+ /// <inheritdoc/>
102+ protected override AutomationControlType GetAutomationControlTypeCore ( )
103+ {
104+ return AutomationControlType . Window ;
105+ }
106+
107+ #if NET48_OR_GREATER || NET5_0_OR_GREATER
108+ /// <inheritdoc/>
109+ protected override bool IsDialogCore ( )
110+ {
111+ return true ;
112+ }
113+ #endif
114+
115+ /// <inheritdoc/>
116+ protected override bool IsControlElementCore ( )
117+ {
118+ return true ;
119+ }
120+
121+ /// <inheritdoc/>
122+ protected override bool IsContentElementCore ( )
123+ {
124+ return true ;
125+ }
126+
127+ /// <inheritdoc/>
128+ protected override bool IsKeyboardFocusableCore ( )
129+ {
130+ return false ;
131+ }
132+
133+ /// <summary>
134+ /// Returns whether the dialog is currently offscreen. A dialog is considered offscreen when not loaded or not visible.
135+ /// </summary>
136+ protected override bool IsOffscreenCore ( )
137+ {
138+ return Owner is ContentDialog { IsLoaded : false } or { IsVisible : false } ;
139+ }
140+
141+ /// <summary>
142+ /// Returns automation pattern implementations supported by this peer. Provides <see cref="IWindowProvider"/>.
143+ /// </summary>
144+ /// <param name="pattern">The requested automation pattern.</param>
145+ /// <returns>An object implementing the requested pattern or <see langword="null"/> when not supported.</returns>
146+ public override object ? GetPattern ( PatternInterface pattern )
147+ {
148+ // Include PatternInterface.ScrollItem to align with WinUI3 behavior: WinUI3 exposes this pattern
149+ // for dialog-like popups, and exposing it here helps automation clients that rely on that behavior.
150+ if ( pattern is PatternInterface . Window or PatternInterface . ScrollItem )
151+ {
152+ return this ;
153+ }
154+
155+ return null ;
156+ }
157+
158+ /// <summary>
159+ /// Closes the associated <see cref="ContentDialog"/>.
160+ /// This is invoked by UI Automation clients through the <see cref="IWindowProvider"/> pattern.
161+ /// </summary>
162+ void IWindowProvider . Close ( )
163+ {
164+ if ( Owner is ContentDialog dialog )
165+ {
166+ Dispatcher ? dispatcher = dialog . Dispatcher ;
167+ if ( dispatcher is { HasShutdownStarted : false , HasShutdownFinished : false } )
168+ {
169+ dispatcher . BeginInvoke (
170+ ( ) =>
171+ {
172+ dialog . Hide ( ) ;
173+ } ,
174+ DispatcherPriority . Normal
175+ ) ;
176+ }
177+ else
178+ {
179+ dialog . Hide ( ) ;
180+ }
181+ }
182+ }
183+
184+ /// <summary>
185+ /// Sets the visual state of the window. Not supported for <see cref="ContentDialog"/>.
186+ /// </summary>
187+ void IWindowProvider . SetVisualState ( WindowVisualState state )
188+ {
189+ // Not supported for this.
190+ }
191+
192+ /// <summary>
193+ /// Waits for the dialog to become idle.
194+ /// Always returns <see langword="true"/> for <see cref="ContentDialog"/>.
195+ /// </summary>
196+ /// <param name="milliseconds">Maximum time to wait in milliseconds (ignored).</param>
197+ /// <returns>
198+ /// <see langword="true"/> if the dialog is idle or the operation completed;
199+ /// otherwise <see langword="false"/>.
200+ /// </returns>
201+ public bool WaitForInputIdle ( int milliseconds )
202+ {
203+ return true ;
204+ }
205+ }
0 commit comments