18using System.Collections.Generic;
19using System.Diagnostics;
20using System.Runtime.InteropServices;
21using System.Threading;
22using System.Windows.Automation;
43 internal static class FileDialogHelper
48 private const string FILE_NAME_ID =
"1148";
49 private const string FILE_NAME_ALTERNATE_ID =
"1152";
50 private const string FILE_NAME_HOST_ID =
"FileNameControlHost";
51 private const string ACCEPT_ID =
"1";
54 private const int BUTTONS_LISTED = 8;
57 private const string BUTTON_CLASS =
"Button";
60 private const string CANCEL_ID =
"2";
67 [DllImport(
"user32.dll")]
68 private static extern bool IsWindow(IntPtr hWnd);
70 private const int BETWEEN_LOOKS = 250;
75 private const int SETTLING_MS = 5000;
78 private const int BETWEEN_CLOSING_LOOKS = 50;
89 internal static bool Answer(Application application, GPALFile file,
int waitSeconds)
93 AutomationElement dialog = WaitForDialog(application, waitSeconds);
96 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"No file dialog appeared within [{waitSeconds}]s, so there was nothing to answer. {WindowsSeen(application)}", application, GPALObjectType.Application);
97 else if (
null == file || 0 == file.Count)
98 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"A file dialog appeared but no file was named to answer it with.", application, GPALObjectType.Application);
101 AutomationElement field = FileNameField(dialog);
104 retVal = AnswerByField(application, dialog, field, file);
106 retVal = AnswerByTree(application, dialog, file);
113 if (
false == retVal &&
null != dialog)
114 Cancel(application, dialog);
133 private static AutomationElement WaitForDialog(Application application,
int waitSeconds)
135 AutomationElement retVal =
null;
136 Stopwatch waited = Stopwatch.StartNew();
138 while (
null == retVal && waited.ElapsedMilliseconds < waitSeconds * 1000)
140 retVal = FirstWindowWhere(application, LooksLikeADialog);
143 Thread.Sleep(BETWEEN_LOOKS);
160 private static AutomationElement FirstWindowWhere(Application application, Func<AutomationElement, bool> matches)
162 AutomationElement retVal =
null;
163 var isWindow =
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window);
165 if (
null != application?.RootAutomationElement)
167 retVal = FirstWindowUnder(application.RootAutomationElement, TreeScope.Children, isWindow, matches);
170 retVal = FirstWindowUnder(application.RootAutomationElement, TreeScope.Descendants, isWindow, matches);
174 foreach (AutomationElement top
in TopLevelWindows(application))
175 if (
true == matches(top))
194 private static List<AutomationElement> TopLevelWindows(Application application)
196 List<AutomationElement> retVal =
new List<AutomationElement>();
198 if (
false == application.applicationSettings.HiddenDesktop)
200 var isWindow =
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window);
202 foreach (AutomationElement window
in AutomationElement.RootElement.FindAll(TreeScope.Children, isWindow))
206 foreach (IntPtr handle
in Desktops.WindowsOn(application.applicationSettings.HiddenDesktopName))
208 AutomationElement onDesktop = AutomationElement.FromHandle(handle);
210 if (
null != onDesktop)
211 retVal.Add(onDesktop);
225 private static AutomationElement FirstWindowUnder(AutomationElement under, TreeScope scope, Condition isWindow, Func<AutomationElement, bool> matches)
227 AutomationElement retVal =
null;
231 foreach (AutomationElement window
in under.FindAll(scope, isWindow))
232 if (
null == retVal &&
true == matches(window))
255 private static Condition AcceptCondition()
257 return ButtonCondition(ACCEPT_ID);
263 private static Condition ButtonCondition(
string id)
265 return new AndCondition(
266 new PropertyCondition(AutomationElement.AutomationIdProperty,
id),
268 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button),
269 new PropertyCondition(AutomationElement.ClassNameProperty, BUTTON_CLASS)));
284 private static AutomationElement AcceptButton(AutomationElement window)
286 return ButtonWithId(window, ACCEPT_ID);
295 private static AutomationElement ButtonWithId(AutomationElement window,
string id)
297 AutomationElement retVal = window.FindFirst(TreeScope.Descendants, ButtonCondition(
id));
301 System.Collections.Generic.List<AutomationElement> raw = ElementHelper.RawMatches(window, ButtonCondition(
id),
false);
303 retVal = 0 < raw.Count ? raw[0] :
null;
319 private static bool LooksLikeADialog(AutomationElement window)
323 AutomationElement accept = AcceptButton(window);
326 retVal =
null != FileNameField(window)
327 ||
null != window.FindFirst(TreeScope.Descendants,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Tree));
341 private static string WindowsSeen(Application application)
343 var seen =
new List<string>();
345 if (
null != application?.RootAutomationElement)
347 Describe(seen,
"under the application", application.RootAutomationElement, TreeScope.Children);
348 Describe(seen,
"deeper in the application", application.RootAutomationElement, TreeScope.Descendants);
351 foreach (AutomationElement top
in TopLevelWindows(application))
352 Describe(seen,
true == application.applicationSettings.HiddenDesktop
353 ? $
"on [{application.applicationSettings.HiddenDesktopName}]"
354 :
"on the desktop", top, TreeScope.Element);
356 return 0 == seen.Count ?
"No windows were found in any of the places a dialog is looked for." : $
"Windows looked at: {string.Join(" |
", seen)}";
366 private static void Describe(List<string> seen,
string where, AutomationElement under, TreeScope scope)
373 AutomationElementCollection windows = TreeScope.Element == scope
374 ? under.FindAll(scope, Condition.TrueCondition)
375 : under.FindAll(scope,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window));
377 foreach (AutomationElement window
in windows)
379 bool accept =
null != AcceptButton(window);
381 int edits = window.FindAll(TreeScope.Descendants,
382 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit)).Count;
384 bool named =
null != window.FindFirst(TreeScope.Descendants,
new AndCondition(
385 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit),
386 new PropertyCondition(AutomationElement.AutomationIdProperty, FILE_NAME_ID)));
388 bool tree =
null != window.FindFirst(TreeScope.Descendants,
389 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Tree));
391 seen.Add($
"{where}: [{window.Current.ClassName}] '{window.Current.Name}' accept button {(true == accept ? "yes
" : $"NO, its buttons are {ButtonsIn(window)}
")}, field {FILE_NAME_ID} {(true == named ? "yes
" : "NO
")}, edits [{edits}], tree {(true == tree ? "yes
" : "no
")}");
412 private static string ButtonsIn(AutomationElement window)
414 var named =
new System.Collections.Generic.List<
string>();
416 AutomationElementCollection buttons = window.FindAll(TreeScope.Descendants,
417 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button));
419 for (
int button = 0; button < buttons.Count && named.Count < BUTTONS_LISTED; button++)
420 named.Add($
"[{buttons[button].Current.AutomationId}] '{buttons[button].Current.Name}'");
422 if (BUTTONS_LISTED <= named.Count && BUTTONS_LISTED < buttons.Count)
423 named.Add($
"and [{buttons.Count - BUTTONS_LISTED}] more");
425 return 0 == named.Count ?
"none in the control view" :
string.Join(
" ", named);
442 private static AutomationElement FileNameField(AutomationElement dialog)
444 var isEdit =
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit);
447 AutomationElement retVal = dialog.FindFirst(TreeScope.Descendants,
new AndCondition(isEdit,
448 new PropertyCondition(AutomationElement.AutomationIdProperty, FILE_NAME_ID)));
453 AutomationElement host = dialog.FindFirst(TreeScope.Descendants,
new AndCondition(
454 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ComboBox),
456 new PropertyCondition(AutomationElement.AutomationIdProperty, FILE_NAME_ID),
457 new PropertyCondition(AutomationElement.AutomationIdProperty, FILE_NAME_HOST_ID))));
460 retVal = host.FindFirst(TreeScope.Descendants, isEdit);
465 retVal = dialog.FindFirst(TreeScope.Descendants,
new AndCondition(isEdit,
466 new PropertyCondition(AutomationElement.AutomationIdProperty, FILE_NAME_ALTERNATE_ID)));
470 retVal = FieldBesideTheButton(dialog, isEdit);
482 private static AutomationElement FieldBesideTheButton(AutomationElement dialog, Condition isEdit)
484 AutomationElement retVal =
null;
486 AutomationElement accept = dialog.FindFirst(TreeScope.Descendants,
new AndCondition(
487 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button),
488 new PropertyCondition(AutomationElement.AutomationIdProperty, ACCEPT_ID)));
492 System.Windows.Rect button = accept.Current.BoundingRectangle;
493 double nearest =
double.MaxValue;
495 foreach (AutomationElement edit
in dialog.FindAll(TreeScope.Descendants, isEdit))
499 if (
false == edit.Current.IsOffscreen
500 &&
true == edit.Current.IsEnabled
501 &&
true == edit.TryGetCurrentPattern(ValuePattern.Pattern, out
object unused))
503 System.Windows.Rect field = edit.Current.BoundingRectangle;
504 double apart = Math.Abs((field.Top + field.Height / 2) - (button.Top + button.Height / 2));
506 if (field.Left < button.Left && apart < button.Height && apart < nearest)
526 private static bool AnswerByField(Application application, AutomationElement dialog, AutomationElement field, GPALFile file)
532 var names =
new List<string>();
534 foreach (
string name
in file.Filenames)
535 names.Add(1 == file.Count ? name : $
"\"{name}\"");
537 string answer =
string.Join(
" ", names);
539 if (
true == field.TryGetCurrentPattern(ValuePattern.Pattern, out
object valuePattern))
541 ((ValuePattern)valuePattern).SetValue(answer);
543 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Answered the file dialog with [{answer}]", application, GPALObjectType.Application);
545 retVal = Accept(application, dialog);
548 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"The file dialog's file name field cannot be written to, so the path could not be put in it.", application, GPALObjectType.Application);
565 private static bool AnswerByTree(Application application, AutomationElement dialog, GPALFile file)
569 AutomationElement tree = dialog.FindFirst(TreeScope.Descendants,
570 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Tree));
573 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The dialog has neither a file name field nor a folder tree, so it is not a common file dialog this can answer. {ElementHelper.WhatIsThere(dialog)}", application, GPALObjectType.Application);
576 AutomationElement current = tree;
577 string reached =
string.Empty;
579 foreach (
string segment
in file.Filename.Split(
new char[] {
'\\' }, StringSplitOptions.RemoveEmptyEntries))
581 AutomationElement next = ChildNamed(current, segment);
585 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The folder tree has no [{segment}] under [{(true == string.IsNullOrEmpty(reached) ? "the root
" : reached)}], so [{file.Filename}] cannot be reached. {ElementHelper.WhatIsThere(current)}", application, GPALObjectType.Application);
600 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Walked the folder tree to [{file.Filename}]", application, GPALObjectType.Application);
602 retVal = Accept(application, dialog);
616 private static AutomationElement ChildNamed(AutomationElement parent,
string segment)
618 AutomationElement retVal = parent.FindFirst(TreeScope.Children,
new AndCondition(
619 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TreeItem),
620 new PropertyCondition(AutomationElement.NameProperty, segment)));
623 foreach (AutomationElement child
in parent.FindAll(TreeScope.Children,
624 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TreeItem)))
626 string name = child.Current.Name ??
string.Empty;
628 if (
null == retVal && (
true == name.EndsWith($
"({segment})") ||
true == name.StartsWith($
"{segment} ", StringComparison.OrdinalIgnoreCase)))
639 private static void Expand(AutomationElement item)
641 if (
true == item.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out
object pattern)
642 && ExpandCollapseState.Collapsed == ((ExpandCollapsePattern)pattern).Current.ExpandCollapseState)
643 ((ExpandCollapsePattern)pattern).Expand();
650 private static void Select(AutomationElement item)
652 if (
true == item.TryGetCurrentPattern(SelectionItemPattern.Pattern, out
object pattern))
653 ((SelectionItemPattern)pattern).Select();
666 private static bool Accept(Application application, AutomationElement dialog)
668 bool retVal = PressAndWait(application, dialog, AcceptButton(dialog),
"accept");
683 private static void Cancel(Application application, AutomationElement dialog)
685 AutomationElement button = ButtonWithId(dialog, CANCEL_ID);
687 if (
true == PressAndWait(application, dialog, button,
"cancel"))
688 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"The file dialog could not be answered, so it was cancelled. The application is where it was before the dialog was raised.", application, GPALObjectType.Application);
690 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"The file dialog could not be answered and could not be cancelled either, so it is still on screen and everything after this goes into it.", application, GPALObjectType.Application);
706 private static bool PressAndWait(Application application, AutomationElement dialog, AutomationElement button,
string what)
709 bool pressed =
false;
713 IntPtr handle =
new IntPtr(dialog.Current.NativeWindowHandle);
716 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The file dialog has no {what} button. {ElementHelper.WhatIsThere(dialog)}", application, GPALObjectType.Application);
717 else if (
true == button.TryGetCurrentPattern(InvokePattern.Pattern, out
object pattern))
719 ((InvokePattern)pattern).Invoke();
723 else if (
true == MsaaHelper.Press(application, button))
725 else if (
true == ElementHelper.HardwareReaches(application))
727 ElementHelper.HardwareClick(application,
new GPALAutomationElement(button), ClickType.LeftClick, ModifierKeys.NONE);
732 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The file dialog's {what} button cannot be pressed. It offers no InvokePattern, MSAA would not press it, and the mouse cannot reach it here.", application, GPALObjectType.Application);
736 retVal = Settled(handle);
739 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"The file dialog's {what} button was pressed but the dialog is still on screen after [{SETTLING_MS / 1000}]s.", application, GPALObjectType.Application);
750 private static bool Settled(IntPtr handle)
753 Stopwatch waited = Stopwatch.StartNew();
755 while (
false == retVal && waited.ElapsedMilliseconds < SETTLING_MS)
757 retVal = IntPtr.Zero == handle ||
false == IsWindow(handle);
760 Thread.Sleep(BETWEEN_CLOSING_LOOKS);