GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ElementHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Collections.ObjectModel;
20using System.Diagnostics;
21using System.Drawing;
22using System.Linq;
23using System.Runtime.InteropServices;
24using System.Text.RegularExpressions;
25using System.Threading;
26using System.Windows;
27using System.Windows.Automation;
28using System.Windows.Forms;
29using OpenQA.Selenium;
30using OpenQA.Selenium.Interactions;
31using OpenQA.Selenium.Support.UI;
32using static GenerallyPositive.Enums;
34using Condition = System.Windows.Automation.Condition;
36
38{
39
40 internal class ElementHelper
41 {
42 delegate ReadOnlyCollection<GPALAutomationElement> FindElementsByDelegate(Application application, SelectorPathEntry selectorPath, bool searchForSelector = true, AutomationElement element = null);
43 // virtual keycodes: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes?redirectedfrom=MSDN
44 // scan codes: https://en.wikipedia.org/wiki/Scancode
45 static byte ScanCode {get; } = 0;
46
47 // to avoid repeat messages for the same selector when waiting for a selector
48 static List<string> listOfMessages = new List<string>();
49 static bool supressedMessage = false;
50 static Selector lastSelector = null;
51
64 public static ReadOnlyCollection<GPALAutomationElement> FindElements(Application application, UnitOfWork currentUOW, Selector selector, out bool matchedAll, out List<GPALAutomationElement> matchedElements, AutomationElement automationElement = null, bool callIfHandler = true)
65 {
66 int foundCount = 0;
67
68 string elementStringToMatch = null;
69 ReadOnlyCollection<GPALAutomationElement> retElems = null;
70 List<GPALAutomationElement> matchedElems = new List<GPALAutomationElement>();
71 MatchCollection mc = null;
72 ReadOnlyCollection<GPALAutomationElement> allFoundElems = null;
73 List<GPALAutomationElement> totalAllFoundElems = new List<GPALAutomationElement>();
74 List<GPALAutomationElement> iterateOverTheseElements = null;
75 bool tryNextSelector = false;
76 int criteriaCnt = 0;
77 string message = string.Empty;
78 int retries = 1;
79 bool retry = false;
80
81 matchedAll = false;
82 matchedElements = new List<GPALAutomationElement>();
83
84 currentUOW.CurrentSelector = selector;
85
86 if (false == lastSelector?.Name.Equals(selector.Name))
87 {
88 listOfMessages.Clear();
89 supressedMessage = false;
90 lastSelector = selector;
91 }
92 else if (null == lastSelector)
93 lastSelector = selector;
94
95 // !!!!!!!!!!!!!!! MUST MATCH THE ENUM ORDER !!!!!!!!!!!!!!!!!!
96 // public enum SelectorPathType { NotSet = 0, Css = 1, Image = 2, Text = 4, Value = 8, Xpath = 16, Name = 32, AutomationID = 64, ClassName = 128 };
97 List<FindElementsByDelegate> functionList = new List<FindElementsByDelegate> { null /* NotSet=0 */, FindElementsByCSS, FindElementsByImage, FindElementsByText, FindElementsByValue, FindElementsByXPath, FindElementsByName, FindElementsByAutomationID, FindElementsByClassName, FindElementsByPlaceholder };
98 // iterate selectors, finding elements
99 foreach (SelectorSettings.SelectorPathEntry selectorPathEntry in selector.SelectorPaths)
100 {
101 tryAgain: // NOTE: CAVEAT: goto label - don't like it, but it has to happen sometimes. we cannot have any inner loop as that will break retrying the next selector
102 int textLength = selectorPathEntry.SelectorPath.Length - 1;
103 string abbreviatedText = selectorPathEntry.SelectorPath.Substring(0, Math.Min(7, textLength)) + "..." + selectorPathEntry.SelectorPath.Substring(Math.Max(0, textLength - 7));
104 if (18 > selectorPathEntry.SelectorPath.Length)
105 abbreviatedText = selectorPathEntry.SelectorPath;
106
107 int idx = GetIndexOfEnum<SelectorPathType>((int)selectorPathEntry.SelectorPathType);
108 tryNextSelector = false;
109 string errorMessage = $"Error finding elements for selector [{selector.Name}][{abbreviatedText}]";
110
111 try
112 {
113 message = $"Searching for elements for selector [{selector.Name}] using [{selectorPathEntry.SelectorPathType}] [{abbreviatedText}]";
114
115 if (false == listOfMessages.Contains(message))
116 {
117 listOfMessages.Add(message);
118 GPAL.PublishSimpleEvent(GPALEventType.INFO, message, application, GPALObjectType.Application);
119 }
120
121 allFoundElems = functionList[idx](application, selectorPathEntry, selector.SearchForSelector, automationElement);
122
123 // if we get null back, we will retry once just to make sure
124 if (0 < (allFoundElems?.Count ?? 0)) // did not find any elements
125 totalAllFoundElems.AddRange(allFoundElems.ToList());
126
127 #region <Element Matching>
128 if (0 < selector.MatchCriteria.Count)
129 {
130 foreach (SelectorSettings.MatchCriteriaEntry matchCriteriaEntry in selector.MatchCriteria)
131 {
132 if (MatchType.Custom == matchCriteriaEntry.MatchType)
133 {
134 // match function returns true or false for matchedAll
135 CallIfStatus handled = matchCriteriaEntry.AppMatchingFunction(allFoundElems.ToList(), out matchedElems, out matchedAll, selector, matchCriteriaEntry.ExactMatch);
136
137 if (CallIfStatus.Terminate == handled)
138 {
139 string msg = $"Custom match handler [{matchCriteriaEntry.AppMatchingFunction.GetInvocationList()[0].Method.Name}] requested program termination for selector {selector.Name} path [{selectorPathEntry.SelectorPath}].";
140 GPAL.PublishSimpleEvent(GPALEventType.WARNING, msg, application, GPALObjectType.Application);
141 throw new GPALException(msg);
142 }
143
144 // always return matched elements, it's either all or just those matched
145 matchedElements.AddRange(matchedElems);
146 matchedElements = matchedElements.Distinct().ToList(); // dedupe // what order these will be in over iterations is anyone's guess, but they will be on one page
147
148 if (CallIfStatus.Handled == handled) // stop matching
149 break;
150 else if (CallIfStatus.TryNext == handled) // custom algorithm didn't like the result set and wants us to try the next selectorpath
151 {
152 tryNextSelector = true;
153 break;
154 }
155 }
156 }
157
158 if (true == tryNextSelector)
159 continue;
160
161 // if our custom match matched items, then apply further matching to these items
162 // else apply matching to all found elements
163 if (0 == matchedElements.Count)
164 iterateOverTheseElements = new List<GPALAutomationElement>(allFoundElems);
165 else
166 iterateOverTheseElements = new List<GPALAutomationElement>(matchedElements);
167
168 // we are done with idx, we can reuse, used as index into findelement method array
169 for (idx = 0; idx < iterateOverTheseElements.Count; idx++)
170 {
171 GPALAutomationElement automationElement2 = iterateOverTheseElements[idx];
172 criteriaCnt = 0;
173
174 for (int matchIdx = 0; matchIdx < selector.MatchCriteria.Count; matchIdx++)
175 {
176 SelectorSettings.MatchCriteriaEntry matchCriteriaEntry = selector.MatchCriteria[matchIdx];
177
178 if (MatchType.None != matchCriteriaEntry.MatchType)
179 {
180 criteriaCnt++; // keep count of how many match criteria we have, ALL must match on the element, we should have as many matched elements as criteria for ALL to match
181
182 // Mask out the REGEX 1 bit to get to the actual type
183 switch ((MatchType)((int)matchCriteriaEntry.MatchType & 0XFE))
184 {
185 case MatchType.Text:
186 elementStringToMatch = automationElement2.Ae.GetText();
187 break;
188
189 // none has a meaning on a desktop control, so they read as the
190 // element's text. placeholder had no case and compared against
191 // whatever the criterion before it had read, or null
192 case MatchType.Href:
193 case MatchType.Src:
194 case MatchType.Placeholder:
195 elementStringToMatch = automationElement2.Ae.GetText();
196 break;
197
198 // the short one on purpose. A value match is exact and the length of
199 // what is being matched is known, so a provider truncating a long value
200 // cannot change the answer. MatchText is the one that has to see a whole
201 // document, and a control offering no value still answers with its text
202 case MatchType.Value:
203 elementStringToMatch = automationElement2.Ae.GetValue() ?? automationElement2.Ae.GetText();
204 break;
205 }
206
207 if (1 == ((int)MatchType.Regex & (int)matchCriteriaEntry.MatchType))
208 {
209
210 mc = Regex.Matches(elementStringToMatch, matchCriteriaEntry.StringToMatch);
211 if (0 < mc.Count)
212 matchedElems.Add(automationElement2);
213 }
214 else
215 {
216 if (false == matchCriteriaEntry.ExactMatch && elementStringToMatch.Contains(matchCriteriaEntry.StringToMatch))
217 matchedElems.Add(automationElement2);
218 else if (true == matchCriteriaEntry.ExactMatch && elementStringToMatch.Equals(matchCriteriaEntry.StringToMatch))
219 matchedElems.Add(automationElement2);
220 }
221 }
222 }
223 if (criteriaCnt == matchedElems.Count) // we have one element that must be added once for every match, if not, it doesn't match all criteria, so try the next element
224 matchedElements.AddRange(matchedElems.Distinct()); // add this match to the out return list (dedupe it first)
225 }
226 matchedElements = matchedElements.Distinct().ToList(); // dedupe // what order these will be in over iterations is anyone's guess, but they will be on one page
227 matchedAll = (allFoundElems.Count == foundCount);
228 }
229 else // no match criteria - all found
230 {
231 matchedAll = true;
232 matchedElems = allFoundElems?.ToList() ?? new List<GPALAutomationElement>();
233 matchedElements = matchedElems; // out return list
234 }
235 #endregion <Element Matching>
236
237 }
238 catch (Exception ex) // element not found for some reason
239 {
240 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Exception thrown trying to find elements for selector [{selector.Name}]. Continuing.", currentUOW, GPALObjectType.UnitOfWork, ex);
241 continue;
242 }
243
244 // return only WithAllThatMatch row count of those that matched (or from all)
245 // matchedElems contains either All elememts or just the matched
246 if (int.MaxValue != currentUOW.WithAllThatMatch && 0 < matchedElements?.Count) // just WithAllThatMatch row count of elements
247 {
248 int elemIdx = 0;
249 List<GPALAutomationElement> tmpList = new List<GPALAutomationElement>();
250
251 // ensure we copy over only as many results as requested, or how many results we have
252 while (elemIdx < Math.Min((int)currentUOW.WithAllThatMatch, (int)matchedElements?.Count))
253 tmpList.Add(matchedElements[elemIdx++]);
254
255 matchedElements = tmpList;
256 }
257
258 selectorPathEntry.AppSelectorFoundResults = allFoundElems?.ToList() ?? new List<GPALAutomationElement>();
259 selectorPathEntry.AppSelectorMatchedResults = matchedElems;
260
261 retElems = new ReadOnlyCollection<GPALAutomationElement>(totalAllFoundElems.Distinct().ToList()); // always return everything found
262
263 foreach (GPALAutomationElement element in retElems)
264 {
265 element.Application = application;
266 element.Selector = selector;
267 }
268
269 foreach (GPALAutomationElement element in matchedElements)
270 {
271 element.Application = application;
272 element.Selector = selector;
273 }
274
275 if (true == retry && 0 < retries--)
276 {
277 retry = false;
278 goto tryAgain; // NOTE: CAVEAT: goto label
279 }
280
281 selector.ElementsFoundAndMatchedCount = matchedElements.Count();
282
283 message = $"[{retElems?.Count ?? 0}] elements found for selector [{selector.Name}]";
284
285 if (false == listOfMessages.Contains(message))
286 {
287 listOfMessages.Add(message);
288 GPAL.PublishSimpleEvent(GPALEventType.INFO, message, application, GPALObjectType.Application);
289
290 if (0 < retElems?.Count && (true == selector.AnyMatchCriteria() || Int32.MaxValue != currentUOW.WithAllThatMatch))
291 {
292 var matchCount = selector.MatchCriteria.Count > 0 ? selector.MatchCriteria.Count.ToString() : $"WithAllThatMatch({currentUOW.WithAllThatMatch})";
293
294 if ((true == matchCount.Contains("WithAllThatMatch") && 1 < currentUOW.WithAllThatMatch) || 0 < selector.MatchCriteria.Count)
295 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{matchedElements.Count}] elements matched for selector [{selector.Name}] with [{matchCount}] match criteria", application, GPALObjectType.Application);
296 }
297 }
298 else if (false == supressedMessage)
299 {
300 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", application, GPALObjectType.Application);
301 supressedMessage = true;
302 }
303
304 // CAVEAT:if one path matches all elements found, are we done? maybe the next path finds different elements we want to add?
305 // TODO: do we have a fluent switch 'TryAllPaths' or 'StopOnSuccess'
306 // For complex evaluations, roll your own matching function,
307 if (true == matchedAll || retElems?.Count > 0) // for now, if a selector path matches one match criteria, we consider ourselves done with matching
308 break;
309
310 } // foreach selectorpath
311
312 // false == callIfHandler will be the waitfor call
313 if (true == callIfHandler)
314 CallIfHandlers(application, currentUOW, allFoundElems, matchedElements, selector, matchedAll);
315
316 // a selector whose every path threw leaves both of these null, and the catch above says "Continuing",
317 // so counting them unguarded turned a handled search failure into a crash. Counted the way the two
318 // reads further up already do it, and the way the browser twin does
319 selector.ElementsFoundAndMatchedCount = true == selector.AnyMatchCriteria()
320 ? (matchedElements?.Count() ?? 0)
321 : (retElems?.Count ?? 0);
322
323 return retElems;
324 }
325
326 // TODO: what do these values mean outside of the bubble handlers?
327 // currentUOW.callFoundHandled
328 // currentUOW.callNotFoundHandled
339 private static void CallIfHandlers(Application application, UnitOfWork currentUOW, ReadOnlyCollection<GPALAutomationElement> foundElements, List<GPALAutomationElement> matchedElements, Selector selector, bool matchedAll)
340 {
341 string str = null;
342 string currentMethodName = null;
343 UnitOfWork safeUOW;
344
345 // TODO: do we care about matchedall?
346
347 // bubble thru handlers based upon handler return value
348 // 0 = not handled, bubble to next handler
349 // 1 = handled and continue with the program
350 // -1 = unexpected error and terminate program
351 if (0 < (foundElements?.Count ?? 0))
352 {
353 if (0 < selector.AppCallIfFound?.Count)
354 {
355 foreach (Application.CallIfDelegate func in selector.AppCallIfFound)
356 {
357 bool safeActionCalled = application.CurrentUOW.ActionCalled;
358 safeUOW = application.CurrentUOW;
359 application.CurrentUOW.ActionCalled = true; // this will allow the callif method to immediately use application.WaitFor
360
361 currentMethodName = func.Method.Name;
362 str = $"Application Selector CallIfFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
363
364 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Selector CallIfFound [{func.Method.Name}]", application, GPALObjectType.Application);
365
366 selector.AppCallIfFoundHandled = func(application, foundElements?.Cast<IGPALAutomationElement>().ToList(), matchedElements?.Cast<IGPALAutomationElement>().ToList(), selector, matchedAll);
367
368 application.CurrentUOW = safeUOW;
369 application.CurrentUOW.ActionCalled = safeActionCalled;
370
371 if (CallIfStatus.NotHandled == selector.AppCallIfFoundHandled)
372 continue;
373 else
374 break;
375 }
376
377 if (CallIfStatus.Terminate == selector.AppCallIfFoundHandled)
378 {
379 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Application);
380 throw new GPALException(str);
381 }
382 }
383
384 // selector.AppCallIfFoundHandled - if handled by selector handler, don't call UOW handler
385 if (0 == selector.AppCallIfFoundHandled && 0 < currentUOW.AppCallIfFound.Count)
386 {
387 foreach (Application.CallIfDelegate func in currentUOW.AppCallIfFound)
388 {
389 safeUOW = application.CurrentUOW;
390
391 currentMethodName = func.Method.Name;
392 str = $"Application UOW Selector CallIfFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
393
394 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking UOW CallIfFound [{func.Method.Name}]", application, GPALObjectType.Application);
395
396 currentUOW.CallIfNotFoundHandled = func(application, foundElements?.Cast<IGPALAutomationElement>().ToList(), matchedElements?.Cast<IGPALAutomationElement>().ToList(), selector, matchedAll);
397
398 application.CurrentUOW = safeUOW;
399
400 if (CallIfStatus.NotHandled == currentUOW.CallIfNotFoundHandled)
401 continue;
402 else
403 break;
404 }
405
406 if (CallIfStatus.Terminate == currentUOW.AppCallIfFoundHandled)
407 {
408 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Application);
409 throw new GPALException(str);
410 }
411 }
412
413 // currentUOW.AppCallIfFoundHandled - if handled by UOW handler, don't call global handler
414 if (0 == currentUOW.AppCallIfFoundHandled && 0 < GPAL.GPALSettings.AppCallIfFoundList.Count)
415 {
416 foreach (Application.CallIfDelegate func in GPAL.GPALSettings.AppCallIfFoundList)
417 {
418 safeUOW = application.CurrentUOW;
419
420 currentMethodName = func.Method.Name;
421 str = $"Gloabl CallIfFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
422
423 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Global CallIfFound [{func.Method.Name}]", application, GPALObjectType.Application);
424
425 GPAL.GPALSettings.AppCallIfFoundHandled = func(application, foundElements?.Cast<IGPALAutomationElement>().ToList(), matchedElements?.Cast<IGPALAutomationElement>().ToList(), selector, matchedAll);
426
427 application.CurrentUOW = safeUOW;
428
429 if (CallIfStatus.NotHandled == GPAL.GPALSettings.AppCallIfFoundHandled)
430 continue;
431 else
432 break;
433 }
434
435 if (CallIfStatus.Terminate == GPAL.GPALSettings.AppCallIfFoundHandled)
436 {
437 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Browser);
438 throw new GPALException(str);
439 }
440 }
441 }
442 else
443 {
444 if (0 < selector.AppCallIfNotFound.Count)
445 {
446 foreach (Application.CallIfDelegate func in currentUOW.AppCallIfNotFound)
447 {
448 safeUOW = application.CurrentUOW;
449
450 currentMethodName = func.Method.Name;
451 str = $"Application Selector CallIfNotFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
452
453 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Selector CallIfNotFound [{func.Method.Name}]", application, GPALObjectType.Application);
454
455 selector.AppCallIfNotFoundHandled = func(application, foundElements?.Cast<IGPALAutomationElement>().ToList(), matchedElements?.Cast<IGPALAutomationElement>().ToList(), selector, matchedAll);
456
457 application.CurrentUOW = safeUOW;
458
459 if (CallIfStatus.NotHandled == selector.AppCallIfNotFoundHandled)
460 continue;
461 else
462 break;
463 }
464
465 if (CallIfStatus.Terminate == currentUOW.AppCallIfNotFoundHandled)
466 {
467 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Application);
468 throw new GPALException(str);
469 }
470 }
471
472 if (0 == selector.AppCallIfNotFoundHandled && 0 < currentUOW.AppCallIfNotFound.Count)
473 {
474 foreach (Application.CallIfDelegate func in currentUOW.AppCallIfNotFound)
475 {
476 safeUOW = application.CurrentUOW;
477
478 currentMethodName = func.Method.Name;
479 str = $"Application UOW Selector CallIfNotFound handler [{currentMethodName}] requested program termination on selector ]{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
480
481 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking UOW CallIfNotFound [{func.Method.Name}]", application, GPALObjectType.Application);
482
483 currentUOW.AppCallIfNotFoundHandled = func(application, foundElements?.Cast<IGPALAutomationElement>().ToList(), matchedElements?.Cast<IGPALAutomationElement>().ToList(), selector, matchedAll);
484
485 application.CurrentUOW = safeUOW;
486
487 if (CallIfStatus.NotHandled == currentUOW.AppCallIfNotFoundHandled)
488 continue;
489 else
490 break;
491 }
492
493 if (CallIfStatus.Terminate == currentUOW.AppCallIfNotFoundHandled)
494 {
495 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Application);
496 throw new GPALException(str);
497 }
498 }
499
500 // currentUOW.AppCallIfNotFoundHandled - if handled by UOW handler, don't call global handler
501 if (0 == currentUOW.AppCallIfNotFoundHandled && 0 < GPAL.GPALSettings.AppCallIfNotFoundList.Count)
502 {
503 foreach (Application.CallIfDelegate func in GPAL.GPALSettings.AppCallIfNotFoundList)
504 {
505 safeUOW = application.CurrentUOW;
506
507 currentMethodName = func.Method.Name;
508 str = $"Gloabl CallIfNotFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
509
510 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Global CallIfNotFound [{func.Method.Name}]", application, GPALObjectType.Application);
511
512 GPAL.GPALSettings.AppCallIfNotFoundHandled = func(application, foundElements?.Cast<IGPALAutomationElement>().ToList(), matchedElements?.Cast<IGPALAutomationElement>().ToList(), selector, matchedAll);
513
514 application.CurrentUOW = safeUOW;
515
516 if (CallIfStatus.NotHandled == GPAL.GPALSettings.AppCallIfNotFoundHandled)
517 continue;
518 else
519 break;
520 }
521
522 if (CallIfStatus.Terminate == GPAL.GPALSettings.AppCallIfNotFoundHandled)
523 {
524 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Browser);
525 throw new GPALException(str);
526 }
527 }
528
529 if (true == selector.StopOnNotFound || true == GPAL.StopOnNotFound)
530 {
531 str = $@"{(true == selector.StopOnNotFound ? "selector." : "GPAL.")}StopOnNotFound set, terminating on Not Found element for [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
532 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, application, GPALObjectType.Application);
533 throw new GPALException(str);
534 }
535 }
536 }
546 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByImage(Application application, SelectorPathEntry selectorPath, bool searchForSelector = true, AutomationElement element = null)
547 {
548 ReadOnlyCollection<GPALAutomationElement> elems = null;
549
550 Rectangle foundImageBounds = new Rectangle();
551 List<GPALAutomationElement> retElement = new List<GPALAutomationElement>(); ;
552
553 if (null == selectorPath.Image)
554 {
555 try
556 {
557 selectorPath.Image = Image.FromFile(selectorPath.SelectorPath);
558 }
559 catch (Exception ex)
560 {
561 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to load image for [{selectorPath.SelectorPath}]", application, GPALObjectType.Application, ex);
562 return null;
563 }
564 }
565
566 Image template = selectorPath.Image;
567 foundImageBounds = ((ImageHelper)GPAL.ImageHelper).FindImage(selectorPath.SelectorPath, application); // first check, is image we seek already on-screen?
568
569 // not on screen: from the top of the application's scrolling view, page down through it the way the
570 // browser pages down a document, until the image shows up or the view is at its end. ScrollPattern moves
571 // the view without a keystroke, so this works on a hidden desktop too
572 // with no matcher registered nothing can be found, so the view is not paged through for nothing
573 if (true == searchForSelector && true == foundImageBounds.IsEmpty && null != GPAL.ImageMatcher)
574 {
575 ScrollPattern view = application.ScrollableView();
576
577 if (null != view)
578 {
579 view.SetScrollPercent(ScrollPattern.NoScroll, 0);
580 Thread.Sleep(250);
581 foundImageBounds = ((ImageHelper)GPAL.ImageHelper).FindImage(selectorPath.SelectorPath, application);
582
583 // a page step that does not move the view is the end too, so a control that will not scroll
584 // cannot hold the search forever
585 double was = -1;
586
587 while (true == foundImageBounds.IsEmpty && 100 > Math.Round(view.Current.VerticalScrollPercent) && was != view.Current.VerticalScrollPercent)
588 {
589 was = view.Current.VerticalScrollPercent;
590 view.ScrollVertical(ScrollAmount.LargeIncrement);
591 Thread.Sleep(250);
592 foundImageBounds = ((ImageHelper)GPAL.ImageHelper).FindImage(selectorPath.SelectorPath, application);
593 }
594 }
595 }
596
597 if (false == foundImageBounds.IsEmpty)
598 {
599 // create a psuedo automationelement to use in the subsequent methods
600 retElement.Add(new GPALAutomationElement(null, new GPALElement(foundImageBounds.Location, foundImageBounds.Size, selectorPath.SelectorPathType.ToString(), null, "GPALElement")));
601 elems = new ReadOnlyCollection<GPALAutomationElement>(retElement);
602
603 // an image match is one more way of finding a control. unless the workflow asked for hardware, the
604 // control under the match is handed back instead, so what follows is an ordinary UIAutomation
605 // action. a hidden desktop is left alone, since the match was made against the visible screen
606 Selector selector = application.CurrentUOW?.CurrentSelector;
607
608 if (false == application.applicationSettings.HiddenDesktop
609 && false == application.applicationSettings.UseHardware
610 && Enums.InteractionType.Hardware != selector?.InteractionType)
611 {
612 GPALAutomationElement under = ControlUnderImage(application, foundImageBounds, selector);
613
614 if (null != under)
615 {
616 under.ImageMatch = retElement[0];
617 elems = new ReadOnlyCollection<GPALAutomationElement>(new List<GPALAutomationElement> { under });
618 }
619 }
620 }
621
622 return elems;
623 }
641 private static GPALAutomationElement ControlUnderImage(Application application, Rectangle match, Selector selector)
642 {
643 GPALAutomationElement retVal = null;
644
645 // both zero means nothing was asked for, the middle of the match, same rule the clicks use
646 int aimX = match.X + ((0 == (selector?.OffsetX ?? 0) && 0 == (selector?.OffsetY ?? 0)) ? match.Width / 2 : selector.OffsetX);
647 int aimY = match.Y + ((0 == (selector?.OffsetX ?? 0) && 0 == (selector?.OffsetY ?? 0)) ? match.Height / 2 : selector.OffsetY);
648
649 try
650 {
651 AutomationElement ae = AutomationElement.FromPoint(new System.Windows.Point(aimX, aimY));
652
653 if (null != ae)
654 {
655 // walked in the raw view, since WinForms hides whole controls from the control view. stops at a
656 // top level window, whose parent is the desktop
657 AutomationElement step = ae;
658 AutomationElement actionable = null;
659
660 while (null == actionable && null != step && false == Automation.Compare(step, AutomationElement.RootElement))
661 {
662 if (true == IsActionable(step))
663 actionable = step;
664 else
665 step = TreeWalker.RawViewWalker.GetParent(step);
666 }
667
668 retVal = new GPALAutomationElement(actionable ?? ae);
669 }
670 }
671 catch (Exception ex)
672 {
673 // a control that goes away while it is being read leaves only the point
674 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Could not read the control under the image match for [{selector?.Name}]", application, GPALObjectType.Application, ex);
675 }
676
677 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, null != retVal
678 ? $"Image match for [{selector?.Name}] is control [{retVal.Ae.Current.ControlType.ProgrammaticName}][{retVal.Ae.Current.Name}], clicked at the matched point"
679 : $"Image match for [{selector?.Name}] has no control of its own under it, so only the point is used", application, GPALObjectType.Application);
680
681 return retVal;
682 }
689 private static bool IsActionable(AutomationElement ae)
690 {
691 ControlType type = ae.Current.ControlType;
692
693 return true == (bool)ae.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)
694 || true == (bool)ae.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)
695 || true == (bool)ae.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)
696 || true == (bool)ae.GetCurrentPropertyValue(AutomationElement.IsSelectionItemPatternAvailableProperty)
697 || true == (bool)ae.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)
698 || ControlType.Button == type || ControlType.MenuItem == type || ControlType.CheckBox == type
699 || ControlType.RadioButton == type || ControlType.Edit == type || ControlType.Hyperlink == type
700 || ControlType.ComboBox == type || ControlType.ListItem == type || ControlType.TabItem == type
701 || ControlType.TreeItem == type || ControlType.SplitButton == type || ControlType.DataItem == type;
702 }
711 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByCSS(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
712 {
713 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Find elements by CSS is not supported for applications.", application, GPALObjectType.Application);
714 return null;
715 }
724 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByPlaceholder(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
725 {
726 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Find elements by placeholder is not supported for applications.", application, GPALObjectType.Application);
727 return null;
728 }
729
745 private static bool RootedAtDesktop(string selectorPath)
746 {
747 string[] tokens = (selectorPath ?? string.Empty).Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
748
749 return 1 < tokens.Length
750 && "desktop" == tokens[0].ToLowerInvariant()
751 && false == tokens[1].ToLowerInvariant().StartsWith("window");
752 }
753
754 [DllImport("user32.dll", SetLastError = true)]
755 private static extern IntPtr GetMenu(IntPtr window);
756
757 [DllImport("user32.dll", SetLastError = true)]
758 private static extern IntPtr GetSubMenu(IntPtr menu, int position);
759
760 [DllImport("user32.dll", SetLastError = true)]
761 private static extern int GetMenuItemCount(IntPtr menu);
762
763 [DllImport("user32.dll", SetLastError = true)]
764 private static extern int GetMenuItemID(IntPtr menu, int position);
765
766 [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
767 private static extern int GetMenuString(IntPtr menu, int item, System.Text.StringBuilder text, int count, uint flags);
768
769 [DllImport("user32.dll", SetLastError = true)]
770 private static extern bool PostMessage(IntPtr window, int message, IntPtr wParam, IntPtr lParam);
771
772 private const uint MF_BYPOSITION = 0x0400;
773 private const int WM_COMMAND = 0x0111;
774
781 private static List<string> MenuNamesIn(string selectorPath)
782 {
783 List<string> retVal = new List<string>();
784
785 foreach (Match step in Regex.Matches(selectorPath ?? string.Empty, @"MenuItem[^/]*?\‍[@Name='([^']*)'\‍]"))
786 retVal.Add(step.Groups[1].Value);
787
788 return retVal;
789 }
790
801 private static string MenuItemName(IntPtr menu, int position)
802 {
803 System.Text.StringBuilder text = new System.Text.StringBuilder(256);
804
805 GetMenuString(menu, position, text, text.Capacity, MF_BYPOSITION);
806
807 string name = text.ToString().Replace("&", string.Empty);
808 int shortcut = name.IndexOf('\t');
809
810 return (0 <= shortcut ? name.Substring(0, shortcut) : name).Trim();
811 }
812
826 private static int CommandForMenuPath(IntPtr menu, List<string> names, int index)
827 {
828 int retVal = 0;
829
830 if (IntPtr.Zero != menu && index < names.Count)
831 for (int position = 0; position < GetMenuItemCount(menu) && 0 == retVal; position++)
832 {
833 if (false == string.Equals(MenuItemName(menu, position), names[index], StringComparison.OrdinalIgnoreCase))
834 continue;
835
836 IntPtr inside = GetSubMenu(menu, position);
837
838 retVal = index == names.Count - 1 && IntPtr.Zero == inside
839 ? GetMenuItemID(menu, position)
840 : CommandForMenuPath(inside, names, index + 1);
841 }
842
843 return retVal;
844 }
845
852 private static GPALAutomationElement MenuItemFor(Application application, string selectorPath)
853 {
854 GPALAutomationElement retVal = null;
855 List<string> names = MenuNamesIn(selectorPath);
856 AutomationElement root = application.RootAutomationElement;
857
858 if (0 < names.Count && null != root)
859 {
860 IntPtr window = new IntPtr(root.Current.NativeWindowHandle);
861 int command = CommandForMenuPath(GetMenu(window), names, 0);
862
863 if (0 != command)
864 {
865 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{string.Join(" then ", names)}] was not there to be walked, so it was read from the window's own menu, where it sends command [{command}].", application, GPALObjectType.Application);
866
867 retVal = new GPALAutomationElement { MenuCommand = command, MenuWindow = window };
868 }
869 }
870
871 return retVal;
872 }
873
890 private static List<AutomationElement> StartingPoints(Application application, string selectorPath)
891 {
892 List<AutomationElement> retVal = new List<AutomationElement>();
893
894 if (false == RootedAtDesktop(selectorPath))
895 retVal.Add(application.RootAutomationElement);
896 else if (false == application.applicationSettings.HiddenDesktop)
897 retVal.Add(AutomationElement.RootElement);
898 else
899 {
900 // the application's own window first, since that is what most paths are after and it saves
901 // walking the rest
902 retVal.Add(application.RootAutomationElement);
903
904 foreach (IntPtr window in Desktops.WindowsOn(application.applicationSettings.HiddenDesktopName))
905 {
906 AutomationElement onDesktop = AutomationElement.FromHandle(window);
907
908 if (null != onDesktop && false == onDesktop.Equals(application.RootAutomationElement))
909 retVal.Add(onDesktop);
910 }
911 }
912
913 retVal.RemoveAll(each => null == each);
914
915 return retVal;
916 }
917
918 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByXPath(
919 Application application,
920 SelectorPathEntry selectorPath,
921 bool searchForElement = true,
922 AutomationElement element = null)
923 {
924 if (string.IsNullOrWhiteSpace(selectorPath?.SelectorPath))
925 return new ReadOnlyCollection<GPALAutomationElement>(Array.Empty<GPALAutomationElement>());
926
927 List<AutomationElement> roots = null != element
928 ? new List<AutomationElement> { element }
929 : StartingPoints(application, selectorPath.SelectorPath);
930
931 AutomationElement current = 0 < roots.Count ? roots[0] : null;
932
933 if (current == null)
934 return new ReadOnlyCollection<GPALAutomationElement>(Array.Empty<GPALAutomationElement>());
935
936 string[] tokens = selectorPath.SelectorPath.Split('/');
937
938 // the last step of a path is the element being asked for, not a step on the way to it
939 int lastToken = Array.FindLastIndex(tokens, t => false == string.IsNullOrWhiteSpace(t));
940
941 WalkFailure failure = new WalkFailure();
942 List<AutomationElement> found = new List<AutomationElement>();
943
944 foreach (AutomationElement root in roots)
945 {
946 found = WalkFrom(application, root, tokens, 0, lastToken, true, failure);
947
948 if (0 < found.Count)
949 break;
950 }
951
952 // and a menu that would not open is a path that cannot be walked however the tree is searched, so
953 // the window's own menu is asked instead. That is the only way in on a desktop that is not
954 // receiving input, where opening a menu at all is the mouse or the keyboard
955 GPALAutomationElement throughTheMenu = 0 == found.Count ? MenuItemFor(application, selectorPath.SelectorPath) : null;
956
957 if (0 == found.Count && null == throughTheMenu)
958 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to locate element for Selector [{selectorPath.SelectorPath}], stopped at token [{failure.Token}]. {WhatIsThere(failure.Under ?? current)}", application, GPALObjectType.Application);
959
960 List<GPALAutomationElement> retVal = new List<GPALAutomationElement>();
961
962 foreach (AutomationElement each in found)
963 retVal.Add(new GPALAutomationElement(each));
964
965 if (null != throughTheMenu)
966 retVal.Add(throughTheMenu);
967
968 return new ReadOnlyCollection<GPALAutomationElement>(retVal);
969 }
970
976 private class WalkFailure
977 {
978 internal int Depth = -1;
979 internal string Token;
980 internal AutomationElement Under;
981 }
982
1007 private static List<AutomationElement> WalkFrom(Application application, AutomationElement current, string[] tokens, int index, int lastToken, bool firstToken, WalkFailure failure)
1008 {
1009 List<AutomationElement> result = new List<AutomationElement>();
1010
1011 // an empty step from a leading slash, and Desktop, which names no control type and carries no
1012 // attributes, are steps with nothing to match. They are passed over rather than failed on
1013 int next = index;
1014 bool descendant = false;
1015 Condition condition = null;
1016
1017 while (next < tokens.Length && null == (condition = ConditionFor(tokens[next])))
1018 {
1019 // an empty step that follows another step is the second slash of a descendant axis, as in
1020 // Window//Button, which asks for the next step anywhere below instead of directly under. The
1021 // empty step a leading slash makes is the first of all, with nothing above it to descend from
1022 if (true == string.IsNullOrWhiteSpace(tokens[next]) && 0 < next)
1023 descendant = true;
1024
1025 next++;
1026 }
1027
1028 if (next >= tokens.Length)
1029 result.Add(current);
1030 else
1031 {
1032 List<AutomationElement> candidates = new List<AutomationElement>();
1033
1034 // RootAutomationElement is the window itself, which is what the recorder roots its path at, so
1035 // the first step is matched against the current element before its children
1036 if (true == firstToken)
1037 {
1038 AutomationElement self = current.FindFirst(TreeScope.Element, condition);
1039
1040 if (null != self)
1041 candidates.Add(self);
1042 }
1043
1044 if (0 == candidates.Count)
1045 foreach (AutomationElement child in current.FindAll(true == descendant ? TreeScope.Descendants : TreeScope.Children, condition))
1046 candidates.Add(child);
1047
1048 // and when that finds nothing, the same step against the raw view. FindAll searches the
1049 // control view, which holds what a framework chose to publish, and WinForms leaves whole
1050 // controls out of it: a spinner on a tab page is missing from the control view with only its
1051 // two buttons hoisted up in its place, while the raw view has the spinner, the edit box inside
1052 // it and the tab page they sit on. Looking here second rather than first means every path that
1053 // already worked still walks the tree it always walked
1054 if (0 == candidates.Count)
1055 candidates = RawMatches(current, condition, descendant);
1056
1057 // a step written as Pane[2] names the second match and no others, so the rest stop being
1058 // candidates. A position past the end of the matches names nothing, which fails the step
1059 int position = IndexFor(tokens[next]);
1060
1061 if (0 < position)
1062 candidates = position <= candidates.Count
1063 ? new List<AutomationElement> { candidates[position - 1] }
1064 : new List<AutomationElement>();
1065
1066 if (0 == candidates.Count && next > failure.Depth)
1067 {
1068 failure.Depth = next;
1069 failure.Token = tokens[next];
1070 failure.Under = current;
1071 }
1072
1073 foreach (AutomationElement candidate in candidates)
1074 {
1075 // Expand a menu item to expose what is inside it, but never the one the path is asking for:
1076 // expanding falls through to InvokePattern, which activates it, and finding an element is
1077 // not supposed to click it
1078 if (candidate.Current.ControlType == ControlType.MenuItem && next < lastToken)
1079 ExpandMenuItem(application, candidate);
1080
1081 List<AutomationElement> reached = WalkFrom(application, candidate, tokens, next + 1, lastToken, false, failure);
1082
1083 if (0 == reached.Count)
1084 continue;
1085
1086 result.AddRange(reached);
1087
1088 // a step before the last one is a way through, and one way through is all that is wanted.
1089 // The last step is the element being asked for, so every match of it is kept
1090 if (next < lastToken)
1091 break;
1092 }
1093 }
1094
1095 return result;
1096 }
1097
1111 internal static List<AutomationElement> RawMatches(AutomationElement current, Condition condition, bool descendant)
1112 {
1113 List<AutomationElement> retVal = new List<AutomationElement>();
1114 TreeWalker walker = TreeWalker.RawViewWalker;
1115
1116 for (AutomationElement child = walker.GetFirstChild(current); null != child; child = walker.GetNextSibling(child))
1117 {
1118 if (true == Matches(child, condition))
1119 retVal.Add(child);
1120
1121 if (true == descendant)
1122 retVal.AddRange(RawMatches(child, condition, true));
1123 }
1124
1125 return retVal;
1126 }
1127
1139 private static bool Matches(AutomationElement element, Condition condition)
1140 {
1141 bool retVal = false;
1142
1143 if (ReferenceEquals(Condition.TrueCondition, condition))
1144 retVal = true;
1145 else if (condition is PropertyCondition property)
1146 {
1147 object wanted = property.Value;
1148 object actual = element.GetCurrentPropertyValue(property.Property);
1149
1150 // a control type condition holds the type's number rather than the type, while the element
1151 // answers with the type itself, so those two are compared on the number they both carry
1152 retVal = actual is ControlType controlType && wanted is int wantedType
1153 ? controlType.Id == wantedType
1154 : Equals(actual, wanted);
1155 }
1156 else if (condition is AndCondition all)
1157 {
1158 retVal = true;
1159
1160 foreach (Condition each in all.GetConditions())
1161 if (false == Matches(element, each))
1162 {
1163 retVal = false;
1164 break;
1165 }
1166 }
1167 else if (condition is OrCondition any)
1168 {
1169 foreach (Condition each in any.GetConditions())
1170 if (true == Matches(element, each))
1171 {
1172 retVal = true;
1173 break;
1174 }
1175 }
1176 else if (condition is NotCondition other)
1177 retVal = false == Matches(element, other.Condition);
1178
1179 return retVal;
1180 }
1181
1187 private static int IndexFor(string token)
1188 {
1189 int result = 0;
1190 int start = (token ?? string.Empty).IndexOf('[');
1191
1192 // a step can carry a position and attributes together, as in Pane[2][@ClassName='x'], so the
1193 // brackets are read in turn until one of them holds a number
1194 while (0 < start && 0 == result)
1195 {
1196 int end = token.IndexOf(']', start);
1197
1198 if (0 > end)
1199 break;
1200
1201 if (true == int.TryParse(token.Substring(start + 1, end - start - 1), out int position) && 0 < position)
1202 result = position;
1203
1204 start = token.IndexOf('[', end);
1205 }
1206
1207 return result;
1208 }
1209
1215 private static Condition ConditionFor(string token)
1216 {
1217 Condition result = null;
1218
1219 if (false == string.IsNullOrWhiteSpace(token))
1220 {
1221 string controlTypeStr = token;
1222 var conditions = new List<Condition>();
1223
1224 int attrStart = token.IndexOf('[');
1225 if (attrStart > 0)
1226 {
1227 controlTypeStr = token.Substring(0, attrStart);
1228
1229 // Parse attributes [@Name='xxx'][@ClassName='yyy'] etc.
1230 string remaining = token.Substring(attrStart);
1231 while (remaining.Contains("[@"))
1232 {
1233 int start = remaining.IndexOf("[@");
1234 int end = remaining.IndexOf(']', start);
1235 if (end == -1) break;
1236
1237 string attrSegment = remaining.Substring(start + 2, end - start - 2);
1238
1239 var parts = attrSegment.Split(new char[] { '=' }, 2);
1240
1241 if (parts.Length == 2)
1242 {
1243 string attrName = parts[0].Trim();
1244 string value = parts[1].Trim().Trim('\'', '"');
1245
1246 Condition cond = attrName.ToLowerInvariant() switch
1247 {
1248 "name" => new PropertyCondition(AutomationElement.NameProperty, value),
1249 "automationid" => new PropertyCondition(AutomationElement.AutomationIdProperty, value),
1250 "classname" => new PropertyCondition(AutomationElement.ClassNameProperty, value),
1251 _ => null
1252 };
1253
1254 if (cond != null)
1255 conditions.Add(cond);
1256 }
1257
1258 remaining = remaining.Substring(end + 1);
1259 }
1260 }
1261
1262 // Add ControlType condition
1263 ControlType controlType = GetControlType(controlTypeStr);
1264 if (controlType != null)
1265 conditions.Add(new PropertyCondition(AutomationElement.ControlTypeProperty, controlType));
1266
1267 if (1 == conditions.Count)
1268 result = conditions[0];
1269 else if (1 < conditions.Count)
1270 result = new AndCondition(conditions.ToArray());
1271 }
1272
1273 return result;
1274 }
1275
1282 private static ControlType GetControlType(string programmaticName)
1283 {
1284 if (string.IsNullOrWhiteSpace(programmaticName))
1285 return null;
1286
1287 // Force initialization of the static constructor (important quirk!)
1288 _ = ControlType.Button.ToString();
1289
1290 // Try direct lookup by name via the known AutomationControlType enum + offset
1291 if (Enum.TryParse<System.Windows.Automation.Peers.AutomationControlType>(programmaticName, true, out var enumValue))
1292 {
1293 int id = (int)enumValue + 50000; // Official offset for ControlType IDs
1294 return ControlType.LookupById(id);
1295 }
1296
1297 // Fallback: try common aliases
1298 return programmaticName.ToLowerInvariant() switch
1299 {
1300 "window" => ControlType.Window,
1301 "pane" => ControlType.Pane,
1302 "button" => ControlType.Button,
1303 "edit" => ControlType.Edit, // TextBox
1304 "textbox" => ControlType.Edit,
1305 "combobox" => ControlType.ComboBox,
1306 "list" => ControlType.List,
1307 "listitem" => ControlType.ListItem,
1308 "menuitem" => ControlType.MenuItem,
1309 "tab" => ControlType.Tab,
1310 "tabitem" => ControlType.TabItem,
1311 "tree" => ControlType.Tree,
1312 "treeitem" => ControlType.TreeItem,
1313 "document" => ControlType.Document,
1314 "group" => ControlType.Group,
1315 "checkbox" => ControlType.CheckBox,
1316 "radiobutton" => ControlType.RadioButton,
1317 "hyperlink" => ControlType.Hyperlink,
1318 "image" => ControlType.Image,
1319 "spinner" => ControlType.Spinner,
1320 "slider" => ControlType.Slider,
1321 "statusbar" => ControlType.StatusBar,
1322 "toolbar" => ControlType.ToolBar,
1323 "datagrid" => ControlType.DataGrid,
1324 "dataitem" => ControlType.DataItem,
1325 _ => null
1326 };
1327 }
1336 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByAutomationID(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
1337 {
1338 return FindElementsBy(application, selectorPath, ConditionPropertyType.AutomationID, searchForElement, element);
1339 }
1348 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByText(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
1349 {
1350 return FindElementsBy(application, selectorPath, ConditionPropertyType.Value, searchForElement, element); // TODO: CAVEAT: KLUDGE: is this correct? Value?
1351 }
1360 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByValue(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
1361 {
1362 return FindElementsBy(application, selectorPath, ConditionPropertyType.Value, searchForElement, element);
1363 }
1372 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByName(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
1373 {
1374 return FindElementsBy(application, selectorPath, ConditionPropertyType.Name, searchForElement, element);
1375 }
1384 private static ReadOnlyCollection<GPALAutomationElement> FindElementsByClassName(Application application, SelectorPathEntry selectorPath, bool searchForElement = true, AutomationElement element = null)
1385 {
1386 return FindElementsBy(application, selectorPath, ConditionPropertyType.ClassName, searchForElement, element);
1387 }
1398 private static ReadOnlyCollection<GPALAutomationElement> FindElementsBy(Application application, SelectorPathEntry selectorPath, ConditionPropertyType conditionPropertyType, bool searchForElement = true, AutomationElement element = null)
1399 {
1400 ReadOnlyCollection<GPALAutomationElement> elem;
1401
1402 if (null != element)
1403 elem = element.FindElements(selectorPath.SelectorPath, conditionPropertyType);
1404 else
1405 elem = application.RootAutomationElement.FindElements(selectorPath.SelectorPath, conditionPropertyType);
1406
1407 return elem;
1408 }
1418 internal static void PublishToEventHandler(Application application, Selector selector, List<GPALAutomationElement> elements)
1419 {
1420 string msg = "";
1421 if (true == selector.DeleteMe)
1422 msg = $"Ignoring selector {selector.Name} marked for deletion. Continuing.";
1423 else if (null == elements || 0 == elements.Count)
1424 msg = $"No elements found for selector {selector.Name}. Continuing.";
1425
1426 GPAL.PublishSimpleEvent(GPALEventType.INFO, msg, application, GPALObjectType.Application);
1427 }
1436 public static List<GPALAutomationElement> WaitFor(Application application, int timeoutInTicks, out bool matchedAll)
1437 {
1438 bool matchedAll2 = true;
1439 List<GPALAutomationElement> foundElements = null;
1440 List<GPALAutomationElement> tmpFoundElements = null;
1441 List<GPALAutomationElement> matchedElements;
1442
1443 foundElements = new List<GPALAutomationElement>();
1444
1445 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
1446 {
1447 if (SelectorType.Selector != selector.SelectorType)
1448 continue; // we don't look up data literals
1449
1450 tmpFoundElements = WaitFor(application, selector, timeoutInTicks, out matchedAll2, out matchedElements);
1451
1452 tmpFoundElements = matchedElements;
1453
1454 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
1455 if (true == selector.DeleteMe || null == tmpFoundElements)
1456 {
1457 PublishToEventHandler(application, selector, tmpFoundElements);
1458 continue;
1459 }
1460
1461 int rowCount = 0;
1462
1463 foreach (GPALAutomationElement element in tmpFoundElements)
1464 {
1465 foundElements.Add(element);
1466 if (++rowCount >= application.CurrentUOW.WithAllThatMatch)
1467 break;
1468 }
1469 }
1470
1471 matchedAll = matchedAll2;
1472 return foundElements;
1473 }
1474
1485 private static List<GPALAutomationElement> WaitFor(Application application, Selector selector, int timeoutInTicks, out bool matchedAll, out List<GPALAutomationElement> matchedElements, AutomationElement element = null)
1486 {
1487 bool matchedAll2 = true, matchedAll3 = true;
1488 List<GPALAutomationElement> matchedElements2;
1489 ReadOnlyCollection<GPALAutomationElement> elements = FindElements(application, application.CurrentUOW, selector, out matchedAll2, out matchedElements2, element);
1490
1491 if (WaitTime.Forever == timeoutInTicks)
1492 timeoutInTicks = Int32.MaxValue; // forever
1493
1494 // keep looking while we have time left and nothing has turned up. FindElements returns an empty
1495 // collection (never null) when it finds nothing, so this has to test the count, not the reference.
1496 for (; 0 < timeoutInTicks && 0 == (elements?.Count ?? 0); timeoutInTicks -= 1000)
1497 {
1498 elements = FindElements(application, application.CurrentUOW, selector, out matchedAll3, out matchedElements2, element);
1499 if (0 == elements.Count)
1500 Thread.Sleep(Math.Min(1000, timeoutInTicks));
1501 else
1502 break;
1503 }
1504
1505 matchedAll = (matchedAll2 | matchedAll3); // one of those found it or not :)
1506 matchedElements = matchedElements2; // always use matched, it's either all or those matched
1507
1508 return elements?.ToList<GPALAutomationElement>();
1509 }
1510
1518 public static void FillInFrom(Application application, string useText, WriteMode writeMode)
1519 {
1520 List<GPALAutomationElement> matchedElements;
1521
1522 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
1523 {
1524 if (SelectorType.Selector != selector.SelectorType)
1525 continue; // we don't look up data literals
1526
1527 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out matchedElements);
1528
1529 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
1530 if (true == selector.DeleteMe || null == elems)
1531 {
1532 PublishToEventHandler(application, selector, elems?.ToList());
1533 continue;
1534 }
1535
1536 if (null != matchedElements)
1537 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
1538
1539 int rowCount = 0;
1540
1541 foreach (GPALAutomationElement elem in elems)
1542 {
1543 if (null != elem.Ae)
1544 {
1545 bool isPassword = elem.Ae.Current.IsPassword;
1546
1547 // an empty string is a workflow clearing the field and a one or two character string has
1548 // no middle to leave out, so those are said in full. Indexing unconditionally threw
1549 // IndexOutOfRange on the empty string, which made clearing a field a crash
1550 string abbreviatedText = 2 < (useText?.Length ?? 0)
1551 ? useText[0] + "..." + useText[useText.Length - 1]
1552 : useText;
1553
1554 if (false == isPassword)
1555 {
1556 if (GPALEventType.DEBUG == (GPALEventType.DEBUG & GPAL.GPALSettings.DebugEvents) || GPALEventType.DEBUG == (GPALEventType.DEBUG & GPAL.GPALSettings.ConsoleEvents))
1557 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{writeMode}] text [{useText}] in element [{elem.Ae.Current.LocalizedControlType}][{selector.Name}]", application, GPALObjectType.Application);
1558 else
1559 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{writeMode}] text [{abbreviatedText}] in element [{elem.Ae.Current.LocalizedControlType}][{selector.Name}]", application, GPALObjectType.Application);
1560 }
1561 else
1562 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{writeMode}] XXXpasswordXXX in element [{elem.Ae.Current.LocalizedControlType}][{selector.Name}]", application, GPALObjectType.Application);
1563
1564 FillInFrom(application, elem.Ae, new UnitOfWork.ElementNode() { OffsetX = selector.OffsetX, OffsetY = selector.OffsetY }, useText, writeMode);
1565 if (++rowCount >= HowManyToActOn(application))
1566 break;
1567 }
1568 }
1569
1570 UnitOfWork safeUOW = application.CurrentUOW;
1571 CallIfStatus handled = 0;
1572
1573 // CAVEAT: there is no concept of 'handled (1)' vs 'not handled (0)' but definitely can request to exit
1574 IGPALGrid<string> tokens = GPAL.Grid.ToGPALObject();
1575 tokens.AddRow(new List<string> { useText });
1576 if (null != application.CurrentUOW.AppCallAfterFillIn)
1577 handled = application.CurrentUOW.AppCallAfterFillIn(application, tokens, 1);
1578
1579 // CAVEAT: kludge - the call after handler can and probably will set a new current unit of work, but we need our old current unit
1580 // the call after handler UOW is no longer in scope, so restore our UOW
1581 application.CurrentUOW = safeUOW;
1582
1583 if (CallIfStatus.Terminate == handled)
1584 {
1585 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"CallAfterFillIn handler [{application.CurrentUOW.AppCallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.", application, GPALObjectType.Application);
1586 throw new GPALException($"CallAfterFillIn handler [{application.CurrentUOW.AppCallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.");
1587 }
1588 }
1589 }
1597 internal static void Scroll(Application application, object amount, ScrollTypes incrementsPercent, ScrollTypes horzVert)
1598 {
1599 List<GPALAutomationElement> matchedElements;
1600
1601 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
1602 {
1603 if (SelectorType.Selector != selector.SelectorType)
1604 continue; // we don't look up data literals
1605
1606 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out matchedElements);
1607
1608 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
1609 if (true == selector.DeleteMe || null == elems)
1610 {
1611 PublishToEventHandler(application, selector, elems?.ToList());
1612 continue;
1613 }
1614
1615 if (null != matchedElements)
1616 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
1617
1618 if (0 == elems.Count)
1619 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selector [{selector.Name}] matched no elements, so there was nothing to scroll.", application, GPALObjectType.Application);
1620
1621 int rowCount = 0;
1622
1623 foreach (GPALAutomationElement elem in elems)
1624 {
1625 if (null != elem.Ae)
1626 {
1627 Scroll(application, elem.Ae, selector.InteractionType, amount, incrementsPercent, horzVert);
1628 if (++rowCount >= HowManyToActOn(application))
1629 break;
1630 }
1631 }
1632 }
1633 }
1657 private static void SetRangeValue(Application application, RangeValuePattern rangeValue, AutomationElement ae, object amount, ScrollTypes incrementsPercent)
1658 {
1659 var now = rangeValue.Current;
1660
1661 if (true == now.IsReadOnly)
1662 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{ae.Current.ControlType.LocalizedControlType}] reports its range as read only, so it cannot be scrolled.", application, GPALObjectType.Application);
1663 else
1664 {
1665 double span = now.Maximum - now.Minimum;
1666 double wanted;
1667
1668 if (ScrollTypes.Percent == incrementsPercent)
1669 wanted = now.Minimum + (((double)(int)amount / 100.0) * span);
1670 else
1671 {
1672 // the enum names the size and the direction; the control names how far each size goes. A
1673 // control that does not say is documented to answer NaN, so a hundredth and a tenth of its
1674 // own range stand in, which is what those steps mean everywhere else
1675 ScrollAmount step = (ScrollAmount)amount;
1676
1677 bool large = ScrollAmount.LargeIncrement == step || ScrollAmount.LargeDecrement == step;
1678 bool back = ScrollAmount.SmallDecrement == step || ScrollAmount.LargeDecrement == step;
1679
1680 double by = large ? now.LargeChange : now.SmallChange;
1681
1682 if (double.IsNaN(by) || 0 == by)
1683 by = span * (large ? 0.1 : 0.01);
1684
1685 wanted = now.Value + (back ? -by : by);
1686 }
1687
1688 // a value off the end of its own range is refused by the control, and the endpoint is what was
1689 // being asked for anyway
1690 wanted = wanted < now.Minimum ? now.Minimum : (wanted > now.Maximum ? now.Maximum : wanted);
1691
1692 try
1693 {
1694 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"RangeValuePattern on [{ae.Current.ControlType.LocalizedControlType}]: range [{now.Minimum}] to [{now.Maximum}], value [{now.Value}], setting [{wanted}].", application, GPALObjectType.Application);
1695
1696 rangeValue.SetValue(wanted);
1697
1698 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"RangeValuePattern on [{ae.Current.ControlType.LocalizedControlType}]: value is now [{rangeValue.Current.Value}].", application, GPALObjectType.Application);
1699 }
1700 catch (Exception ex)
1701 {
1702 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to set a value of [{wanted}] on control [{ae.Current.ControlType.LocalizedControlType}].", application, GPALObjectType.Application, ex);
1703 }
1704 }
1705 }
1706
1721 private static void ScrollToPercent(Application application, AutomationElement ae, ScrollPattern scrollPattern, int percent, ScrollTypes horzVert)
1722 {
1723 bool vertical = ScrollTypes.Vertical == horzVert;
1724 var info = scrollPattern.Current;
1725
1726 if (false == (vertical ? info.VerticallyScrollable : info.HorizontallyScrollable))
1727 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{ae.Current.ControlType.LocalizedControlType}] is not scrollable {(vertical ? "vertically" : "horizontally")}.", application, GPALObjectType.Application);
1728 else
1729 {
1730 double page = vertical ? info.VerticalViewSize : info.HorizontalViewSize;
1731 double at = vertical ? info.VerticalScrollPercent : info.HorizontalScrollPercent;
1732 double was = -1;
1733 int steps = 0;
1734
1735 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"ScrollPattern on [{ae.Current.ControlType.LocalizedControlType}]: at [{at}], one view is [{page}] percent, walking to [{percent}].", application, GPALObjectType.Application);
1736
1737 // whole views first
1738 while (page <= Math.Abs(percent - at) && at != was && steps < STEP_LIMIT)
1739 {
1740 was = at;
1741 at = OneStep(scrollPattern, vertical, percent > at, true);
1742 steps++;
1743 }
1744
1745 double off = Math.Abs(percent - at);
1746 was = -1;
1747
1748 // then lines, stopping the moment a step no longer gets closer
1749 while (0 < off && at != was && steps < STEP_LIMIT)
1750 {
1751 was = at;
1752 at = OneStep(scrollPattern, vertical, percent > at, false);
1753 steps++;
1754
1755 double closer = Math.Abs(percent - at);
1756
1757 if (closer >= off)
1758 break;
1759
1760 off = closer;
1761 }
1762
1763 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"ScrollPattern on [{ae.Current.ControlType.LocalizedControlType}]: landed at [{at}] after [{steps}] step(s).", application, GPALObjectType.Application);
1764 }
1765 }
1766
1773 private static double OneStep(ScrollPattern scrollPattern, bool vertical, bool forward, bool large)
1774 {
1775 ScrollAmount step = large
1776 ? (forward ? ScrollAmount.LargeIncrement : ScrollAmount.LargeDecrement)
1777 : (forward ? ScrollAmount.SmallIncrement : ScrollAmount.SmallDecrement);
1778
1779 scrollPattern.Scroll(vertical ? ScrollAmount.NoAmount : step, vertical ? step : ScrollAmount.NoAmount);
1780
1781 return vertical ? scrollPattern.Current.VerticalScrollPercent : scrollPattern.Current.HorizontalScrollPercent;
1782 }
1783
1785 private const int STEP_LIMIT = 500;
1786
1802 internal static bool HardwareReaches(Application application)
1803 {
1804 bool retVal = true;
1805
1806 if (true == application.applicationSettings.HiddenDesktop)
1807 {
1808 // once per application. A workflow driving a hidden desktop turns hardware down on nearly every
1809 // action, and saying so each time buries the log
1810 if (false == application.applicationSettings.HiddenDesktopHardwareReported)
1811 {
1812 application.applicationSettings.HiddenDesktopHardwareReported = true;
1813
1814 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Hardware input cannot reach [{application.applicationSettings.HiddenDesktopName}] while it is hidden, so UIAutomation is used instead. Peek at the desktop to use hardware there.", application, GPALObjectType.Application);
1815 }
1816
1817 retVal = false;
1818 }
1819
1820 return retVal;
1821 }
1822
1823 internal static void Scroll(Application application, AutomationElement ae, InteractionType interactionType, object amount, ScrollTypes incrementsPercent, ScrollTypes horzVert)
1824 {
1825 //UIAutomation only
1826 if ((Enums.InteractionType.Hardware == interactionType || true == application.applicationSettings.UseHardware) && true == HardwareReaches(application))
1827 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Hardware scrolling is not supported at this time. Find the scroll controls and click them instead.", application, GPALObjectType.Application);
1828 else
1829 {
1830 // the element is a scrollbar, so it only goes one direction horizontal or vertical
1831 if (true == ae.TryGetCurrentPattern(RangeValuePattern.Pattern, out object rangeValuePattern))
1832 {
1833 SetRangeValue(application, (RangeValuePattern)rangeValuePattern, ae, amount, incrementsPercent);
1834 }
1835 else if (true == ae.TryGetCurrentPattern(ScrollPattern.Pattern, out object scrollPattern))
1836 {
1837 if (ScrollTypes.Percent == incrementsPercent)
1838 ScrollToPercent(application, ae, (ScrollPattern)scrollPattern, (int)amount, horzVert);
1839 else
1840 {
1841 ScrollAmount hAmount = ScrollTypes.Horizontal == horzVert ? (ScrollAmount)amount : ScrollAmount.NoAmount;
1842 ScrollAmount vAmount = ScrollTypes.Vertical == horzVert ? (ScrollAmount)amount : ScrollAmount.NoAmount;
1843
1844 try
1845 {
1846 var before = ((ScrollPattern)scrollPattern).Current;
1847
1848 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"ScrollPattern on [{ae.Current.ControlType.LocalizedControlType}]: scrollable h [{before.HorizontallyScrollable}] v [{before.VerticallyScrollable}], at h [{before.HorizontalScrollPercent}] v [{before.VerticalScrollPercent}], applying h [{hAmount}] v [{vAmount}].", application, GPALObjectType.Application);
1849
1850 ((ScrollPattern)scrollPattern).Scroll(hAmount, vAmount);
1851
1852 var after = ((ScrollPattern)scrollPattern).Current;
1853
1854 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"ScrollPattern on [{ae.Current.ControlType.LocalizedControlType}]: now at h [{after.HorizontalScrollPercent}] v [{after.VerticalScrollPercent}].", application, GPALObjectType.Application);
1855 }
1856 catch (Exception ex)
1857 {
1858 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to Scroll on control [{ae.Current.ControlType.LocalizedControlType}].", application, GPALObjectType.Application, ex); ;
1859 }
1860 }
1861 }
1862 else
1863 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{ae.Current.ControlType.LocalizedControlType}] offers neither RangeValuePattern nor ScrollPattern, so UIAutomation cannot scroll it. Point the selector at its ScrollBar, which does.", application, GPALObjectType.Application);
1864
1865 }
1866 }
1876 internal static void FillInFrom(Application application, AutomationElement ae, UnitOfWork.ElementNode elementNode, string useText, WriteMode writeMode)
1877 {
1878 bool taken = false;
1879
1880 if ((Enums.InteractionType.Hardware == elementNode.InteractionType || true == application.applicationSettings.UseHardware) && true == HardwareReaches(application))
1881 {
1882 HardwareFillInFrom(application, new GPALAutomationElement(ae), elementNode.OffsetX, elementNode.OffsetY, useText, writeMode);
1883
1884 taken = true;
1885 }
1886
1887 if (false == taken)
1888 {
1889 // LegacyIAccessiblePattern is only accessible via the UIAComWrapperX nuget package
1890 /*
1891 if (true == ae.TryGetCurrentPattern(System.Windows.Automation.LegacyIAccessiblePattern.Pattern, out object legacyPattern))
1892 {
1893 // Set focus for input functionality and begin.
1894 try
1895 {
1896 ae.SetFocus();
1897 }
1898 catch (Exception ex)
1899 {
1900 GPAL.GPALEventArgs args = new GPAL.GPALEventArgs()
1901 {
1902 GPALObject = application,
1903 GPALObjectType = GPALObjectType.Application,
1904 CurrentUOW = application.CurrentUOW,
1905 AutomationElement = ae,
1906 Message = $"Unable to set focus on Control {ae.Current.ControlType.LocalizedControlType}.",
1907 ExceptionRaised = ex,
1908 ScreenShot = ImageHelper.CaptureScreen(),
1909 DateTimeStamp = DateTime.Now
1910 };
1911
1912 GPAL.ExceptionHandler?.Invoke(application, args);
1913 }
1914 if (WriteMode.Overwrite == writeMode)
1915 ((LegacyIAccessiblePattern)legacyPattern).SetValue(useText);
1916 else if (WriteMode.Append == writeMode)
1917 {
1918 string content = ((LegacyIAccessiblePattern)legacyPattern).Current.Value;
1919 content += useText;
1920 ((LegacyIAccessiblePattern)legacyPattern).SetValue(content);
1921 }
1922 else if (WriteMode.Insert == writeMode)
1923 {
1924 string content = ((LegacyIAccessiblePattern)legacyPattern).Current.Value;
1925 useText += content;
1926 ((LegacyIAccessiblePattern)legacyPattern).SetValue(content);
1927 }
1928 }
1929 else
1930 */
1931 // a control offering a value does not have to let it be written. A read only value answers the
1932 // offer the same way and throws when it is taken up, which a multi line edit does, so the write
1933 // is attempted rather than trusted and what follows is the next way in
1934 if (true == ae.TryGetCurrentPattern(ValuePattern.Pattern, out object valuePattern))
1935 {
1936 // Set focus for input functionality and begin.
1937 try
1938 {
1939 ae.SetFocus();
1940 }
1941 catch (Exception)
1942 {
1943 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to set focus on control [{ae.Current.ControlType.LocalizedControlType}], so the value is written to it without the focus.", application, GPALObjectType.Application);
1944 }
1945
1946 try
1947 {
1948 string content = WriteMode.Overwrite == writeMode ? string.Empty : ((ValuePattern)valuePattern).Current.Value;
1949
1950 ((ValuePattern)valuePattern).SetValue(WriteMode.Append == writeMode ? content + useText
1951 : WriteMode.Insert == writeMode ? useText + content
1952 : useText);
1953
1954 taken = true;
1955 }
1956 catch (Exception ex)
1957 {
1958 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"ValuePattern on [{ae.Current.ControlType.ProgrammaticName}] would not take the text, so the next way of writing it is tried.", application, GPALObjectType.Application, ex);
1959 }
1960 }
1961
1962 // and a control with no value to write, or one that would not take what it was given, is still
1963 // a window, and a window holds its text whether or not it publishes one
1964 if (false == taken)
1965 taken = SetNativeText(application, ae, useText, writeMode);
1966
1967 if (false == taken)
1968 {
1969 // typing is recovery here rather than what the workflow asked for, so it is what
1970 // NoFallbackRecoveryActions turns off
1971 if (false == GPAL.NoFallbackRecoveryActions && true == HardwareReaches(application))
1972 {
1973 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{ae.Current.ControlType.ProgrammaticName}] offers no way to write text into it, so it is typed into instead.", application, GPALObjectType.Application);
1974
1975 HardwareFillInFrom(application, new GPALAutomationElement(ae), elementNode.OffsetX, elementNode.OffsetY, useText, writeMode);
1976 }
1977 else
1978 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{ae.Current.ControlType.ProgrammaticName}] offers no way to write text into it, so it was left as it was.", application, GPALObjectType.Application);
1979 }
1980 }
1981 }
1992 public static void HardwareFillInFrom(Application application, GPALAutomationElement element, int offsetX, int offsetY, string text, WriteMode writeMode)
1993 {
1994 ApplicationHelper.TopProcess(application.Process);
1995
1996 HardwareFocus(application, element, offsetX, offsetY);
1997
1998 if (WriteMode.Overwrite == writeMode)
1999 {
2000 // clear the input, send CTRL-a (select all), then delete *altho we can prolly just typeover*
2001 HardwareHelper.SendChar('a', ModifierKeys.Control, true);
2002 Thread.Sleep(150);
2003 // HardwareHelper.SendKey(GPAL.VK_DELETE);
2004 }
2005 else if (WriteMode.Append == writeMode) // CTRL-End goto end of document
2006 {
2007 HardwareHelper.SendKey(GPAL.VK_END, ModifierKeys.Control);
2008 Thread.Sleep(150);
2009 }
2010 else if (WriteMode.Insert == writeMode) // CTRL-End goto start of document
2011 {
2012 HardwareHelper.SendKey(GPAL.VK_HOME, ModifierKeys.Control);
2013 Thread.Sleep(150);
2014 }
2015
2016 HardwareHelper.SendString(text, GPAL.TypingDelay);
2017
2018 SettleInput(application);
2019 }
2020
2035 internal static void SettleInput(Application application)
2036 {
2037 try
2038 {
2039 application?.Process?.WaitForInputIdle(INPUT_SETTLE_MS);
2040 }
2041 catch (Exception)
2042 {
2043 // a process with no message loop has no input queue to empty, and one that has gone has no
2044 // input at all. Asking is how that is found out, and neither is a failure
2045 }
2046 }
2047
2048 // how long to wait for an application to take what was sent it before carrying on regardless. Reached
2049 // only by a process that never reports itself idle, never by one that simply took the keystrokes
2050 private const int INPUT_SETTLE_MS = 2_000;
2051
2057 public static void Focus(Application application)
2058 {
2059 List<GPALAutomationElement> matchedElements;
2060
2061 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
2062 {
2063 if (SelectorType.Selector != selector.SelectorType)
2064 continue; // we don't look up data literals
2065
2066 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2067
2068 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2069 if (true == selector.DeleteMe || null == elems)
2070 {
2071 PublishToEventHandler(application, selector, elems?.ToList());
2072 continue;
2073 }
2074
2075 if (null != matchedElements)
2076 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
2077
2078 int rowCount = 0;
2079
2080 foreach (GPALAutomationElement elem in elems)
2081 {
2082 if (null != elem.Ae)
2083 {
2084 if ((Enums.InteractionType.Hardware == selector.InteractionType || true == application.applicationSettings.UseHardware) && true == HardwareReaches(application))
2085 {
2086 int offsetX, offsetY;
2087 offsetX = selector.SelectorSettings.OffsetX;
2088 offsetY = selector.SelectorSettings.OffsetY;
2089
2090 HardwareFocus(application, elem, offsetX, offsetY); // hardwre focus will try to click in text inputs
2091 }
2092 else
2093 {
2094
2095 try
2096 {
2097 if (null != elem.Ae)
2098 elem.Ae.SetFocus();
2099 else
2100 HardwareFocus(application, elem, 0, 0);
2101 }
2102 catch (Exception ex)
2103 {
2104 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Focus threw on selector [{selector.Name}], so the focus did not move.", application, GPALObjectType.Application, ex);
2105 }
2106 }
2107
2108 if (++rowCount >= HowManyToActOn(application))
2109 break;
2110 }
2111 }
2112 }
2113 }
2121 public static void Hover(Application application, UnitOfWork currentUOW)
2122 {
2123 List<GPALAutomationElement> matchedElements;
2124
2125 foreach (Selector selector in currentUOW.WithSelectorList)
2126 {
2127 if (SelectorType.Selector != selector.SelectorType)
2128 continue; // we don't look up data literals
2129
2130 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, currentUOW, selector, out bool matchedAll, out matchedElements);
2131
2132 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2133 if (true == selector.DeleteMe || null == elems)
2134 {
2135 PublishToEventHandler(application, selector, elems?.ToList());
2136 continue;
2137 }
2138
2139 if (null != matchedElements)
2140 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
2141
2142 int rowCount = 0;
2143
2144 foreach (GPALAutomationElement elem in elems)
2145 {
2146 if (null != elem.Ae)
2147 {
2148 if (0 == elem.Ae.Current.BoundingRectangle.Width && 0 == elem.Ae.Current.BoundingRectangle.Height)
2149 {
2150 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Element has Zero Width and Height, skipping", application, GPALObjectType.Application);
2151 continue;
2152 }
2153
2154 // both ways of moving to an element are the mouse, because a hover is a mouse position and
2155 // there is no pattern for one. So a hidden desktop cannot be hovered over at all, and
2156 // saying so beats moving the real pointer across the user's screen
2157 if (false == HardwareReaches(application))
2158 {
2159 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selector [{selector.Name}] cannot be moved to on a hidden desktop, since a hover is a mouse position and UIAutomation has no equivalent. Peek at the desktop to hover, or click, which does work there.", application, GPALObjectType.Application);
2160 }
2161 else if (Enums.InteractionType.Hardware == selector.InteractionType || true == application.applicationSettings.UseHardware)
2162 {
2163 // same as hardware moveto
2164 HardwareMoveTo(application, elem.ImageMatch ?? elem, selector.OffsetX, selector.OffsetY); // an image match moves to where the picture is
2165 }
2166 else if (0 != elem.Ae.Current.BoundingRectangle.Width && 0 != elem.Ae.Current.BoundingRectangle.Height)
2167 {
2168 try
2169 {
2170 elem.Ae.SetFocus();
2171 HardwareMoveTo(application, elem.ImageMatch ?? elem, selector.OffsetX, selector.OffsetY); // an image match moves to where the picture is
2172 }
2173 catch (Exception)
2174 {
2175 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Focusing [{selector.Name}] threw, so it is moved to without the focus.", application, GPALObjectType.Application);
2176 HardwareMoveTo(application, elem.ImageMatch ?? elem, selector.OffsetX, selector.OffsetY); // an image match moves to where the picture is
2177 }
2178 }
2179
2180 if (++rowCount >= HowManyToActOn(application))
2181 break;
2182 }
2183 }
2184 }
2185 }
2191 public static void MoveTo(Application application)
2192 {
2193 List<GPALAutomationElement> matchedElements;
2194
2195 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
2196 {
2197 if (SelectorType.Selector != selector.SelectorType)
2198 continue; // we don't look up data literals
2199
2200 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2201
2202 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2203 if (true == selector.DeleteMe || null == elems)
2204 {
2205 PublishToEventHandler(application, selector, elems?.ToList());
2206 continue;
2207 }
2208
2209 if (null != matchedElements)
2210 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
2211
2212 int rowCount = 0;
2213
2214 // moving to an element is the mouse and nothing else, since UIAutomation has no way to put a
2215 // pointer anywhere, so this is one of the two things a hidden desktop cannot be asked for
2216 if (false == HardwareReaches(application))
2217 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selector [{selector.Name}] cannot be moved to on a hidden desktop, since moving the pointer is the mouse and UIAutomation has no equivalent. Peek at the desktop to move there, or click, which does work.", application, GPALObjectType.Application);
2218 else
2219 foreach (GPALAutomationElement elem in elems)
2220 {
2221 if (null != elem.Gae || null != elem.Ae)
2222 HardwareMoveTo(application, elem.ImageMatch ?? elem, selector.OffsetX, selector.OffsetY); // an image match moves to where the picture is
2223
2224 if (++rowCount >= HowManyToActOn(application))
2225 break;
2226 }
2227 }
2228 }
2229
2244 internal static string WhatIsThere(AutomationElement parent)
2245 {
2246 var names = new List<string>();
2247 string retVal;
2248
2249 try
2250 {
2251 AutomationElement at = parent ?? AutomationElement.RootElement;
2252
2253 foreach (AutomationElement child in at.FindAll(TreeScope.Children, System.Windows.Automation.Condition.TrueCondition))
2254 {
2255 string controlType = child.Current.ControlType.ProgrammaticName.Replace("ControlType.", "");
2256 string automationId = child.Current.AutomationId;
2257
2258 string className = child.Current.ClassName;
2259
2260 names.Add(string.IsNullOrEmpty(automationId)
2261 ? $"{controlType}[@ClassName='{className}'] '{child.Current.Name}'"
2262 : $"{controlType}[@AutomationId='{automationId}'][@ClassName='{className}'] '{child.Current.Name}'");
2263 }
2264
2265 retVal = 0 == names.Count
2266 ? "It has no children."
2267 : $"What is there: {string.Join(" | ", names)}";
2268 }
2269 catch (Exception ex)
2270 {
2271 retVal = $"Its children could not be read: {ex.Message}";
2272 }
2273
2274 return retVal;
2275 }
2276
2286 private static void MoveTo(AutomationElement element, int offsetX, int offsetY, string appName)
2287 {
2288 try
2289 {
2290 HardwareMoveTo(null, new GPALAutomationElement(element), offsetX, offsetY);
2291 }
2292 catch (Exception ex)
2293 {
2294 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"(ElementHelper): unable to move the mouse to the element in Application [{appName}].", element, GPALObjectType.Other, ex);
2295 }
2296 }
2304 private static void HardwareFocus(Application application, GPALAutomationElement element, int OffsetX, int OffsetY)
2305 {
2306 if (null != element.Ae && ControlType.Text == element.Ae.Current.ControlType)
2307 HardwareClick(application, element, ClickType.LeftClick, ModifierKeys.NONE, OffsetX, OffsetY);
2308 else
2309 HardwareMoveTo(application, element, OffsetX, OffsetY);
2310 }
2318 private static System.Drawing.Point HardwareMoveTo(Application application, GPALAutomationElement element, int OffsetX, int OffsetY)
2319 {
2320 Rectangle rect2 = new Rectangle(), iframeRect = new Rectangle(0, 0, 0, 0);
2321
2322 // the size as well as the corner. Without it the rectangle is a point, the middle of it is the
2323 // corner, and every aim inside the element lands on its top left border
2324 if (null != element.Gae)
2325 {
2326 rect2.Location = element.Gae.Location;
2327 rect2.Size = element.Gae.Size;
2328 }
2329 else
2330 {
2331 rect2.Location = new System.Drawing.Point((int) element.Ae.Current.BoundingRectangle.Location.X, (int) element.Ae.Current.BoundingRectangle.Location.Y);
2332 rect2.Size = new System.Drawing.Size((int) element.Ae.Current.BoundingRectangle.Width, (int) element.Ae.Current.BoundingRectangle.Height);
2333
2334 // TODO: does this mean anything in an application?
2335 //if (true == application.CurrentUOW.CurrentSelector?.SearchForSelector)
2336 //{
2337 // // TODO: CAVEAT: is this good? we have to try to scroll the element on screen, but what happens if it can't find it?
2338 // if (false == IsVisibleInViewport(element))
2339 // {
2340 // SendKey(GPAL.VK_HOME); // home - go to document top, then scroll down
2341
2342 // while (false == IsVisibleInViewport(element) && false == IsEndOfPage(element))
2343 // {
2344 // SendKey(GPAL.VK_NEXT); // pagedown
2345 // Thread.Sleep(125);
2346 // }
2347 // rect2 = GetAbsCoordinates(application.BrowserSettings.BrowserType, element);
2348 // }
2349 //}
2350 }
2351
2352 // where inside the element to aim. A zero offset means the middle, give or take, the same as it
2353 // does on the browser side: the corner of a control is its border, and on a list row it is the
2354 // blank margin beside the icon. The jitter keeps repeated clicks off one pixel without wandering
2355 // out of the control
2356 int offsetX = OffsetX;
2357 int offsetY = OffsetY;
2358
2359 if (0 == offsetX && 0 == offsetY)
2360 {
2361 int spreadX = Math.Max(0, Math.Min(2, (rect2.Width / 2) - 1));
2362 int spreadY = Math.Max(0, Math.Min(2, (rect2.Height / 2) - 1));
2363
2364 offsetX = rect2.Width / 2 + Jitter.Next(-spreadX, spreadX + 1);
2365 offsetY = rect2.Height / 2 + Jitter.Next(-spreadY, spreadY + 1);
2366 }
2367
2368 System.Drawing.Point aimedAt = new System.Drawing.Point(rect2.X + offsetX, rect2.Y + offsetY);
2369
2370 // a control with no size gives nothing to aim inside, and its corner is as likely to be on the
2371 // thing beside it. UIAutomation's own answer for where a control can be clicked is the fallback,
2372 // and it is only a fallback: taken ahead of the rectangle it puts the pointer somewhere other than
2373 // where the workflow's offset asked for
2374 if ((0 == rect2.Width || 0 == rect2.Height)
2375 && null != element.Ae
2376 && true == element.Ae.TryGetClickablePoint(out System.Windows.Point clickablePoint))
2377 aimedAt = new System.Drawing.Point((int)clickablePoint.X, (int)clickablePoint.Y);
2378
2379 if (true == GPAL.SimulateMouse || true == application.CurrentUOW?.CurrentSelector?.SimulateMouse)
2380 HardwareHelper.MoveMouse(aimedAt.X, aimedAt.Y, 10, 10); // source didn't define values for rx, ry
2381 else
2382 HardwareHelper.MoveMouse(aimedAt.X, aimedAt.Y);
2383
2384 // the point the pointer is now on, so a caller clicks where it moved rather than working the
2385 // location out a second time and landing somewhere else
2386 return aimedAt;
2387 }
2388
2389 private static Random _jitter;
2390 private static Random Jitter => _jitter ?? (_jitter = new Random(Guid.NewGuid().GetHashCode()));
2396 public static void LeftClick(Application application, ModifierKeys modifierKeys = ModifierKeys.NONE)
2397 {
2398 List<GPALAutomationElement> matchedElements;
2399
2400 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
2401 {
2402 if (SelectorType.Selector != selector.SelectorType)
2403 continue; // we don't look up data literals
2404
2405 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2406
2407 // FindElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2408 if (true == selector.DeleteMe || null == elems)
2409 {
2410 PublishToEventHandler(application, selector, elems?.ToList());
2411 continue;
2412 }
2413
2414 if (null != matchedElements)
2415 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements); // either All or just those that matched
2416
2417 int rowCount = 0;
2418
2419 foreach (GPALAutomationElement elem in elems)
2420 {
2421 Click(application, selector, elem, ClickType.LeftClick, modifierKeys);
2422
2423 if (++rowCount >= HowManyToActOn(application))
2424 break;
2425 }
2426 }
2427 }
2434 public static bool GenericClick(Application application, ClickType clickType)
2435 {
2436 List<GPALAutomationElement> matchedElements;
2437 bool matchedAll = false;
2438 bool clickPerformed = false;
2439
2440 foreach (Selector sel in application.CurrentUOW.WithSelectorList)
2441 {
2442 if (SelectorType.Selector != sel.SelectorType)
2443 continue; // we don't look up data literals
2444
2445 ElementHelper.FindElements(application, application.CurrentUOW, sel, out matchedAll, out matchedElements);
2446
2447 if (0 == (matchedElements?.Count ?? 0))
2448 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selector [{sel.Name}] matched no elements, so there was nothing to [{clickType}].", application, GPALObjectType.Application);
2449
2450 if (null != matchedElements)
2451 {
2452 int rowCount = 0;
2453
2454 foreach (GPALAutomationElement elem in matchedElements)
2455 {
2456 Click(application, sel, elem, clickType, ModifierKeys.NONE);
2457 clickPerformed = true;
2458
2459 if (++rowCount >= HowManyToActOn(application))
2460 break;
2461 }
2462 }
2463 }
2464 return clickPerformed;
2465 }
2475 public static void Click(Application application, Selector selector, GPALAutomationElement element, ClickType clickType, ModifierKeys modifierKeys)
2476 {
2477 int offsetX, offsetY;
2478
2479 // a control found under an image is clicked where the picture is, not in its own middle, which on a
2480 // large control is somewhere else entirely. on a desktop a point is clicked with the mouse
2481 if (null != element.ImageMatch)
2482 element = element.ImageMatch;
2483 offsetX = selector.SelectorSettings.OffsetX;
2484 offsetY = selector.SelectorSettings.OffsetY;
2485
2486 // an item reached through the window's own menu is clicked by sending the command it sends, which
2487 // is what the menu would have sent had anyone been able to open it
2488 if (0 != element.MenuCommand)
2489 {
2490 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Selector [{selector.Name}] is a menu item, so command [{element.MenuCommand}] was sent to the window it belongs to.", application, GPALObjectType.Application);
2491
2492 PostMessage(element.MenuWindow, WM_COMMAND, new IntPtr(element.MenuCommand), IntPtr.Zero);
2493 }
2494 // a right click is the mouse or nothing: UIAutomation has no pattern for a context menu, so it is
2495 // the one click a hidden desktop cannot be given
2496 else if (ClickType.RightClick == clickType && false == HardwareReaches(application))
2497 {
2498 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selector [{selector.Name}] cannot be right clicked on a hidden desktop, since UIAutomation offers no pattern for one. Peek at the desktop to right click there.", application, GPALObjectType.Application);
2499 }
2500 else if ((Enums.InteractionType.Hardware == selector.InteractionType || null != element.Gae || true == application.applicationSettings.UseHardware || ClickType.RightClick == clickType)
2501 && true == HardwareReaches(application))
2502 HardwareClick(application, element, clickType, modifierKeys, offsetX, offsetY);
2503 else if (null != element.Ae)
2504 {
2505 UIAutomationClick(application, selector, element, clickType, modifierKeys);
2506 }
2507 // an image match is a place on the screen with no control behind it, so the mouse is its only way in,
2508 // and on a hidden desktop the mouse cannot get there
2509 else
2510 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selector [{selector.Name}] is an image match, which only the mouse can click, and the mouse cannot reach [{application.applicationSettings.HiddenDesktopName}]. Peek at the desktop to click there.", application, GPALObjectType.Application);
2511 }
2512
2535 public static void UIAutomationClick(Application application, Selector selector, GPALAutomationElement element, ClickType clickType, ModifierKeys modifierKeys)
2536 {
2537 object objPattern;
2538 bool invoked = false;
2539 string control = element.Ae.Current.LocalizedControlType;
2540
2541 if (ModifierKeys.NONE != modifierKeys)
2542 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"A click holding [{Browser.Browser.GetModifierKeys(modifierKeys)}] has no UIAutomation equivalent, so [{control}] is clicked with the mouse.", application, GPALObjectType.Application);
2543 else if (ClickType.LeftClick == clickType && true == element.Ae.TryGetCurrentPattern(SelectionItemPattern.Pattern, out objPattern))
2544 {
2545 // a left click on a selectable item selects it. Invoke would be its default action, which for
2546 // an item is what a double click does, so a single click must not go there
2547 try
2548 {
2549 ((SelectionItemPattern)objPattern).Select();
2550 invoked = true;
2551
2552 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Element [{NameOf(element)}] [{clickType}] clicked through SelectionItemPattern.", application, GPALObjectType.Application);
2553 }
2554 catch (Exception)
2555 {
2556 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"SelectionItemPattern threw on control [{control}], falling back to the mouse.", application, GPALObjectType.Application);
2557 }
2558 }
2559 else if ((ClickType.LeftClick == clickType || ClickType.LeftDoubleClick == clickType)
2560 && true == element.Ae.TryGetCurrentPattern(InvokePattern.Pattern, out objPattern))
2561 {
2562 // the default action: a left click on a button, and a double click on anything that has one,
2563 // which is how an item is activated
2564 try
2565 {
2566 ((InvokePattern)objPattern).Invoke();
2567 invoked = true;
2568
2569 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Element [{NameOf(element)}] [{clickType}] clicked through InvokePattern.", application, GPALObjectType.Application);
2570 }
2571 catch (Exception)
2572 {
2573 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"InvokePattern threw on control [{control}], falling back to the mouse.", application, GPALObjectType.Application);
2574 }
2575 }
2576 else
2577 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Control [{control}] offers no pattern for a [{clickType}], so the mouse is how it is clicked.", application, GPALObjectType.Application);
2578
2579 // a control with no InvokePattern, and any click that is not a plain left one, are clicked with
2580 // the mouse
2581 if (false == invoked)
2582 HardwareClick(application, element, clickType, modifierKeys, selector?.OffsetX ?? 5, selector?.OffsetY ?? 5); // CAVEAT: if .ForSelector is not provided, default to 5
2583 }
2584
2597 private static string NameOf(GPALAutomationElement element)
2598 {
2599 string retVal = null == element || null == element.Ae ? "nothing" : element.Ae.Current.Name;
2600
2601 if (true == string.IsNullOrWhiteSpace(retVal) && null != element && null != element.Ae)
2602 retVal = element.Ae.Current.ControlType.ProgrammaticName;
2603
2604 return retVal;
2605 }
2606
2617 public static void HardwareClick(Application application, GPALAutomationElement element, ClickType clickType, ModifierKeys modifierKeys, int OffsetX = 0, int OffsetY = 0)
2618 {
2619 try
2620 {
2621 if (ModifierKeys.NONE != modifierKeys)
2622 HardwareHelper.PressModifierKey(modifierKeys);
2623
2624 // the move decides where inside the element to aim and answers with the point it moved to, so
2625 // the click lands where the pointer already is. Working it out a second time here is what had
2626 // the pointer jump to the corner and then to somewhere else for one click
2627 System.Drawing.Point clickAt = HardwareMoveTo(application, element, OffsetX, OffsetY);
2628
2629 // said here, between the modifier going down and coming up, because that is where the click
2630 // happens. Said at all because a press and a release with nothing logged between them read as
2631 // a modifier held around nothing
2632 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Element [{NameOf(element)}] [{clickType}] clicked with modifiers [{Browser.Browser.GetModifierKeys(modifierKeys)}] at [{clickAt.X},{clickAt.Y}] with the mouse.", application, GPALObjectType.Application);
2633
2634 HardwareHelper.HardwareClick(clickAt.X, clickAt.Y, clickType);
2635 }
2636 catch
2637 {
2638
2639 }
2640 finally
2641 {
2642 if (ModifierKeys.NONE != modifierKeys)
2643 HardwareHelper.ReleaseModifierKeys(modifierKeys);
2644 }
2645 }
2652 public static void DragAndDrop(Application application, ModifierKeys modifierKeys = ModifierKeys.NONE)
2653 {
2654 var selectors = new List<Selector>();
2655
2656 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
2657 if (SelectorType.Selector == selector.SelectorType)
2658 selectors.Add(selector);
2659
2660 // two selectors name the two ends of the drag: grab the first, let go over the second. The distance
2661 // is worked out from where the two elements actually are, so the workflow never counts pixels and
2662 // the drag survives a resized window, a re-sorted list or a different screen
2663 if (2 == selectors.Count)
2664 DragToTarget(application, selectors[0], selectors[1], modifierKeys);
2665 else
2666 DragByDelta(application, modifierKeys);
2667 }
2668
2684 private static void DragToTarget(Application application, Selector source, Selector target, ModifierKeys modifierKeys)
2685 {
2686 Rectangle from = FirstRectangle(application, source);
2687 Rectangle to = FirstRectangle(application, target);
2688
2689 if (true == from.IsEmpty || true == to.IsEmpty)
2690 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"A drag needs both ends. Selector [{source.Name}] {(true == from.IsEmpty ? "matched nothing" : "matched")} and selector [{target.Name}] {(true == to.IsEmpty ? "matched nothing" : "matched")}.", application, GPALObjectType.Application);
2691 else
2692 {
2693 // the middle of the element unless the selector says where. A drag that starts at the corner
2694 // starts on whatever sits there, which on a list row is the blank margin beside the icon, and
2695 // pressing empty space and moving is a rubber band selection rather than a drag of the row
2696 int grabX = from.X + (0 == source.OffsetX ? from.Width / 2 : source.OffsetX);
2697 int grabY = from.Y + (0 == source.OffsetY ? from.Height / 2 : source.OffsetY);
2698
2699 int dropX = to.X + (0 == target.OffsetX ? to.Width / 2 : target.OffsetX) + target.DeltaX;
2700 int dropY = to.Y + (0 == target.OffsetY ? to.Height / 2 : target.OffsetY) + target.DeltaY;
2701
2702 // a held key is machine wide, so it is released in a finally. Anything that throws mid drag
2703 // would otherwise leave Control or Shift down for every window the user touches afterwards
2704 try
2705 {
2706 HardwareHelper.ReleaseStrayModifiers(modifierKeys);
2707
2708 if (ModifierKeys.NONE != modifierKeys)
2709 HardwareHelper.PressModifierKey(modifierKeys);
2710
2711 HardwareHelper.MouseDown(grabX, grabY, ClickType.LeftClick);
2712 HardwareHelper.DragTravel(dropX, dropY, true == GPAL.SimulateMouse || true == source.SimulateMouse);
2713 HardwareHelper.MouseUp(dropX, dropY, ClickType.LeftClick);
2714 }
2715 finally
2716 {
2717 if (ModifierKeys.NONE != modifierKeys)
2718 HardwareHelper.ReleaseModifierKeys(modifierKeys);
2719 }
2720
2721 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Dragged [{source.Name}] from [{grabX},{grabY}] onto [{target.Name}] at [{dropX},{dropY}]", application, GPALObjectType.Application);
2722 }
2723 }
2724
2731 private static Rectangle FirstRectangle(Application application, Selector selector)
2732 {
2733 Rectangle retVal = Rectangle.Empty;
2734
2735 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out List<GPALAutomationElement> matchedElements);
2736
2737 if (null != matchedElements && 0 < matchedElements.Count)
2738 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
2739
2740 if (false == selector.DeleteMe && null != elems && 0 < elems.Count)
2741 {
2742 // a control found under an image is dragged from, or dropped on, where the picture is
2743 GPALAutomationElement elem = elems[0].ImageMatch ?? elems[0];
2744
2745 if (null != elem.Gae)
2746 retVal = new Rectangle(elem.Gae.Location, elem.Gae.Size);
2747 else
2748 retVal = new Rectangle(
2749 new System.Drawing.Point((int)elem.Ae.Current.BoundingRectangle.X, (int)elem.Ae.Current.BoundingRectangle.Y),
2750 new System.Drawing.Size((int)elem.Ae.Current.BoundingRectangle.Width, (int)elem.Ae.Current.BoundingRectangle.Height));
2751 }
2752 else
2753 PublishToEventHandler(application, selector, elems?.ToList());
2754
2755 return retVal;
2756 }
2757
2764 private static void DragByDelta(Application application, ModifierKeys modifierKeys)
2765 {
2766 List<GPALAutomationElement> matchedElements;
2767
2768 foreach (Selector selector in application.CurrentUOW.WithSelectorList)
2769 {
2770 if (SelectorType.Selector != selector.SelectorType)
2771 continue;
2772
2773 ReadOnlyCollection<GPALAutomationElement> elems = ElementHelper.FindElements(application, application.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2774
2775 if (true == selector.DeleteMe || null == elems)
2776 {
2777 PublishToEventHandler(application, selector, elems?.ToList());
2778 continue;
2779 }
2780
2781 if (null != matchedElements)
2782 elems = new ReadOnlyCollection<GPALAutomationElement>(matchedElements);
2783
2784 int rowCount = 0;
2785
2786 foreach (GPALAutomationElement found in elems)
2787 {
2788 Rectangle rect;
2789
2790 // a control found under an image is dragged from where the picture is
2791 GPALAutomationElement elem = found.ImageMatch ?? found;
2792
2793 if (null != elem.Gae)
2794 rect = new Rectangle(elem.Gae.Location, elem.Gae.Size);
2795 else
2796 rect = new Rectangle(
2797 new System.Drawing.Point((int)elem.Ae.Current.BoundingRectangle.X, (int)elem.Ae.Current.BoundingRectangle.Y),
2798 new System.Drawing.Size((int)elem.Ae.Current.BoundingRectangle.Width, (int)elem.Ae.Current.BoundingRectangle.Height));
2799
2800 // Mouse down at element origin + OffsetX/Y, travel to that point + DeltaX/Y, mouse up there.
2801 // The travel is the drag. Windows starts one only when the pointer moves past its drag
2802 // threshold while the button is held, and only if it sees the movement: going straight from
2803 // down to up is a click wherever it lands, which is why Explorer went through the motions
2804 // and never picked the file up. This is what the browser side has always done
2805 // the middle of the element unless the selector says where, for the same reason the two
2806 // selector drag does it: a press on the corner lands on whatever is there, not on this
2807 int grabX = rect.X + (0 == selector.OffsetX ? rect.Width / 2 : selector.OffsetX);
2808 int grabY = rect.Y + (0 == selector.OffsetY ? rect.Height / 2 : selector.OffsetY);
2809
2810 int endX = grabX + selector.DeltaX;
2811 int endY = grabY + selector.DeltaY;
2812
2813 // a held key is machine wide, so it is released in a finally whatever happens in between
2814 try
2815 {
2816 HardwareHelper.ReleaseStrayModifiers(modifierKeys);
2817
2818 if (ModifierKeys.NONE != modifierKeys)
2819 HardwareHelper.PressModifierKey(modifierKeys);
2820
2821 HardwareHelper.MouseDown(grabX, grabY, ClickType.LeftClick);
2822 HardwareHelper.DragTravel(endX, endY, true == GPAL.SimulateMouse || true == selector.SimulateMouse);
2823 HardwareHelper.MouseUp(endX, endY, ClickType.LeftClick);
2824 }
2825 finally
2826 {
2827 if (ModifierKeys.NONE != modifierKeys)
2828 HardwareHelper.ReleaseModifierKeys(modifierKeys);
2829 }
2830
2831 if (++rowCount >= HowManyToActOn(application))
2832 break;
2833 }
2834 }
2835 }
2836
2844 public static void ExpandMenuItem(Application application, AutomationElement automationElement)
2845 {
2846 if (true == GPAL.SimulateMouse || true == application.CurrentUOW.CurrentSelector?.SimulateMouse)
2847 {
2848 HardwareHelper.MoveMouse((int)automationElement.Current.BoundingRectangle.X, (int)automationElement.Current.BoundingRectangle.Y, 10, 10); // source didn't define values for rx, ry }
2849 HardwareHelper.LeftClick((int)automationElement.Current.BoundingRectangle.X, (int)automationElement.Current.BoundingRectangle.Y);
2850 Thread.Sleep(250); // wait for display before trying to find elements
2851 }
2852 else
2853 {
2854 try
2855 {
2856 ExpandCollapsePattern pattern = automationElement.GetCurrentPattern(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern;
2857 pattern.Expand();
2858 }
2859 catch (Exception)
2860 {
2861 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to expand menuitem for Control [{automationElement.Current.LocalizedControlType}], trying InvokePattern.", application, GPALObjectType.Application);
2862 try
2863 {
2864 InvokePattern pattern = automationElement.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
2865 pattern.Invoke();
2866 }
2867 catch (Exception ex2)
2868 {
2869 // a menu that answers neither pattern opens with the mouse or does not open at all. An
2870 // item on a menu that stays shut is reached by reading the window's own menu instead,
2871 // which is what the search does next, so this gives up quietly rather than throwing on
2872 // the way out and taking that with it.
2873 //
2874 // Nothing is asked of the element here. Invoking a menu item can leave the element it
2875 // was invoked on behind, and asking a gone element for its rectangle throws, which is
2876 // the failure this whole branch exists to avoid
2877 if (false == HardwareReaches(application))
2878 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, "A menu would not open through UIAutomation and the mouse cannot open it either, so what is on it is read from the window's own menu.", application, GPALObjectType.Application, ex2);
2879 else
2880 {
2881 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to expand menuitem for Control [{automationElement.Current.LocalizedControlType}], using hardware fallback.", application, GPALObjectType.Application);
2882
2883 Random random = new Random(10007);
2884
2885 int offsetX = random.Next(1, (int)automationElement.Current.BoundingRectangle.Width - 2);
2886 int offsetY = random.Next(1, (int)automationElement.Current.BoundingRectangle.Height - 2);
2887 ElementHelper.HardwareClick(null, new GPALAutomationElement(automationElement), Enums.ClickType.LeftClick, Enums.ModifierKeys.NONE, offsetX, offsetY);
2888 }
2889 }
2890 }
2891 }
2892 }
2897 public static void SwitchToTab(Application application)
2898 {
2899 List<GPALAutomationElement> matchedElements;
2900 bool matchedAll = false;
2901
2902 foreach (Selector sel in application.CurrentUOW.WithSelectorList)
2903 {
2904 if (SelectorType.Selector != sel.SelectorType)
2905 continue; // we don't look up data literals
2906
2907 ElementHelper.FindElements(application, application.CurrentUOW, sel, out matchedAll, out matchedElements);
2908
2909 if (null != matchedElements)
2910 {
2911 int rowCount = 0;
2912
2913 // what makes something switchable is that it can be selected, not what it is called.
2914 // LocalizedControlType is a display string in the user's language, "tab item" on an
2915 // English Windows and something else everywhere else, so it was never "TabControl" and
2916 // this loop never did anything
2917 foreach (GPALAutomationElement elem in matchedElements)
2918 {
2919 if (null == elem.Ae)
2920 continue;
2921
2922 if (true == elem.Ae.TryGetCurrentPattern(SelectionItemPattern.Pattern, out object selectionPattern))
2923 {
2924 try
2925 {
2926 // which tab, whether it was already the one showing, and that the pattern is
2927 // what switched it. Selecting through the pattern moves nothing on screen, so
2928 // without this a switch that worked and a switch that did nothing look alike
2929 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Switching to [{elem.Ae.Current.Name}] through SelectionItemPattern, which reads Selected [{elem.Selected}] before the switch.", application, GPALObjectType.Application);
2930
2931 ((SelectionItemPattern)selectionPattern).Select();
2932
2933 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{elem.Ae.Current.Name}] reads Selected [{elem.Selected}] after the switch.", application, GPALObjectType.Application);
2934 }
2935 catch (Exception ex)
2936 {
2937 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to use SelectionPattern to select the tab [{elem.Ae.Current.Name}], falling back on hardware click.", application, GPALObjectType.Application, ex);
2938
2939 if (true == MsaaHelper.Select(application, elem.Ae))
2940 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Switched to [{elem.Ae.Current.Name}] through MSAA.", application, GPALObjectType.Application);
2941 else if (true == HardwareReaches(application))
2942 HardwareClick(application, elem, ClickType.LeftClick, ModifierKeys.NONE, sel.OffsetX, sel.OffsetY);
2943 else
2944 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"The tab [{elem.Ae.Current.Name}] was not switched to. Neither SelectionPattern nor MSAA would select it and the mouse cannot reach it here.", application, GPALObjectType.Application);
2945 }
2946 }
2947 else if (true == MsaaHelper.Select(application, elem.Ae))
2948 {
2949 // the tab reads as selected afterwards or this arm was not taken, so the switch is
2950 // known to have happened rather than assumed. That is what makes it safe to try
2951 // before the mouse: a refusal costs a read and falls through
2952 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Switched to [{elem.Ae.Current.Name}] through MSAA.", application, GPALObjectType.Application);
2953 }
2954 else if (true == HardwareReaches(application))
2955 {
2956 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{elem.Ae.Current.Name}] publishes no selection through UIAutomation or MSAA, so the tab is switched to with the mouse.", application, GPALObjectType.Application);
2957
2958 HardwareClick(application, elem, ClickType.LeftClick, ModifierKeys.NONE, sel.OffsetX, sel.OffsetY);
2959 }
2960 else
2961 {
2962 // no selection to take and no mouse to click with is no way in at all. Said out
2963 // loud because the click used to be made anyway, land nowhere, and leave the
2964 // workflow reading the tab it started on
2965 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"The tab [{elem.Ae.Current.Name}] was not switched to. It publishes no selection through UIAutomation or MSAA, so switching it is the mouse, and the mouse cannot reach [{application.applicationSettings.HiddenDesktopName}] while it is hidden.", application, GPALObjectType.Application);
2966 }
2967
2968 if (++rowCount >= HowManyToActOn(application))
2969 break;
2970 }
2971 }
2972 }
2973
2974 }
2992 private static int HowManyToActOn(Application application)
2993 {
2994 return int.MaxValue == application.CurrentUOW.WithAllThatMatch ? 1 : application.CurrentUOW.WithAllThatMatch;
2995 }
2996
3010 private static AutomationElement WritableFor(Application application, AutomationElement element)
3011 {
3012 AutomationElement retVal = element;
3013
3014 if (false == element.TryGetCurrentPattern(RangeValuePattern.Pattern, out object unused)
3015 && false == element.TryGetCurrentPattern(ValuePattern.Pattern, out unused))
3016 {
3017 AutomationElement inside = element.FindFirst(TreeScope.Descendants, new OrCondition(
3018 new PropertyCondition(AutomationElement.IsRangeValuePatternAvailableProperty, true),
3019 new PropertyCondition(AutomationElement.IsValuePatternAvailableProperty, true)));
3020
3021 if (null != inside)
3022 {
3023 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{element.Current.ControlType.ProgrammaticName}] carries no value of its own, so the [{inside.Current.ControlType.ProgrammaticName}] inside it is what takes one.", application, GPALObjectType.Application);
3024
3025 retVal = inside;
3026 }
3027 }
3028
3029 return retVal;
3030 }
3031
3032 // a control's own windows. UIAutomation builds its tree from what a framework chooses to publish, and
3033 // WinForms publishes a spinner as two buttons and nothing else, so the box holding the value is reached
3034 // through Windows itself
3035 [DllImport("user32.dll")]
3036 private static extern bool EnumChildWindows(IntPtr parent, EnumChildProc callback, IntPtr lParam);
3037
3038 private delegate bool EnumChildProc(IntPtr handle, IntPtr lParam);
3039
3040 [DllImport("user32.dll")]
3041 private static extern IntPtr SendMessage(IntPtr handle, int message, IntPtr wParam, IntPtr lParam);
3042
3043 [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
3044 private static extern IntPtr SendMessage(IntPtr handle, int message, IntPtr wParam, string lParam);
3045
3046 [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
3047 private static extern int SendMessage(IntPtr handle, int message, int wParam, System.Text.StringBuilder lParam);
3048
3049 private const int WM_KILLFOCUS = 0x0008;
3050 private const int WM_SETTEXT = 0x000C;
3051 private const int WM_GETTEXT = 0x000D;
3052 private const int WM_GETTEXTLENGTH = 0x000E;
3053
3059 private static string NativeTextOf(IntPtr handle)
3060 {
3061 int length = SendMessage(handle, WM_GETTEXTLENGTH, 0, null);
3062 System.Text.StringBuilder text = new System.Text.StringBuilder(length + 1);
3063
3064 SendMessage(handle, WM_GETTEXT, text.Capacity, text);
3065
3066 return text.ToString();
3067 }
3068
3088 private static bool SetNativeText(Application application, AutomationElement element, string useText, WriteMode writeMode)
3089 {
3090 bool retVal = false;
3091 IntPtr handle = new IntPtr(element.Current.NativeWindowHandle);
3092
3093 // WM_SETTEXT replaces everything the control holds, so it can only write text that was going to
3094 // replace everything anyway. An Append or an Insert has to keep what is there, and keeping it
3095 // means reading it, adding to it and writing the whole lot back: if that write lands short, and a
3096 // window is free to take one character of it and answer TRUE, what the workflow asked to keep is
3097 // gone, and putting it back goes through the same message that just lost it. Adding to a control
3098 // is typed in, where the caret is moved and nothing existing is rewritten
3099 if (WriteMode.Overwrite != writeMode)
3100 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{element.Current.ControlType.ProgrammaticName}] is added to rather than replaced, which its window cannot be asked to do, so it is typed into instead.", application, GPALObjectType.Application);
3101 else if (IntPtr.Zero != handle)
3102 {
3103 // and the write is checked rather than trusted, the same way the value above it is. A window
3104 // that answers WM_SETTEXT with TRUE has not promised to have stored what it was given: one
3105 // that reads the message as bytes takes a string of wide characters as its first character and
3106 // the zero beside it, answers TRUE, and holds one letter. Reading it back is the only thing
3107 // that tells the difference, and nothing is lost by finding out, because an overwrite was
3108 // taking the old text away regardless
3109 bool answered = IntPtr.Zero != SendMessage(handle, WM_SETTEXT, IntPtr.Zero, useText);
3110
3111 retVal = true == answered
3112 && (true == string.IsNullOrEmpty(useText) || true == NativeTextOf(handle).Contains(useText));
3113
3114 if (true == retVal)
3115 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{element.Current.ControlType.ProgrammaticName}] publishes no value that can be written, so the text was written to the window holding it.", application, GPALObjectType.Application);
3116 else if (true == answered)
3117 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{element.Current.ControlType.ProgrammaticName}] said it took the text written to its window but is not holding it, so it is typed into instead.", application, GPALObjectType.Application);
3118 }
3119
3120 return retVal;
3121 }
3122
3135 private static AutomationElement NativeWritableFor(AutomationElement element)
3136 {
3137 AutomationElement retVal = null;
3138 IntPtr handle = new IntPtr(element.Current.NativeWindowHandle);
3139
3140 if (IntPtr.Zero != handle)
3141 {
3142 List<IntPtr> inside = new List<IntPtr>();
3143
3144 EnumChildWindows(handle, (child, unused) => { inside.Add(child); return true; }, IntPtr.Zero);
3145
3146 foreach (IntPtr child in inside)
3147 {
3148 AutomationElement fromWindow = AutomationElement.FromHandle(child);
3149
3150 // ValuePattern and nothing else. The same windows answer to RangeValuePattern as well, but
3151 // the range it reports is not the control's: it reads a value of zero and calls the real
3152 // maximum out of bounds, so setting through it throws on any number worth setting
3153 if (true == fromWindow.TryGetCurrentPattern(ValuePattern.Pattern, out object unused))
3154 {
3155 retVal = fromWindow;
3156 break;
3157 }
3158 }
3159 }
3160
3161 return retVal;
3162 }
3163
3174 private static void Commit(AutomationElement element)
3175 {
3176 IntPtr handle = new IntPtr(element.Current.NativeWindowHandle);
3177
3178 if (IntPtr.Zero != handle)
3179 SendMessage(handle, WM_KILLFOCUS, IntPtr.Zero, IntPtr.Zero);
3180 }
3181
3196 private static bool SetNativeValue(Application application, AutomationElement element, string value)
3197 {
3198 bool retVal = false;
3199 AutomationElement writable = NativeWritableFor(element);
3200
3201 if (null != writable && true == writable.TryGetCurrentPattern(ValuePattern.Pattern, out object valuePattern))
3202 {
3203 ((ValuePattern)valuePattern).SetValue(value);
3204
3205 Commit(writable);
3206
3207 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{element.Current.ControlType.ProgrammaticName}] keeps its value in a window UIAutomation does not list, so [{value}] was written there and the control was told to read it.", application, GPALObjectType.Application);
3208
3209 retVal = true;
3210 }
3211
3212 return retVal;
3213 }
3214
3215 public static void SetRange(Application application, double rangeValue)
3216 {
3217 List<GPALAutomationElement> matchedElements;
3218 bool matchedAll = false;
3219
3220 foreach (Selector sel in application.CurrentUOW.WithSelectorList)
3221 {
3222 if (SelectorType.Selector != sel.SelectorType)
3223 continue;
3224
3225 ElementHelper.FindElements(application, application.CurrentUOW, sel, out matchedAll, out matchedElements);
3226
3227 if (null != matchedElements)
3228 {
3229 foreach (GPALAutomationElement elem in matchedElements)
3230 {
3231 if (null == elem.Ae) continue;
3232
3233 // a spinner is not one control, it is an edit with two buttons beside it, and which of
3234 // them carries the value is the framework's business rather than the workflow's. So the
3235 // element named is asked first and what is inside it second, which means a workflow can
3236 // say "set the spinner" and mean it
3237 AutomationElement writable = WritableFor(application, elem.Ae);
3238 bool taken = false;
3239
3240 // each way of setting it is tried until one works. A control that offers a pattern
3241 // does not have to honour it: a WinForms spinner offers RangeValuePattern and then
3242 // reports a range that is not its own, refusing anything inside the range it really
3243 // has, so an offer that comes to nothing has to be followed by the next way in
3244 if (true == writable.TryGetCurrentPattern(RangeValuePattern.Pattern, out object rangeValuePattern))
3245 {
3246 try
3247 {
3248 ((RangeValuePattern)rangeValuePattern).SetValue(rangeValue);
3249
3250 taken = true;
3251 }
3252 catch (Exception ex)
3253 {
3254 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"RangeValuePattern on [{writable.Current.ControlType.ProgrammaticName}] refused [{rangeValue}], so the next way of setting it is tried.", application, GPALObjectType.Application, ex);
3255 }
3256 }
3257
3258 if (false == taken && true == writable.TryGetCurrentPattern(ValuePattern.Pattern, out object valuePattern))
3259 {
3260 try
3261 {
3262 ((ValuePattern)valuePattern).SetValue(rangeValue.ToString());
3263
3264 // the box the value went into may be a spinner's, whether the workflow named
3265 // the spinner or the box itself, and either way it is not the value until the
3266 // control has read it
3267 Commit(writable);
3268
3269 taken = true;
3270 }
3271 catch (Exception ex)
3272 {
3273 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"ValuePattern on [{writable.Current.ControlType.ProgrammaticName}] refused [{rangeValue}], so the next way of setting it is tried.", application, GPALObjectType.Application, ex);
3274 }
3275 }
3276
3277 // and failing both, the control still has the value somewhere. A WinForms spinner is
3278 // older than UIAutomation and publishes nothing that holds it, while the edit box it
3279 // keeps it in sits there as a window Windows will hand over on request
3280 if (false == taken)
3281 taken = SetNativeValue(application, elem.Ae, rangeValue.ToString());
3282
3283 // and when even that finds nothing, the way a person does it
3284 if (false == taken)
3285 {
3286 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{elem.Ae.Current.ControlType.ProgrammaticName}] offers no value to set through UIAutomation, so it is typed into instead.", application, GPALObjectType.Application);
3287
3288 HardwareFillInFrom(application, elem, sel.OffsetX, sel.OffsetY, rangeValue.ToString(), WriteMode.Overwrite);
3289 }
3290 }
3291 }
3292 }
3293 }
3294 }
3295}
3296
3297
3298
Settings for the current selector. Use this only for debugging.