-
Notifications
You must be signed in to change notification settings - Fork 905
/
win.cpp
2371 lines (2041 loc) · 88 KB
/
win.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
AutoHotkey
Copyright 2003-2009 Chris Mallett (support@autohotkey.com)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "stdafx.h"
#include "script.h"
#include "window.h"
#include "application.h"
#include "script_func_impl.h"
#include "abi.h"
static FResult WinAct(WINTITLE_PARAMETERS_DECL, BuiltInFunctionID action, optl<double> aWaitTime = nullptr)
{
TCHAR title_buf[MAX_NUMBER_SIZE];
auto aTitle = aWinTitle ? TokenToString(*aWinTitle, title_buf) : _T("");
auto aText = aWinText.value_or_empty();
// Set initial guess for is_ahk_group (further refined later). For ahk_group, WinText,
// ExcludeTitle, and ExcludeText must be blank so that they are reserved for future use
// (i.e. they're currently not supported since the group's own criteria take precedence):
bool is_ahk_group = !_tcsnicmp(aTitle, _T("ahk_group"), 9) && !*aText
&& aExcludeTitle.is_blank_or_omitted() && aExcludeText.is_blank_or_omitted();
// The following is not quite accurate since is_ahk_group is only a guess at this stage, but
// given the extreme rarity of the guess being wrong, this shortcut seems justified to reduce
// the code size/complexity. A wait_time of zero seems best for group closing because it's
// currently implemented to do the wait after every window in the group. In addition,
// this makes "WinClose ahk_group GroupName" behave identically to "GroupClose GroupName",
// which seems best, for consistency:
int wait_time = is_ahk_group ? 0 : DEFAULT_WINCLOSE_WAIT;
if (aWaitTime.has_value()) // Implies (action == FID_WinClose || action == FID_WinKill)
wait_time = (int)(1000 * aWaitTime.value());
if (is_ahk_group)
if (WinGroup *group = g_script.FindGroup(omit_leading_whitespace(aTitle + 9)))
{
group->ActUponAll(action, wait_time); // It will do DoWinDelay if appropriate.
return OK;
}
// Since above didn't return, either the group doesn't exist or it's paired with other
// criteria, such as "ahk_group G ahk_class C", so do the normal single-window behavior.
HWND target_window = NULL;
if (aWinTitle)
{
bool hwnd_specified;
auto fr = DetermineTargetHwnd(target_window, hwnd_specified, *aWinTitle);
if (fr != OK)
return fr;
if (hwnd_specified && !target_window) // Specified a HWND of 0, or IsWindow() returned false.
return FError(ERR_NO_WINDOW, nullptr, ErrorPrototype::Target);
}
if (action == FID_WinClose || action == FID_WinKill)
{
if (target_window)
{
WinClose(target_window, wait_time, action == FID_WinKill);
DoWinDelay;
return OK;
}
if (!WinClose(*g, aTitle, aText, wait_time, aExcludeTitle.value_or_empty(), aExcludeText.value_or_empty(), action == FID_WinKill))
// Currently WinClose returns NULL only for this case; it doesn't confirm the window closed.
return FError(ERR_NO_WINDOW, nullptr, ErrorPrototype::Target);
DoWinDelay;
return OK;
}
if (!target_window)
{
// By design, the WinShow command must always unhide a hidden window, even if the user has
// specified that hidden windows should not be detected. So set this now so that
// DetermineTargetWindow() will make its calls in the right mode:
bool need_restore = (action == FID_WinShow && !g->DetectHiddenWindows);
if (need_restore)
g->DetectHiddenWindows = true;
target_window = Line::DetermineTargetWindow(aTitle, aText, aExcludeTitle.value_or_empty(), aExcludeText.value_or_empty());
if (need_restore)
g->DetectHiddenWindows = false;
if (!target_window)
return FError(ERR_NO_WINDOW, nullptr, ErrorPrototype::Target);
}
// WinGroup's EnumParentActUponAll() is quite similar to the following, so the two should be
// maintained together.
int nCmdShow = SW_NONE; // Set default.
switch (action)
{
// SW_FORCEMINIMIZE: supported only in Windows 2000/XP and beyond: "Minimizes a window,
// even if the thread that owns the window is hung. This flag should only be used when
// minimizing windows from a different thread."
// My: It seems best to use SW_FORCEMINIMIZE on OS's that support it because I have
// observed ShowWindow() to hang (thus locking up our app's main thread) if the target
// window is hung.
// UPDATE: For now, not using "force" every time because it has undesirable side-effects such
// as the window not being restored to its maximized state after it was minimized
// this way.
case FID_WinMinimize:
if (IsWindowHung(target_window))
{
nCmdShow = SW_FORCEMINIMIZE;
// SW_MINIMIZE can lock up our thread on WinXP, which is why we revert to SW_FORCEMINIMIZE above.
// Older/obsolete comment for background: don't attempt to minimize hung windows because that
// might hang our thread because the call to ShowWindow() would never return.
}
else
nCmdShow = SW_MINIMIZE;
break;
case FID_WinMaximize: if (!IsWindowHung(target_window)) nCmdShow = SW_MAXIMIZE; break;
case FID_WinRestore: if (!IsWindowHung(target_window)) nCmdShow = SW_RESTORE; break;
// Seems safe to assume it's not hung in these cases, since I'm inclined to believe
// (untested) that hiding and showing a hung window won't lock up our thread, and
// there's a chance they may be effective even against hung windows, unlike the
// others above (except ACT_WINMINIMIZE, which has a special FORCE method):
case FID_WinHide: nCmdShow = SW_HIDE; break;
case FID_WinShow: nCmdShow = SW_SHOW; break;
}
// UPDATE: Trying ShowWindowAsync()
// now, which should avoid the problems with hanging. UPDATE #2: Went back to
// not using Async() because sometimes the script lines that come after the one
// that is doing this action here rely on this action having been completed
// (e.g. a window being maximized prior to clicking somewhere inside it).
if (nCmdShow != SW_NONE)
{
// I'm not certain that SW_FORCEMINIMIZE works with ShowWindowAsync(), but
// it probably does since there's absolutely no mention to the contrary
// anywhere on MS's site or on the web. But clearly, if it does work, it
// does so only because Async() doesn't really post the message to the thread's
// queue, instead opting for more aggressive measures. Thus, it seems best
// to do it this way to have maximum confidence in it:
//if (nCmdShow == SW_FORCEMINIMIZE) // Safer not to use ShowWindowAsync() in this case.
ShowWindow(target_window, nCmdShow);
//else
// ShowWindowAsync(target_window, nCmdShow);
DoWinDelay;
}
return OK;
}
bif_impl FResult WinClose(ExprTokenType *aWinTitle, optl<StrArg> aWinText, optl<double> aWaitTime, optl<StrArg> aExcludeTitle, optl<StrArg> aExcludeText)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinClose, aWaitTime);
}
bif_impl FResult WinKill(ExprTokenType *aWinTitle, optl<StrArg> aWinText, optl<double> aWaitTime, optl<StrArg> aExcludeTitle, optl<StrArg> aExcludeText)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinKill, aWaitTime);
}
bif_impl FResult WinShow(WINTITLE_PARAMETERS_DECL)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinShow);
}
bif_impl FResult WinHide(WINTITLE_PARAMETERS_DECL)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinHide);
}
bif_impl FResult WinMaximize(WINTITLE_PARAMETERS_DECL)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinMaximize);
}
bif_impl FResult WinMinimize(WINTITLE_PARAMETERS_DECL)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinMinimize);
}
bif_impl FResult WinRestore(WINTITLE_PARAMETERS_DECL)
{
return WinAct(WINTITLE_PARAMETERS, FID_WinRestore);
}
static FResult WinActivate(WINTITLE_PARAMETERS_DECL, bool aBottom)
{
HWND target_window;
auto fr = DetermineTargetWindow(target_window, aWinTitle, aWinText.value_or_null(), aExcludeTitle.value_or_null(), aExcludeText.value_or_null()
, aBottom); // The last parameter determines whether the first or last match is used.
if (fr != OK)
return fr;
SetForegroundWindowEx(target_window);
// It seems best to do these sleeps here rather than in the windowing
// functions themselves because that way, the program can use the
// windowing functions without being subject to the script's delay
// setting (i.e. there are probably cases when we don't need to wait,
// such as bringing a message box to the foreground, since no other
// actions will be dependent on it actually having happened):
DoWinDelay;
return OK;
}
bif_impl FResult WinActivate(WINTITLE_PARAMETERS_DECL)
{
return WinActivate(WINTITLE_PARAMETERS, false);
}
bif_impl FResult WinActivateBottom(WINTITLE_PARAMETERS_DECL)
{
return WinActivate(WINTITLE_PARAMETERS, true);
}
bif_impl FResult GroupAdd(StrArg aGroup, optl<StrArg> aTitle, optl<StrArg> aText, optl<StrArg> aExcludeTitle, optl<StrArg> aExcludeText)
{
auto group = g_script.FindGroup(aGroup, true);
if (!group)
return FR_FAIL; // It already displayed the error for us.
return group->AddWindow(aTitle.value_or_null(), aText.value_or_null(), aExcludeTitle.value_or_null(), aExcludeText.value_or_null()) ? OK : FR_FAIL;
}
bif_impl FResult GroupActivate(StrArg aGroup, optl<StrArg> aMode, UINT &aRetVal)
{
WinGroup *group;
if ( !(group = g_script.FindGroup(aGroup, true)) ) // Last parameter -> create-if-not-found.
return FR_FAIL; // It already displayed the error for us.
TCHAR mode = 0;
if (aMode.has_nonempty_value())
{
mode = *aMode.value();
if (mode == 'r')
mode = 'R';
if (mode != 'R' || aMode.value()[1])
return FR_E_ARG(1);
}
aRetVal = (UINT)(UINT_PTR)group->Activate(mode == 'R');
return OK;
}
bif_impl FResult GroupDeactivate(StrArg aGroup, optl<StrArg> aMode)
{
auto group = g_script.FindGroup(aGroup);
if (!group)
return FR_E_ARG(0);
TCHAR mode = 0;
if (aMode.has_nonempty_value())
{
mode = *aMode.value();
if (mode == 'r')
mode = 'R';
if (mode != 'R' || aMode.value()[1])
return FR_E_ARG(1);
}
group->Deactivate(mode == 'R');
return OK;
}
bif_impl FResult GroupClose(StrArg aGroup, optl<StrArg> aMode)
{
auto group = g_script.FindGroup(aGroup);
if (!group)
return FR_E_ARG(0);
TCHAR mode = 0;
if (aMode.has_nonempty_value())
{
mode = ctoupper(*aMode.value());
if ((mode != 'R' && mode != 'A') || aMode.value()[1])
return FR_E_ARG(1);
}
if (mode == 'A')
group->ActUponAll(FID_WinClose, 0);
else
group->CloseAndGoToNext(mode == 'R');
return OK;
}
bif_impl FResult WinMove(optl<int> aX, optl<int> aY, optl<int> aWidth, optl<int> aHeight, WINTITLE_PARAMETERS_DECL)
{
HWND target_window;
DETERMINE_TARGET_WINDOW;
RECT rect;
if (!GetWindowRect(target_window, &rect)
|| !MoveWindow(target_window
, aX.value_or(rect.left) // X-position
, aY.value_or(rect.top) // Y-position
, aWidth.value_or(rect.right - rect.left)
, aHeight.value_or(rect.bottom - rect.top)
, TRUE)) // Do repaint.
return FR_E_WIN32;
DoWinDelay;
return OK;
}
static FResult ControlSend(StrArg aKeys, CONTROL_PARAMETERS_DECL_OPT, SendRawModes aMode)
{
DETERMINE_TARGET_CONTROL2;
SendKeys(aKeys, aMode, SM_EVENT, control_window);
// But don't do WinDelay because KeyDelay should have been in effect for the above.
return OK;
}
bif_impl FResult ControlSend(StrArg aKeys, CONTROL_PARAMETERS_DECL_OPT)
{
return ControlSend(aKeys, CONTROL_PARAMETERS, SCM_NOT_RAW);
}
bif_impl FResult ControlSendText(StrArg aKeys, CONTROL_PARAMETERS_DECL_OPT)
{
return ControlSend(aKeys, CONTROL_PARAMETERS, SCM_RAW_TEXT);
}
bif_impl FResult ControlClick(ExprTokenType *aControlSpec, ExprTokenType *aWinTitle, optl<StrArg> aWinText
, optl<StrArg> aWhichButton, optl<int> aClickCount, optl<StrArg> aOptions, optl<StrArg> aExcludeTitle, optl<StrArg> aExcludeText)
{
int aVK = Line::ConvertMouseButton(aWhichButton.value_or_null());
if (!aVK)
return FR_E_ARG(3);
int click_count = aClickCount.value_or(1);
// Set the defaults that will be in effect unless overridden by options:
KeyEventTypes event_type = KEYDOWNANDUP;
bool position_mode = false;
bool do_activate = true;
// These default coords can be overridden either by aOptions or aControl's X/Y mode:
POINT click = {COORD_UNSPECIFIED, COORD_UNSPECIFIED};
for (auto cp = aOptions.value_or_empty(); *cp; ++cp)
{
switch(ctoupper(*cp))
{
case 'D':
event_type = KEYDOWN;
break;
case 'U':
event_type = KEYUP;
break;
case 'N':
// v1.0.45:
// It was reported (and confirmed through testing) that this new NA mode (which avoids
// AttachThreadInput() and SetActiveWindow()) improves the reliability of ControlClick when
// the user is moving the mouse fairly quickly at the time the command tries to click a button.
// In addition, the new mode avoids activating the window, which tends to happen otherwise.
// HOWEVER, the new mode seems no more reliable than the old mode when the target window is
// the active window. In addition, there may be side-effects of the new mode (I caught it
// causing Notepad's Save-As dialog to hang once, during the display of its "Overwrite?" dialog).
// ALSO, SetControlDelay -1 seems to fix the unreliability issue as well (independently of NA),
// though it might not work with some types of windows/controls (thus, for backward
// compatibility, ControlClick still obeys SetControlDelay).
if (ctoupper(cp[1]) == 'A')
{
cp += 1; // Add 1 vs. 2 to skip over the rest of the letters in this option word.
do_activate = false;
}
break;
case 'P':
if (!_tcsnicmp(cp, _T("Pos"), 3))
{
cp += 2; // Add 2 vs. 3 to skip over the rest of the letters in this option word.
position_mode = true;
}
break;
// For the below:
// Use atoi() vs. ATOI() to avoid interpreting something like 0x01D as hex
// when in fact the D was meant to be an option letter:
case 'X':
click.x = _ttoi(cp + 1); // Will be overridden later below if it turns out that position_mode is in effect.
break;
case 'Y':
click.y = _ttoi(cp + 1); // Will be overridden later below if it turns out that position_mode is in effect.
break;
}
}
HWND target_window, control_window;
if (position_mode)
{
// Determine target window only. Control will be found by position below.
auto fr = DetermineTargetWindow(target_window, WINTITLE_PARAMETERS);
if (fr != OK)
return fr;
control_window = NULL;
}
else
{
// Determine target window and control.
auto fr = DetermineTargetControl(control_window, target_window, CONTROL_PARAMETERS, false);
if (fr != OK)
return fr;
}
ASSERT(target_window != NULL);
// It's debatable, but might be best for flexibility (and backward compatibility) to allow target_window to itself
// be a control (at least for the position_mode handler below). For example, the script may have called SetParent
// to make a top-level window the child of some other window, in which case this policy allows it to be seen like
// a non-child.
TCHAR control_buf[MAX_NUMBER_SIZE];
LPCTSTR aControl = nullptr;
if (!control_window && aControlSpec) // Even if position_mode is false, the below is still attempted, as documented.
{
// New section for v1.0.24. But only after the above fails to find a control do we consider
// whether aControl contains X and Y coordinates. That way, if a control class happens to be
// named something like "X1 Y1", it will still be found by giving precedence to class names.
point_and_hwnd_type pah = {0};
pah.ignore_disabled_controls = true; // v1.1.20: Ignore disabled controls.
// Parse the X an Y coordinates in a strict way to reduce ambiguity with control names and also
// to keep the code simple.
auto cp = aControl = omit_leading_whitespace(TokenToString(*aControlSpec, control_buf));
if (ctoupper(*cp) != 'X')
goto control_error;
++cp;
if (!*cp)
goto control_error;
pah.pt.x = ATOI(cp);
if ( !(cp = StrChrAny(cp, _T(" \t"))) ) // Find next space or tab (there must be one for it to be considered valid).
goto control_error;
cp = omit_leading_whitespace(cp + 1);
if (!*cp || _totupper(*cp) != 'Y')
goto control_error;
++cp;
if (!*cp)
goto control_error;
pah.pt.y = ATOI(cp);
// The passed-in coordinates are always relative to target_window's client area because offering
// an option for absolute/screen coordinates doesn't seem useful.
ClientToScreen(target_window, &pah.pt); // Convert to screen coordinates.
EnumChildWindows(target_window, EnumChildFindPoint, (LPARAM)&pah); // Find topmost control containing point.
// If no control is at this point, try posting the mouse event message(s) directly to the
// parent window to increase the flexibility of this feature:
control_window = pah.hwnd_found ? pah.hwnd_found : target_window;
// Convert click's target coordinates to be relative to the client area of the control or
// parent window because that is the format required by messages such as WM_LBUTTONDOWN
// used later below:
click = pah.pt;
ScreenToClient(control_window, &click);
}
// This is done this late because it seems better to throw an exception whenever the
// target window or control isn't found, or any other error condition occurs above:
if (click_count < 1)
{
if (click_count < 0)
return FR_E_ARG(4);
// Allow this to simply "do nothing", because it increases flexibility
// in the case where the number of clicks is a dereferenced script variable
// that may sometimes (by intent) resolve to zero or negative:
return OK;
}
RECT rect;
if (click.x == COORD_UNSPECIFIED || click.y == COORD_UNSPECIFIED)
{
// The following idea is from AutoIt3. It states: "Get the dimensions of the control so we can click
// the centre of it" (maybe safer and more natural than 0,0).
// My: In addition, this is probably better for some large controls (e.g. SysListView32) because
// clicking at 0,0 might activate a part of the control that is not even visible:
if (!GetWindowRect(control_window, &rect))
return FR_E_WIN32;
if (click.x == COORD_UNSPECIFIED)
click.x = (rect.right - rect.left) / 2;
if (click.y == COORD_UNSPECIFIED)
click.y = (rect.bottom - rect.top) / 2;
}
UINT msg_down, msg_up;
WPARAM wparam, wparam_up = 0;
bool vk_is_wheel = aVK == VK_WHEEL_UP || aVK == VK_WHEEL_DOWN;
bool vk_is_hwheel = aVK == VK_WHEEL_LEFT || aVK == VK_WHEEL_RIGHT; // v1.0.48: Lexikos: Support horizontal scrolling in Windows Vista and later.
if (vk_is_wheel)
{
ClientToScreen(control_window, &click); // Wheel messages use screen coordinates.
wparam = MAKEWPARAM(0, click_count * ((aVK == VK_WHEEL_UP) ? WHEEL_DELTA : -WHEEL_DELTA));
msg_down = WM_MOUSEWHEEL;
// Make the event more accurate by having the state of the keys reflected in the event.
// The logical state (not physical state) of the modifier keys is used so that something
// like this is supported:
// Send, {ShiftDown}
// MouseClick, WheelUp
// Send, {ShiftUp}
// In addition, if the mouse hook is installed, use its logical mouse button state so that
// something like this is supported:
// MouseClick, left, , , , , D ; Hold down the left mouse button
// MouseClick, WheelUp
// MouseClick, left, , , , , U ; Release the left mouse button.
// UPDATE: Since the other ControlClick types (such as leftclick) do not reflect these
// modifiers -- and we want to keep it that way, at least by default, for compatibility
// reasons -- it seems best for consistency not to do them for WheelUp/Down either.
// A script option can be added in the future to obey the state of the modifiers:
//mod_type mod = GetModifierState();
//if (mod & MOD_SHIFT)
// wparam |= MK_SHIFT;
//if (mod & MOD_CONTROL)
// wparam |= MK_CONTROL;
//if (g_MouseHook)
// wparam |= g_mouse_buttons_logical;
}
else if (vk_is_hwheel) // Lexikos: Support horizontal scrolling in Windows Vista and later.
{
wparam = MAKEWPARAM(0, click_count * ((aVK == VK_WHEEL_LEFT) ? -WHEEL_DELTA : WHEEL_DELTA));
msg_down = WM_MOUSEHWHEEL;
}
else
{
switch (aVK)
{
case VK_LBUTTON: msg_down = WM_LBUTTONDOWN; msg_up = WM_LBUTTONUP; wparam = MK_LBUTTON; break;
case VK_RBUTTON: msg_down = WM_RBUTTONDOWN; msg_up = WM_RBUTTONUP; wparam = MK_RBUTTON; break;
case VK_MBUTTON: msg_down = WM_MBUTTONDOWN; msg_up = WM_MBUTTONUP; wparam = MK_MBUTTON; break;
case VK_XBUTTON1: msg_down = WM_XBUTTONDOWN; msg_up = WM_XBUTTONUP; wparam_up = XBUTTON1<<16; wparam = MK_XBUTTON1|wparam_up; break;
case VK_XBUTTON2: msg_down = WM_XBUTTONDOWN; msg_up = WM_XBUTTONUP; wparam_up = XBUTTON2<<16; wparam = MK_XBUTTON2|wparam_up; break;
default: // Just do nothing since this should realistically never happen.
ASSERT(!"aVK value not handled");
}
}
LPARAM lparam = MAKELPARAM(click.x, click.y);
// SetActiveWindow() requires ATTACH_THREAD_INPUT to succeed. Even though the MSDN docs state
// that SetActiveWindow() has no effect unless the parent window is foreground, Jon insists
// that SetActiveWindow() resolved some problems for some users. In any case, it seems best
// to do this in case the window really is foreground, in which case MSDN indicates that
// it will help for certain types of dialogs.
ATTACH_THREAD_INPUT_AND_SETACTIVEWINDOW_IF_DO_ACTIVATE // It's kept with a similar macro for maintainability.
// v1.0.44.13: Notes for the above: Unlike some other Control commands, GetNonChildParent() is not
// called here when target_window==control_window. This is because the script may have called
// SetParent to make target_window the child of some other window, in which case target_window
// should still be used above (unclear). Perhaps more importantly, it's allowed for control_window
// to be the same as target_window, at least in position_mode, whose docs state, "If there is no
// control, the target window itself will be sent the event (which might have no effect depending
// on the nature of the window)." In other words, it seems too complicated and rare to add explicit
// handling for "ahk_id %ControlHWND%" (though the below rules should work).
// The line "ControlClick,, ahk_id %HWND%" can have multiple meanings depending on the nature of HWND:
// 1) If HWND is a top-level window, its topmost child will be clicked.
// 2) If HWND is a top-level window that has become a child of another window via SetParent: same.
// 3) If HWND is a control, its topmost child will be clicked (or itself if it has no children).
// For example, the following works (as documented in the first parameter):
// ControlGet, HWND, HWND,, OK, A ; Get the HWND of the OK button.
// ControlClick,, ahk_id %HWND%
FResult result = OK;
if (vk_is_wheel || vk_is_hwheel) // v1.0.48: Lexikos: Support horizontal scrolling in Windows Vista and later.
{
if (!PostMessage(control_window, msg_down, wparam, lparam))
result = FR_E_WIN32;
else
DoControlDelay;
}
else
{
for (int i = 0; i < click_count; ++i)
{
if (event_type != KEYUP) // It's either down-only or up-and-down so always to the down-event.
{
if (!PostMessage(control_window, msg_down, wparam, lparam))
{
result = FR_E_WIN32;
break;
}
// Seems best to do this one too, which is what AutoIt3 does also. User can always reduce
// ControlDelay to 0 or -1. Update: Jon says this delay might be causing it to fail in
// some cases. Upon reflection, it seems best not to do this anyway because PostMessage()
// should queue up the message for the app correctly even if it's busy. Update: But I
// think the timestamp is available on every posted message, so if some apps check for
// inhumanly fast clicks (to weed out transients with partial clicks of the mouse, or
// to detect artificial input), the click might not work. So it might be better after
// all to do the delay until it's proven to be problematic (Jon implies that he has
// no proof yet). IF THIS IS EVER DISABLED, be sure to do the ControlDelay anyway
// if event_type == KEYDOWN:
DoControlDelay;
}
if (event_type != KEYDOWN) // It's either up-only or up-and-down so always to the up-event.
{
if (!PostMessage(control_window, msg_up, wparam_up, lparam))
{
result = FR_E_WIN32;
break;
}
DoControlDelay;
}
}
}
DETACH_THREAD_INPUT // Also takes into account do_activate, indirectly.
return result;
control_error:
return FError(ERR_NO_CONTROL, aControl, ErrorPrototype::Target);
}
static FResult ControlShow(CONTROL_PARAMETERS_DECL, int aShow)
{
DETERMINE_TARGET_CONTROL2;
ShowWindow(control_window, aShow);
DoControlDelay;
return OK;
}
bif_impl FResult ControlShow(CONTROL_PARAMETERS_DECL)
{
return ControlShow(CONTROL_PARAMETERS, SW_SHOWNOACTIVATE);
}
bif_impl FResult ControlHide(CONTROL_PARAMETERS_DECL)
{
return ControlShow(CONTROL_PARAMETERS, SW_HIDE);
}
bif_impl FResult ControlGetVisible(CONTROL_PARAMETERS_DECL, BOOL &aRetVal)
{
DETERMINE_TARGET_CONTROL2;
aRetVal = IsWindowVisible(control_window);
return OK;
}
bif_impl FResult ControlMove(optl<int> aX, optl<int> aY, optl<int> aWidth, optl<int> aHeight, CONTROL_PARAMETERS_DECL)
{
DETERMINE_TARGET_CONTROL2;
// The following macro is used to keep ControlMove and ControlGetPos in sync:
#define CONTROL_COORD_PARENT(target, control) \
(target == control ? GetNonChildParent(target) : target)
// Determine which window the supplied coordinates are relative to:
HWND coord_parent = CONTROL_COORD_PARENT(target_window, control_window);
// Determine the controls current coordinates relative to coord_parent in case one
// or more parameters were omitted.
RECT control_rect;
if (!GetWindowRect(control_window, &control_rect))
return FR_E_WIN32;
// For simplicity, failure isn't checked for the below since a return value of 0 can
// indicate that the coord_parent is at 0,0 (i.e. the translation made no change).
MapWindowPoints(NULL, coord_parent, (LPPOINT)&control_rect, 2);
POINT point { aX.value_or(control_rect.left), aY.value_or(control_rect.top) };
// MoveWindow accepts coordinates relative to the control's immediate parent, which might
// be different to coord_parent since controls can themselves have child controls. So if
// necessary, map the caller-supplied coordinates to the control's immediate parent:
HWND immediate_parent = GetParent(control_window);
if (immediate_parent != coord_parent)
MapWindowPoints(coord_parent, immediate_parent, &point, 1);
MoveWindow(control_window
, point.x
, point.y
, aWidth.value_or(control_rect.right - control_rect.left)
, aHeight.value_or(control_rect.bottom - control_rect.top)
, TRUE); // Do repaint.
DoControlDelay
return OK;
}
bif_impl FResult ControlGetPos(int *aX, int *aY, int *aWidth, int *aHeight, CONTROL_PARAMETERS_DECL)
{
DETERMINE_TARGET_CONTROL2;
// Determine which window the returned coordinates should be relative to:
HWND coord_parent = CONTROL_COORD_PARENT(target_window, control_window);
RECT child_rect;
// Realistically never fails since DetermineTargetWindow() and ControlExist() should always yield
// valid window handles:
GetWindowRect(control_window, &child_rect);
// Map the screen coordinates returned by GetWindowRect to the client area of coord_parent.
MapWindowPoints(NULL, coord_parent, (LPPOINT)&child_rect, 2);
if (aX) *aX = child_rect.left;
if (aY) *aY = child_rect.top;
if (aWidth) *aWidth = child_rect.right - child_rect.left;
if (aHeight) *aHeight = child_rect.bottom - child_rect.top;
return OK;
}
bif_impl FResult ControlGetFocus(WINTITLE_PARAMETERS_DECL, UINT &aRetVal)
{
HWND target_window;
DETERMINE_TARGET_WINDOW;
GUITHREADINFO guithreadInfo;
guithreadInfo.cbSize = sizeof(GUITHREADINFO);
if (!GetGUIThreadInfo(GetWindowThreadProcessId(target_window, NULL), &guithreadInfo))
{
// Failure is most likely because the target thread has no input queue; i.e. target_window
// is a console window and the process which owns it has no input queue. Controls cannot
// exist without an input queue, so a return value of 0 is appropriate in that case.
// A value of 0 is already ambiguous (window is not focused, or window itself is focused),
// and is most likely preferable to a thrown exception, so returning 0 in the unlikely event
// of some other failure seems acceptable. There might be a possibility of a race condition
// between determining target_window and the window being destroyed, but checking for that
// doesn't seem useful since the window could be destroyed or deactivated after we return.
aRetVal = 0;
return OK;
}
// Use IsChild() to ensure the focused control actually belongs to this window.
// Otherwise, a HWND will be returned if any window in the same thread has focus,
// including the target window itself (typically when it has no controls).
if (!IsChild(target_window, guithreadInfo.hwndFocus))
aRetVal = 0; // As documented, if "none of the target window's controls has focus, the return value is 0".
else
aRetVal = (UINT)(size_t)guithreadInfo.hwndFocus;
return OK;
}
bif_impl FResult ControlGetClassNN(CONTROL_PARAMETERS_DECL, StrRet &aRetVal)
{
DETERMINE_TARGET_CONTROL2;
if (target_window == control_window)
target_window = GetNonChildParent(control_window);
TCHAR class_nn[WINDOW_CLASS_NN_SIZE];
auto fr = ControlGetClassNN(target_window, control_window, class_nn, _countof(class_nn));
return fr != OK ? fr : aRetVal.Copy(class_nn) ? OK : FR_E_OUTOFMEM;
}
// Retrieves the ClassNN of a control.
// aBuf is ideally sized by WINDOW_CLASS_NN_SIZE to avoid any possibility of
// the buffer being insufficient. Must be large enough to fit the class name
// plus CONTROL_NN_SIZE.
FResult ControlGetClassNN(HWND aWindow, HWND aControl, LPTSTR aBuf, int aBufSize)
{
ASSERT(aBufSize > CONTROL_NN_SIZE);
class_and_hwnd_type cah;
cah.hwnd = aControl;
cah.class_name = aBuf;
int length = GetClassName(cah.hwnd, aBuf, aBufSize - CONTROL_NN_SIZE); // Allow room for sequence number.
if (!length)
return FR_E_WIN32;
cah.class_count = 0; // Init for the below.
cah.is_found = false; // Same.
EnumChildWindows(aWindow, EnumChildFindSeqNum, (LPARAM)&cah);
if (!cah.is_found)
return FR_E_FAILED;
// Append the class sequence number onto the class name:
sntprintf(aBuf + length, aBufSize - length, _T("%u"), cah.class_count);
return OK;
}
BOOL CALLBACK EnumChildFindSeqNum(HWND aWnd, LPARAM lParam)
{
class_and_hwnd_type &cah = *(class_and_hwnd_type *)lParam; // For performance and convenience.
TCHAR class_name[WINDOW_CLASS_SIZE];
if (!GetClassName(aWnd, class_name, _countof(class_name)))
return TRUE; // Continue the enumeration.
if (!_tcscmp(class_name, cah.class_name)) // Class names match.
{
++cah.class_count;
if (aWnd == cah.hwnd) // The caller-specified window has been found.
{
cah.is_found = true;
return FALSE;
}
}
return TRUE; // Continue enumeration until a match is found or there aren't any windows remaining.
}
bif_impl FResult ControlFocus(CONTROL_PARAMETERS_DECL)
{
DETERMINE_TARGET_CONTROL2;
// Unlike many of the other Control commands, this one requires AttachThreadInput()
// to have any realistic chance of success (though sometimes it may work by pure
// chance even without it):
ATTACH_THREAD_INPUT
SetFocus(control_window);
DoControlDelay; // Done unconditionally for simplicity, and in case SetFocus() had some effect despite indicating failure.
// GetFocus() isn't called and failure to focus isn't treated as an error because
// a successful change in focus doesn't guarantee that the focus will still be as
// expected when the next line of code runs.
// Very important to detach any threads whose inputs were attached above,
// prior to returning, otherwise the next attempt to attach thread inputs
// for these particular windows may result in a hung thread or other
// undesirable effect:
DETACH_THREAD_INPUT
return OK;
}
bif_impl FResult ControlSetText(StrArg aNewText, CONTROL_PARAMETERS_DECL)
{
DETERMINE_TARGET_CONTROL2;
// SendMessage must be used, not PostMessage(), at least for some (probably most) apps.
// Also: No need to call IsWindowHung() because SendMessageTimeout() should return
// immediately if the OS already "knows" the window is hung:
DWORD_PTR result;
SendMessageTimeout(control_window, WM_SETTEXT, (WPARAM)0, (LPARAM)aNewText
, SMTO_ABORTIFHUNG, 5000, &result);
DoControlDelay;
return OK;
}
bif_impl FResult ControlGetText(CONTROL_PARAMETERS_DECL, StrRet &aRetVal)
{
DETERMINE_TARGET_CONTROL2;
// Handle the output parameter. Note: Using GetWindowTextTimeout() vs. GetWindowText()
// because it is able to get text from more types of controls (e.g. large edit controls):
size_t estimated_length = GetWindowTextTimeout(control_window);
// Allocate memory for the return value.
LPTSTR buf = aRetVal.Alloc(estimated_length);
if (!buf)
return FR_E_OUTOFMEM;
// Fetch the text directly into the buffer. Also set the length explicitly
// in case actual size written was off from the estimated size (since
// GetWindowTextLength() can return more space that will actually be required
// in certain circumstances, see MS docs):
auto actual_length = GetWindowTextTimeout(control_window, buf, estimated_length + 1);
aRetVal.SetLength(actual_length);
if (!actual_length) // There was no text to get or GetWindowTextTimeout() failed.
*buf = '\0';
return OK;
}
static FResult WinSetEnabled(int aValue, CONTROL_PARAMETERS_DECL_OPT)
{
if (aValue < -1 || aValue > 1)
return FR_E_ARG(0);
DETERMINE_TARGET_CONTROL2;
// If this is WinSetEnabled rather than ControlSetEnabled, control_window == target_window.
if (aValue == -1)
aValue = IsWindowEnabled(control_window) ? 0 : 1;
EnableWindow(control_window, aValue);
bool success = !IsWindowEnabled(control_window) == !aValue;
if (aControlSpec)
DoControlDelay;
return success ? OK : FR_E_FAILED;
}
bif_impl FResult WinSetEnabled(int aValue, WINTITLE_PARAMETERS_DECL)
{
return WinSetEnabled(aValue, nullptr, WINTITLE_PARAMETERS);
}
bif_impl FResult ControlSetEnabled(int aValue, CONTROL_PARAMETERS_DECL)
{
return WinSetEnabled(aValue, &CONTROL_PARAMETERS);
}
bif_impl FResult ControlGetEnabled(CONTROL_PARAMETERS_DECL, BOOL &aRetVal)
{
DETERMINE_TARGET_CONTROL2;
aRetVal = IsWindowEnabled(control_window);
return OK;
}
bif_impl FResult ListViewGetContent(optl<StrArg> aOpt, CONTROL_PARAMETERS_DECL, ResultToken &aResultToken)
{
DETERMINE_TARGET_CONTROL2;
auto aHwnd = control_window;
auto aOptions = aOpt.value_or_empty();
// GET ROW COUNT
LRESULT row_count;
if (!SendMessageTimeout(aHwnd, LVM_GETITEMCOUNT, 0, 0, SMTO_ABORTIFHUNG, 2000, (PDWORD_PTR)&row_count)) // Timed out or failed.
return FR_E_WIN32;
// GET COLUMN COUNT
// Through testing, could probably get to a level of 90% certainty that a ListView for which
// InsertColumn() was never called (or was called only once) might lack a header control if the LV is
// created in List/Icon view-mode and/or with LVS_NOCOLUMNHEADER. The problem is that 90% doesn't
// seem to be enough to justify elimination of the code for "undetermined column count" mode. If it
// ever does become a certainty, the following could be changed:
// 1) The extra code for "undetermined" mode rather than simply forcing col_count to be 1.
// 2) Probably should be kept for compatibility: -1 being returned when undetermined "col count".
//
// The following approach might be the only simple yet reliable way to get the column count (sending
// LVM_GETITEM until it returns false doesn't work because it apparently returns true even for
// nonexistent subitems -- the same is reported to happen with LVM_GETCOLUMN and such, though I seem
// to remember that LVM_SETCOLUMN fails on non-existent columns -- but calling that on a ListView
// that isn't in Report view has been known to traumatize the control).
// Fix for v1.0.37.01: It appears that the header doesn't always exist. For example, when an
// Explorer window opens and is *initially* in icon or list mode vs. details/tiles mode, testing
// shows that there is no header control. Testing also shows that there is exactly one column
// in such cases but only for Explorer and other things that avoid creating the invisible columns.
// For example, a script can create a ListView in Icon-mode and give it retrievable column data for
// columns beyond the first. Thus, having the undetermined-col-count mode preserves flexibility
// by allowing individual columns beyond the first to be retrieved. On a related note, testing shows
// that attempts to explicitly retrieve columns (i.e. fields/subitems) other than the first in the
// case of Explorer's Icon/List view modes behave the same as fetching the first column (i.e. Col3
// would retrieve the same text as specifying Col1 or not having the Col option at all).
// Obsolete because not always true: Testing shows that a ListView always has a header control
// (at least on XP), even if you can't see it (such as when the view is Icon/Tile or when -Hdr has
// been specified in the options).
HWND header_control;
LRESULT col_count = -1; // Fix for v1.0.37.01: Use -1 to indicate "undetermined col count".
if (SendMessageTimeout(aHwnd, LVM_GETHEADER, 0, 0, SMTO_ABORTIFHUNG, 2000, (PDWORD_PTR)&header_control)
&& header_control) // Relies on short-circuit boolean order.
SendMessageTimeout(header_control, HDM_GETITEMCOUNT, 0, 0, SMTO_ABORTIFHUNG, 2000, (PDWORD_PTR)&col_count);
// Return value is not checked because if it fails, col_count is left at its default of -1 set above.
// In fact, if any of the above conditions made it impossible to determine col_count, col_count stays
// at -1 to indicate "undetermined".
// PARSE OPTIONS (a simple vs. strict method is used to reduce code size)
bool get_count = tcscasestr(aOptions, _T("Count"));
bool include_selected_only = tcscasestr(aOptions, _T("Selected")); // Explicit "ed" to reserve "Select" for possible future use.
bool include_focused_only = tcscasestr(aOptions, _T("Focused")); // Same.
LPTSTR col_option = tcscasestr(aOptions, _T("Col")); // Also used for mode "Count Col"
int requested_col = col_option ? ATOI(col_option + 3) - 1 : -1;
if (col_option && (get_count ? col_option[3] && !IS_SPACE_OR_TAB(col_option[3]) // "Col" has a suffix.
: (requested_col < 0 || col_count > -1 && requested_col >= col_count))) // Specified column does not exist.
return FR_E_ARG(0);
// IF THE "COUNT" OPTION IS PRESENT, FULLY HANDLE THAT AND RETURN
if (get_count)
{
int result; // Must be signed to support writing a col count of -1 to aOutputVar.
DWORD_PTR msg_result;
if (include_focused_only) // Listed first so that it takes precedence over include_selected_only.
{
if (!SendMessageTimeout(aHwnd, LVM_GETNEXTITEM, -1, LVNI_FOCUSED, SMTO_ABORTIFHUNG, 2000, &msg_result)) // Timed out or failed.
return FR_E_WIN32;
result = (int)msg_result + 1; // i.e. Set it to 0 if not found, or the 1-based row-number otherwise.
}
else if (include_selected_only)
{
if (!SendMessageTimeout(aHwnd, LVM_GETSELECTEDCOUNT, 0, 0, SMTO_ABORTIFHUNG, 2000, &msg_result)) // Timed out or failed.
return FR_E_WIN32;
result = (int)msg_result;
}
else if (col_option) // "Count Col" returns the number of columns.
result = (int)col_count;
else // Total row count.
result = (int)row_count;
aResultToken.SetValue(result);
return OK;
}
// FINAL CHECKS
if (row_count < 1 || !col_count) // But don't return when col_count == -1 (i.e. always make the attempt when col count is undetermined).
return OK; // No text in the control, so indicate success.
// Notes about the following struct definitions: The layout of LVITEM depends on
// which platform THIS executable was compiled for, but we need it to match what
// the TARGET process expects. If the target process is 32-bit and we are 64-bit
// or vice versa, LVITEM can't be used. The following structs are copies of
// LVITEM with UINT (32-bit) or UINT64 (64-bit) in place of the pointer fields.