-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMacroLib.cpp
More file actions
2621 lines (2386 loc) · 135 KB
/
MacroLib.cpp
File metadata and controls
2621 lines (2386 loc) · 135 KB
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
#include "stdafx.h"
#include "MacroLib.h"
#define SET_COLOR_RANGE \
{\
red_low = (aVariation > search_red) ? 0 : search_red - aVariation;\
green_low = (aVariation > search_green) ? 0 : search_green - aVariation;\
blue_low = (aVariation > search_blue) ? 0 : search_blue - aVariation;\
red_high = (aVariation > 0xFF - search_red) ? 0xFF : search_red + aVariation;\
green_high = (aVariation > 0xFF - search_green) ? 0xFF : search_green + aVariation;\
blue_high = (aVariation > 0xFF - search_blue) ? 0xFF : search_blue + aVariation;\
}
CMacroLib::CMacroLib(void)
{
sModifiersLR_persistent = 0;
sSendMode = SM_EVENT;
}
CMacroLib::~CMacroLib(void)
{
}
ResultType CMacroLib::ImageSearch( int aLeft, int aTop, int aRight, int aBottom, LPTSTR aImageFile, int &nRetPosX, int &nRetPosY )
{
POINT origin = {0};
CoordToScreen(origin, COORD_MODE_PIXEL);
aLeft += origin.x;
aTop += origin.y;
aRight += origin.x;
aBottom += origin.y;
// Options are done as asterisk+option to permit future expansion.
// Set defaults to be possibly overridden by any specified options:
int aVariation = 0; // This is named aVariation vs. variation for use with the SET_COLOR_RANGE macro.
COLORREF trans_color = CLR_NONE; // The default must be a value that can't occur naturally in an image.
int icon_number = 0; // Zero means "load icon or bitmap (doesn't matter)".
int width = 0, height = 0;
// For icons, override the default to be 16x16 because that is what is sought 99% of the time.
// This new default can be overridden by explicitly specifying w0 h0:
LPTSTR cp = _tcsrchr(aImageFile, '.');
if (cp)
{
++cp;
if (!(_tcsicmp(cp, _T("ico")) && _tcsicmp(cp, _T("exe")) && _tcsicmp(cp, _T("dll"))))
width = GetSystemMetrics(SM_CXSMICON), height = GetSystemMetrics(SM_CYSMICON);
}
TCHAR color_name[32], *dp;
cp = omit_leading_whitespace(aImageFile); // But don't alter aImageFile yet in case it contains literal whitespace we want to retain.
while (*cp == '*')
{
++cp;
switch (_totupper(*cp))
{
case 'W': width = ATOI(cp + 1); break;
case 'H': height = ATOI(cp + 1); break;
default:
if (!_tcsnicmp(cp, _T("Icon"), 4))
{
cp += 4; // Now it's the character after the word.
icon_number = ATOI(cp); // LoadPicture() correctly handles any negative value.
}
else if (!_tcsnicmp(cp, _T("Trans"), 5))
{
cp += 5; // Now it's the character after the word.
// Isolate the color name/number for ColorNameToBGR():
tcslcpy(color_name, cp, _countof(color_name));
if (dp = StrChrAny(color_name, _T(" \t"))) // Find space or tab, if any.
*dp = '\0';
// Fix for v1.0.44.10: Treat trans_color as containing an RGB value (not BGR) so that it matches
// the documented behavior. In older versions, a specified color like "TransYellow" was wrong in
// every way (inverted) and a specified numeric color like "Trans0xFFFFAA" was treated as BGR vs. RGB.
trans_color = ColorNameToBGR(color_name);
if (trans_color == CLR_NONE) // A matching color name was not found, so assume it's in hex format.
// It seems _tcstol() automatically handles the optional leading "0x" if present:
trans_color = _tcstol(color_name, NULL, 16);
// if color_name did not contain something hex-numeric, black (0x00) will be assumed,
// which seems okay given how rare such a problem would be.
else
trans_color = bgr_to_rgb(trans_color); // v1.0.44.10: See fix/comment above.
}
else // Assume it's a number since that's the only other asterisk-option.
{
aVariation = ATOI(cp); // Seems okay to support hex via ATOI because the space after the number is documented as being mandatory.
if (aVariation < 0)
aVariation = 0;
if (aVariation > 255)
aVariation = 255;
// Note: because it's possible for filenames to start with a space (even though Explorer itself
// won't let you create them that way), allow exactly one space between end of option and the
// filename itself:
}
} // switch()
if ( !(cp = StrChrAny(cp, _T(" \t"))) ) // Find the first space or tab after the option.
goto error; // Bad option/format.
// Now it's the space or tab (if there is one) after the option letter. Advance by exactly one character
// because only one space or tab is considered the delimiter. Any others are considered to be part of the
// filename (though some or all OSes might simply ignore them or tolerate them as first-try match criteria).
aImageFile = ++cp; // This should now point to another asterisk or the filename itself.
// Above also serves to reset the filename to omit the option string whenever at least one asterisk-option is present.
cp = omit_leading_whitespace(cp); // This is done to make it more tolerant of having more than one space/tab between options.
}
// Update: Transparency is now supported in icons by using the icon's mask. In addition, an attempt
// is made to support transparency in GIF, PNG, and possibly TIF files via the *Trans option, which
// assumes that one color in the image is transparent. In GIFs not loaded via GDIPlus, the transparent
// color might always been seen as pure white, but when GDIPlus is used, it's probably always black
// like it is in PNG -- however, this will not relied upon, at least not until confirmed.
// OLDER/OBSOLETE comment kept for background:
// For now, images that can't be loaded as bitmaps (icons and cursors) are not supported because most
// icons have a transparent background or color present, which the image search routine here is
// probably not equipped to handle (since the transparent color, when shown, typically reveals the
// color of whatever is behind it; thus screen pixel color won't match image's pixel color).
// So currently, only BMP and GIF seem to work reliably, though some of the other GDIPlus-supported
// formats might work too.
int image_type;
bool no_delete_bitmap;
HBITMAP hbitmap_image = LoadPicture(aImageFile, width, height, image_type, icon_number, false, &no_delete_bitmap);
// The comment marked OBSOLETE below is no longer true because the elimination of the high-byte via
// 0x00FFFFFF seems to have fixed it. But "true" is still not passed because that should increase
// consistency when GIF/BMP/ICO files are used by a script on both Win9x and other OSs (since the
// same loading method would be used via "false" for these formats across all OSes).
// OBSOLETE: Must not pass "true" with the above because that causes bitmaps and gifs to be not found
// by the search. In other words, nothing works. Obsolete comment: Pass "true" so that an attempt
// will be made to load icons as bitmaps if GDIPlus is available.
if (!hbitmap_image)
goto error;
HDC hdc = GetDC(NULL);
if (!hdc)
{
if (!no_delete_bitmap)
{
if (image_type == IMAGE_ICON)
DestroyIcon((HICON)hbitmap_image);
else
DeleteObject(hbitmap_image);
}
goto error;
}
// From this point on, "goto end" will assume hdc and hbitmap_image are non-NULL, but that the below
// might still be NULL. Therefore, all of the following must be initialized so that the "end"
// label can detect them:
HDC sdc = NULL;
HBITMAP hbitmap_screen = NULL;
LPCOLORREF image_pixel = NULL, screen_pixel = NULL, image_mask = NULL;
HGDIOBJ sdc_orig_select = NULL;
bool found = false; // Must init here for use by "goto end".
bool image_is_16bit;
LONG image_width, image_height;
if (image_type == IMAGE_ICON)
{
// Must be done prior to IconToBitmap() since it deletes (HICON)hbitmap_image:
ICONINFO ii;
if (GetIconInfo((HICON)hbitmap_image, &ii))
{
// If the icon is monochrome (black and white), ii.hbmMask will contain twice as many pixels as
// are actually in the icon. But since the top half of the pixels are the AND-mask, it seems
// okay to get all the pixels given the rarity of monochrome icons. This scenario should be
// handled properly because: 1) the variables image_height and image_width will be overridden
// further below with the correct icon dimensions; 2) Only the first half of the pixels within
// the image_mask array will actually be referenced by the transparency checker in the loops,
// and that first half is the AND-mask, which is the transparency part that is needed. The
// second half, the XOR part, is not needed and thus ignored. Also note that if width/height
// required the icon to be scaled, LoadPicture() has already done that directly to the icon,
// so ii.hbmMask should already be scaled to match the size of the bitmap created later below.
image_mask = getbits(ii.hbmMask, hdc, image_width, image_height, image_is_16bit, 1);
DeleteObject(ii.hbmColor); // DeleteObject() probably handles NULL okay since few MSDN/other examples ever check for NULL.
DeleteObject(ii.hbmMask);
}
if ( !(hbitmap_image = IconToBitmap((HICON)hbitmap_image, true)) )
goto error;
}
if ( !(image_pixel = getbits(hbitmap_image, hdc, image_width, image_height, image_is_16bit)) )
goto end;
// Create an empty bitmap to hold all the pixels currently visible on the screen that lie within the search area:
int search_width = aRight - aLeft + 1;
int search_height = aBottom - aTop + 1;
if ( !(sdc = CreateCompatibleDC(hdc)) || !(hbitmap_screen = CreateCompatibleBitmap(hdc, search_width, search_height)) )
goto end;
if ( !(sdc_orig_select = SelectObject(sdc, hbitmap_screen)) )
goto end;
// Copy the pixels in the search-area of the screen into the DC to be searched:
if ( !(BitBlt(sdc, 0, 0, search_width, search_height, hdc, aLeft, aTop, SRCCOPY)) )
goto end;
LONG screen_width, screen_height;
bool screen_is_16bit;
if ( !(screen_pixel = getbits(hbitmap_screen, sdc, screen_width, screen_height, screen_is_16bit)) )
goto end;
LONG image_pixel_count = image_width * image_height;
LONG screen_pixel_count = screen_width * screen_height;
int i, j, k, x, y; // Declaring as "register" makes no performance difference with current compiler, so let the compiler choose which should be registers.
// If either is 16-bit, convert *both* to the 16-bit-compatible 32-bit format:
if (image_is_16bit || screen_is_16bit)
{
if (trans_color != CLR_NONE)
trans_color &= 0x00F8F8F8; // Convert indicated trans-color to be compatible with the conversion below.
for (i = 0; i < screen_pixel_count; ++i)
screen_pixel[i] &= 0x00F8F8F8; // Highest order byte must be masked to zero for consistency with use of 0x00FFFFFF below.
for (i = 0; i < image_pixel_count; ++i)
image_pixel[i] &= 0x00F8F8F8; // Same.
}
// v1.0.44.03: The below is now done even for variation>0 mode so its results are consistent with those of
// non-variation mode. This is relied upon by variation=0 mode but now also by the following line in the
// variation>0 section:
// || image_pixel[j] == trans_color
// Without this change, there are cases where variation=0 would find a match but a higher variation
// (for the same search) wouldn't.
for (i = 0; i < image_pixel_count; ++i)
image_pixel[i] &= 0x00FFFFFF;
// Search the specified region for the first occurrence of the image:
if (aVariation < 1) // Caller wants an exact match.
{
// Concerning the following use of 0x00FFFFFF, the use of 0x00F8F8F8 above is related (both have high order byte 00).
// The following needs to be done only when shades-of-variation mode isn't in effect because
// shades-of-variation mode ignores the high-order byte due to its use of macros such as GetRValue().
// This transformation incurs about a 15% performance decrease (percentage is fairly constant since
// it is proportional to the search-region size, which tends to be much larger than the search-image and
// is therefore the primary determination of how long the loops take). But it definitely helps find images
// more successfully in some cases. For example, if a PNG file is displayed in a GUI window, this
// transformation allows certain bitmap search-images to be found via variation==0 when they otherwise
// would require variation==1 (possibly the variation==1 success is just a side-effect of it
// ignoring the high-order byte -- maybe a much higher variation would be needed if the high
// order byte were also subject to the same shades-of-variation analysis as the other three bytes [RGB]).
for (i = 0; i < screen_pixel_count; ++i)
screen_pixel[i] &= 0x00FFFFFF;
for (i = 0; i < screen_pixel_count; ++i)
{
// Unlike the variation-loop, the following one uses a first-pixel optimization to boost performance
// by about 10% because it's only 3 extra comparisons and exact-match mode is probably used more often.
// Before even checking whether the other adjacent pixels in the region match the image, ensure
// the image does not extend past the right or bottom edges of the current part of the search region.
// This is done for performance but more importantly to prevent partial matches at the edges of the
// search region from being considered complete matches.
// The following check is ordered for short-circuit performance. In addition, image_mask, if
// non-NULL, is used to determine which pixels are transparent within the image and thus should
// match any color on the screen.
if ((screen_pixel[i] == image_pixel[0] // A screen pixel has been found that matches the image's first pixel.
|| image_mask && image_mask[0] // Or: It's an icon's transparent pixel, which matches any color.
|| image_pixel[0] == trans_color) // This should be okay even if trans_color==CLR_NONE, since CLR_NONE should never occur naturally in the image.
&& image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
&& image_width <= screen_width - i%screen_width) // Image is narrow enough not to exceed the right-side boundary of the search region.
{
// Check if this candidate region -- which is a subset of the search region whose height and width
// matches that of the image -- is a pixel-for-pixel match of the image.
for (found = true, x = 0, y = 0, j = 0, k = i; j < image_pixel_count; ++j)
{
if (!(found = (screen_pixel[k] == image_pixel[j] // At least one pixel doesn't match, so this candidate is discarded.
|| image_mask && image_mask[j] // Or: It's an icon's transparent pixel, which matches any color.
|| image_pixel[j] == trans_color))) // This should be okay even if trans_color==CLR_NONE, since CLR none should never occur naturally in the image.
break;
if (++x < image_width) // We're still within the same row of the image, so just move on to the next screen pixel.
++k;
else // We're starting a new row of the image.
{
x = 0; // Return to the leftmost column of the image.
++y; // Move one row downward in the image.
// Move to the next row within the current-candidate region (not the entire search region).
// This is done by moving vertically downward from "i" (which is the upper-left pixel of the
// current-candidate region) by "y" rows.
k = i + y*screen_width; // Verified correct.
}
}
if (found) // Complete match found.
break;
}
}
}
else // Allow colors to vary by aVariation shades; i.e. approximate match is okay.
{
// The following section is part of the first-pixel-check optimization that improves performance by
// 15% or more depending on where and whether a match is found. This section and one the follows
// later is commented out to reduce code size.
// Set high/low range for the first pixel of the image since it is the pixel most often checked
// (i.e. for performance).
//BYTE search_red1 = GetBValue(image_pixel[0]); // Because it's RGB vs. BGR, the B value is fetched, not R (though it doesn't matter as long as everything is internally consistent here).
//BYTE search_green1 = GetGValue(image_pixel[0]);
//BYTE search_blue1 = GetRValue(image_pixel[0]); // Same comment as above.
//BYTE red_low1 = (aVariation > search_red1) ? 0 : search_red1 - aVariation;
//BYTE green_low1 = (aVariation > search_green1) ? 0 : search_green1 - aVariation;
//BYTE blue_low1 = (aVariation > search_blue1) ? 0 : search_blue1 - aVariation;
//BYTE red_high1 = (aVariation > 0xFF - search_red1) ? 0xFF : search_red1 + aVariation;
//BYTE green_high1 = (aVariation > 0xFF - search_green1) ? 0xFF : search_green1 + aVariation;
//BYTE blue_high1 = (aVariation > 0xFF - search_blue1) ? 0xFF : search_blue1 + aVariation;
// Above relies on the fact that the 16-bit conversion higher above was already done because like
// in PixelSearch, it seems more appropriate to do the 16-bit conversion prior to setting the range
// of high and low colors (vs. than applying 0xF8 to each of the high/low values individually).
BYTE red, green, blue;
BYTE search_red, search_green, search_blue;
BYTE red_low, green_low, blue_low, red_high, green_high, blue_high;
// The following loop is very similar to its counterpart above that finds an exact match, so maintain
// them together and see above for more detailed comments about it.
for (i = 0; i < screen_pixel_count; ++i)
{
// The following is commented out to trade code size reduction for performance (see comment above).
//red = GetBValue(screen_pixel[i]); // Because it's RGB vs. BGR, the B value is fetched, not R (though it doesn't matter as long as everything is internally consistent here).
//green = GetGValue(screen_pixel[i]);
//blue = GetRValue(screen_pixel[i]);
//if ((red >= red_low1 && red <= red_high1
// && green >= green_low1 && green <= green_high1
// && blue >= blue_low1 && blue <= blue_high1 // All three color components are a match, so this screen pixel matches the image's first pixel.
// || image_mask && image_mask[0] // Or: It's an icon's transparent pixel, which matches any color.
// || image_pixel[0] == trans_color) // This should be okay even if trans_color==CLR_NONE, since CLR none should never occur naturally in the image.
// && image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
// && image_width <= screen_width - i%screen_width) // Image is narrow enough not to exceed the right-side boundary of the search region.
// Instead of the above, only this abbreviated check is done:
if (image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
&& image_width <= screen_width - i%screen_width) // Image is narrow enough not to exceed the right-side boundary of the search region.
{
// Since the first pixel is a match, check the other pixels.
for (found = true, x = 0, y = 0, j = 0, k = i; j < image_pixel_count; ++j)
{
search_red = GetBValue(image_pixel[j]);
search_green = GetGValue(image_pixel[j]);
search_blue = GetRValue(image_pixel[j]);
SET_COLOR_RANGE
red = GetBValue(screen_pixel[k]);
green = GetGValue(screen_pixel[k]);
blue = GetRValue(screen_pixel[k]);
if (!(found = red >= red_low && red <= red_high
&& green >= green_low && green <= green_high
&& blue >= blue_low && blue <= blue_high
|| image_mask && image_mask[j] // Or: It's an icon's transparent pixel, which matches any color.
|| image_pixel[j] == trans_color)) // This should be okay even if trans_color==CLR_NONE, since CLR_NONE should never occur naturally in the image.
break; // At least one pixel doesn't match, so this candidate is discarded.
if (++x < image_width) // We're still within the same row of the image, so just move on to the next screen pixel.
++k;
else // We're starting a new row of the image.
{
x = 0; // Return to the leftmost column of the image.
++y; // Move one row downward in the image.
k = i + y*screen_width; // Verified correct.
}
}
if (found) // Complete match found.
break;
}
}
}
end:
// If found==false when execution reaches here, ErrorLevel is already set to the right value, so just
// clean up then return.
ReleaseDC(NULL, hdc);
if (!no_delete_bitmap)
DeleteObject(hbitmap_image);
if (sdc)
{
if (sdc_orig_select) // i.e. the original call to SelectObject() didn't fail.
SelectObject(sdc, sdc_orig_select); // Probably necessary to prevent memory leak.
DeleteDC(sdc);
}
if (hbitmap_screen)
DeleteObject(hbitmap_screen);
if (image_pixel)
free(image_pixel);
if (image_mask)
free(image_mask);
if (screen_pixel)
free(screen_pixel);
else // One of the GDI calls failed.
goto error;
if (!found) // Let ErrorLevel, which is either "1" or "2" as set earlier, tell the story.
return FAIL;
// Otherwise, success. Calculate xpos and ypos of where the match was found and adjust
// coords to make them relative to the position of the target window (rect will contain
// zeroes if this doesn't need to be done):
nRetPosX = (aLeft + i%screen_width) - origin.x;
nRetPosY = (aTop + i/screen_width) - origin.y;
return OK;
error:
return CRITICAL_ERROR;
}
LPCOLORREF CMacroLib::getbits(HBITMAP ahImage, HDC hdc, LONG &aWidth, LONG &aHeight, bool &aIs16Bit, int aMinColorDepth)
{
HDC tdc = CreateCompatibleDC(hdc);
if (!tdc)
return NULL;
// From this point on, "goto end" will assume tdc is non-NULL, but that the below
// might still be NULL. Therefore, all of the following must be initialized so that the "end"
// label can detect them:
HGDIOBJ tdc_orig_select = NULL;
LPCOLORREF image_pixel = NULL;
bool success = false;
// Confirmed:
// Needs extra memory to prevent buffer overflow due to: "A bottom-up DIB is specified by setting
// the height to a positive number, while a top-down DIB is specified by setting the height to a
// negative number. THE BITMAP COLOR TABLE WILL BE APPENDED to the BITMAPINFO structure."
// Maybe this applies only to negative height, in which case the second call to GetDIBits()
// below uses one.
struct BITMAPINFO3
{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[260]; // v1.0.40.10: 260 vs. 3 to allow room for color table when color depth is 8-bit or less.
} bmi;
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biBitCount = 0; // i.e. "query bitmap attributes" only.
if (!GetDIBits(tdc, ahImage, 0, 0, NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS)
|| bmi.bmiHeader.biBitCount < aMinColorDepth) // Relies on short-circuit boolean order.
goto end;
// Set output parameters for caller:
aIs16Bit = (bmi.bmiHeader.biBitCount == 16);
aWidth = bmi.bmiHeader.biWidth;
aHeight = bmi.bmiHeader.biHeight;
int image_pixel_count = aWidth * aHeight;
if ( !(image_pixel = (LPCOLORREF)malloc(image_pixel_count * sizeof(COLORREF))) )
goto end;
// v1.0.40.10: To preserve compatibility with callers who check for transparency in icons, don't do any
// of the extra color table handling for 1-bpp images. Update: For code simplification, support only
// 8-bpp images. If ever support lower color depths, use something like "bmi.bmiHeader.biBitCount > 1
// && bmi.bmiHeader.biBitCount < 9";
bool is_8bit = (bmi.bmiHeader.biBitCount == 8);
if (!is_8bit)
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biHeight = -bmi.bmiHeader.biHeight; // Storing a negative inside the bmiHeader struct is a signal for GetDIBits().
// Must be done only after GetDIBits() because: "The bitmap identified by the hbmp parameter
// must not be selected into a device context when the application calls GetDIBits()."
// (Although testing shows it works anyway, perhaps because GetDIBits() above is being
// called in its informational mode only).
// Note that this seems to return NULL sometimes even though everything still works.
// Perhaps that is normal.
tdc_orig_select = SelectObject(tdc, ahImage); // Returns NULL when we're called the second time?
// Apparently there is no need to specify DIB_PAL_COLORS below when color depth is 8-bit because
// DIB_RGB_COLORS also retrieves the color indices.
if ( !(GetDIBits(tdc, ahImage, 0, aHeight, image_pixel, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS)) )
goto end;
if (is_8bit) // This section added in v1.0.40.10.
{
// Convert the color indices to RGB colors by going through the array in reverse order.
// Reverse order allows an in-place conversion of each 8-bit color index to its corresponding
// 32-bit RGB color.
LPDWORD palette = (LPDWORD)_alloca(256 * sizeof(PALETTEENTRY));
GetSystemPaletteEntries(tdc, 0, 256, (LPPALETTEENTRY)palette); // Even if failure can realistically happen, consequences of using uninitialized palette seem acceptable.
// Above: GetSystemPaletteEntries() is the only approach that provided the correct palette.
// The following other approaches didn't give the right one:
// GetDIBits(): The palette it stores in bmi.bmiColors seems completely wrong.
// GetPaletteEntries()+GetCurrentObject(hdc, OBJ_PAL): Returned only 20 entries rather than the expected 256.
// GetDIBColorTable(): I think same as above or maybe it returns 0.
// The following section is necessary because apparently each new row in the region starts on
// a DWORD boundary. So if the number of pixels in each row isn't an exact multiple of 4, there
// are between 1 and 3 zero-bytes at the end of each row.
int remainder = aWidth % 4;
int empty_bytes_at_end_of_each_row = remainder ? (4 - remainder) : 0;
// Start at the last RGB slot and the last color index slot:
BYTE *byte = (BYTE *)image_pixel + image_pixel_count - 1 + (aHeight * empty_bytes_at_end_of_each_row); // Pointer to 8-bit color indices.
DWORD *pixel = image_pixel + image_pixel_count - 1; // Pointer to 32-bit RGB entries.
int row, col;
for (row = 0; row < aHeight; ++row) // For each row.
{
byte -= empty_bytes_at_end_of_each_row;
for (col = 0; col < aWidth; ++col) // For each column.
*pixel-- = rgb_to_bgr(palette[*byte--]); // Caller always wants RGB vs. BGR format.
}
}
// Since above didn't "goto end", indicate success:
success = true;
end:
if (tdc_orig_select) // i.e. the original call to SelectObject() didn't fail.
SelectObject(tdc, tdc_orig_select); // Probably necessary to prevent memory leak.
DeleteDC(tdc);
if (!success && image_pixel)
{
free(image_pixel);
image_pixel = NULL;
}
return image_pixel;
}
ResultType CMacroLib::ControlSend( CWnd* pWnd, LPTSTR aKeysToSend, LPTSTR aTitle, LPTSTR aText, LPTSTR aExcludeTitle, LPTSTR aExcludeText, bool aSendRaw )
{
ASSERT(pWnd);
CString strTmp = aKeysToSend;
m_strKeysToSend = (LPTSTR)(LPCTSTR)strTmp;
SendKeys(m_strKeysToSend, aSendRaw, SM_EVENT, pWnd->GetSafeHwnd());
return OK;
}
void CMacroLib::SendKeys( LPTSTR aKeys, bool aSendRaw, SendModes aSendModeOrig, HWND aTargetWindow /*= NULL*/ )
{
if (!*aKeys)
return;
// Might be better to do this prior to changing capslock state. UPDATE: In v1.0.44.03, the following section
// has been moved to the top of the function because:
// 1) For ControlSend, GetModifierLRState() might be more accurate if the threads are attached beforehand.
// 2) Determines sTargetKeybdLayout and sTargetLayoutHasAltGr early (for maintainability).
bool threads_are_attached = false; // Set default.
DWORD keybd_layout_thread = 0; //
DWORD target_thread; // Doesn't need init.
DWORD g_MainThreadID = GetCurrentThreadId();
if (aTargetWindow) // Caller has ensured this is NULL for SendInput and SendPlay modes.
{
if ((target_thread = GetWindowThreadProcessId(aTargetWindow, NULL)) // Assign.
&& target_thread != g_MainThreadID && !IsHungAppWindow(aTargetWindow))
{
threads_are_attached = AttachThreadInput(g_MainThreadID, target_thread, TRUE) != 0;
keybd_layout_thread = target_thread; // Testing shows that ControlSend benefits from the adapt-to-layout technique too.
}
//else no target thread, or it's our thread, or it's hung; so keep keybd_layout_thread at its default.
}
else
{
// v1.0.44.03: The following change is meaningful only to people who use more than one keyboard layout.
// It seems that the vast majority of them would want the Send command (as well as other features like
// Hotstrings and the Input command) to adapt to the keyboard layout of the active window (or target window
// in the case of ControlSend) rather than sticking with the script's own keyboard layout. In addition,
// testing shows that this adapt-to-layout method costs almost nothing in performance, especially since
// the active window, its thread, and its layout are retrieved only once for each Send rather than once
// for each keystroke.
HWND active_window;
if (active_window = GetForegroundWindow())
keybd_layout_thread = GetWindowThreadProcessId(active_window, NULL);
//else no foreground window, so keep keybd_layout_thread at default.
}
// Below is now called with "true" so that the hook's modifier state will be corrected (if necessary)
// prior to every send.
modLR_type mods_current = GetModifierLRState(true); // Current "logical" modifier state.
// Make a best guess of what the physical state of the keys is prior to starting (there's no way
// to be certain without the keyboard hook). Note: We only want those physical
// keys that are also logically down (it's possible for a key to be down physically
// but not logically such as when R-control, for example, is a suffix hotkey and the
// user is physically holding it down):
modLR_type mods_down_physically_orig, mods_down_physically_and_logically
, mods_down_physically_but_not_logically_orig;
mods_down_physically_orig = 0;
mods_down_physically_and_logically = mods_down_physically_orig;
mods_down_physically_but_not_logically_orig = 0; // There's no way of knowing, so assume none.
sModifiersLR_persistent &= mods_current & ~mods_down_physically_and_logically;
modLR_type persistent_modifiers_for_this_SendKeys, extra_persistent_modifiers_for_blind_mode;
extra_persistent_modifiers_for_blind_mode = 0;
persistent_modifiers_for_this_SendKeys = sModifiersLR_persistent;
sSendMode = aSendModeOrig;
vk_type vk;
sc_type sc;
modLR_type key_as_modifiersLR = 0;
modLR_type mods_for_next_key = 0;
// Above: For v1.0.35, it was changed to modLR vs. mod so that AltGr keys such as backslash and '{'
// are supported on layouts such as German when sending to apps such as Putty that are fussy about
// which ALT key is held down to produce the character.
vk_type this_event_modifier_down;
size_t key_text_length, key_name_length;
TCHAR *end_pos, *space_pos, *next_word, old_char, single_char_string[2];
KeyEventTypes event_type;
int repeat_count, click_x, click_y;
bool move_offset, key_down_is_persistent;
DWORD placeholder;
single_char_string[1] = '\0'; // Terminate in advance.
static vk_type sPrevEventModifierDown = 0;
LONG_OPERATION_INIT // Needed even for SendInput/Play.
for (; *aKeys; ++aKeys, sPrevEventModifierDown = this_event_modifier_down)
{
this_event_modifier_down = 0; // Set default for this iteration, overridden selectively below.
if (!sSendMode)
LONG_OPERATION_UPDATE_FOR_SENDKEYS // This does not measurably affect the performance of SendPlay/Event.
if (!aSendRaw && _tcschr(_T("^+!#{}"), *aKeys))
{
switch (*aKeys)
{
case '^':
if (!(persistent_modifiers_for_this_SendKeys & (MOD_LCONTROL|MOD_RCONTROL)))
mods_for_next_key |= MOD_LCONTROL;
// else don't add it, because the value of mods_for_next_key may also used to determine
// which keys to release after the key to which this modifier applies is sent.
// We don't want persistent modifiers to ever be released because that's how
// AutoIt2 behaves and it seems like a reasonable standard.
continue;
case '+':
if (!(persistent_modifiers_for_this_SendKeys & (MOD_LSHIFT|MOD_RSHIFT)))
mods_for_next_key |= MOD_LSHIFT;
continue;
case '!':
if (!(persistent_modifiers_for_this_SendKeys & (MOD_LALT|MOD_RALT)))
mods_for_next_key |= MOD_LALT;
continue;
case '#':
if (!(persistent_modifiers_for_this_SendKeys & (MOD_LWIN|MOD_RWIN)))
mods_for_next_key |= MOD_LWIN;
continue;
case '}': continue; // Important that these be ignored. Be very careful about changing this, see below.
case '{':
{
if ( !(end_pos = _tcschr(aKeys + 1, '}')) ) // Ignore it and due to rarity, don't reset mods_for_next_key.
continue; // This check is relied upon by some things below that assume a '}' is present prior to the terminator.
aKeys = omit_leading_whitespace(aKeys + 1); // v1.0.43: Skip leading whitespace inside the braces to be more flexible.
if ( !(key_text_length = end_pos - aKeys) )
{
if (end_pos[1] == '}')
{
// The literal string "{}}" has been encountered, which is interpreted as a single "}".
++end_pos;
key_text_length = 1;
}
else if (IS_SPACE_OR_TAB(end_pos[1])) // v1.0.48: Support "{} down}", "{} downtemp}" and "{} up}".
{
next_word = omit_leading_whitespace(end_pos + 1);
if ( !_tcsnicmp(next_word, _T("Down"), 4) // "Down" or "DownTemp" (or likely enough).
|| !_tcsnicmp(next_word, _T("Up"), 2) )
{
if ( !(end_pos = _tcschr(next_word, '}')) ) // See comments at similar section above.
continue;
key_text_length = end_pos - aKeys; // This result must be non-zero due to the checks above.
}
else
goto brace_case_end; // The loop's ++aKeys will now skip over the '}', ignoring it.
}
else // Empty braces {} were encountered (or all whitespace, but literal whitespace isn't sent).
goto brace_case_end; // The loop's ++aKeys will now skip over the '}', ignoring it.
}
if (!_tcsnicmp(aKeys, _T("Click"), 5))
{
*end_pos = '\0'; // Temporarily terminate the string here to omit the closing brace from consideration below.
ParseClickOptions(omit_leading_whitespace(aKeys + 5), click_x, click_y, vk
, event_type, repeat_count, move_offset);
*end_pos = '}'; // Undo temp termination.
if (repeat_count < 1) // Allow {Click 100, 100, 0} to do a mouse-move vs. click (but modifiers like ^{Click..} aren't supported in this case.
MouseMove(click_x, click_y, placeholder, 10, move_offset);
else // Use SendKey because it supports modifiers (e.g. ^{Click}) SendKey requires repeat_count>=1.
SendKey(vk, 0, mods_for_next_key, persistent_modifiers_for_this_SendKeys
, repeat_count, event_type, 0, aTargetWindow, click_x, click_y, move_offset);
goto brace_case_end; // This {} item completely handled, so move on to next.
}
else if (!_tcsnicmp(aKeys, _T("Raw"), 3)) // This is used by auto-replace hotstrings too.
{
// As documented, there's no way to switch back to non-raw mode afterward since there's no
// correct way to support special (non-literal) strings such as {Raw Off} while in raw mode.
aSendRaw = true;
goto brace_case_end; // This {} item completely handled, so move on to next.
}
// Since above didn't "goto", this item isn't {Click}.
event_type = KEYDOWNANDUP; // Set defaults.
repeat_count = 1; //
key_name_length = key_text_length; //
*end_pos = '\0'; // Temporarily terminate the string here to omit the closing brace from consideration below.
if (space_pos = StrChrAny(aKeys, _T(" \t"))) // Assign. Also, it relies on the fact that {} key names contain no spaces.
{
old_char = *space_pos;
*space_pos = '\0'; // Temporarily terminate here so that TextToVK() can properly resolve a single char.
key_name_length = space_pos - aKeys; // Override the default value set above.
next_word = omit_leading_whitespace(space_pos + 1);
UINT next_word_length = (UINT)(end_pos - next_word);
if (next_word_length > 0)
{
if (!_tcsnicmp(next_word, _T("Down"), 4))
{
event_type = KEYDOWN;
// v1.0.44.05: Added key_down_is_persistent (which is not initialized except here because
// it's only applicable when event_type==KEYDOWN). It avoids the following problem:
// When a key is remapped to become a modifier (such as F1::Control), launching one of
// the script's own hotkeys via F1 would lead to bad side-effects if that hotkey uses
// the Send command. This is because the Send command assumes that any modifiers pressed
// down by the script itself (such as Control) are intended to stay down during all
// keystrokes generated by that script. To work around this, something like KeyWait F1
// would otherwise be needed. within any hotkey triggered by the F1 key.
key_down_is_persistent = _tcsnicmp(next_word + 4, _T("Temp"), 4); // "DownTemp" means non-persistent.
}
else if (!_tcsicmp(next_word, _T("Up")))
event_type = KEYUP;
else
repeat_count = ATOI(next_word);
// Above: If negative or zero, that is handled further below.
// There is no complaint for values <1 to support scripts that want to conditionally send
// zero keystrokes, e.g. Send {a %Count%}
}
}
vk = TextToVK(aKeys, &mods_for_next_key, true, false); // false must be passed due to below.
sc = vk ? 0 : TextToSC(aKeys); // If sc is 0, it will be resolved by KeyEvent() later.
if (!vk && !sc && ctoupper(aKeys[0]) == 'V' && ctoupper(aKeys[1]) == 'K')
{
LPTSTR sc_string = StrChrAny(aKeys + 2, _T("Ss")); // Look for the "SC" that demarks the scan code.
if (sc_string && ctoupper(sc_string[1]) == 'C')
sc = (sc_type)_tcstol(sc_string + 2, NULL, 16); // Convert from hex.
// else leave sc set to zero and just get the specified VK. This supports Send {VKnn}.
vk = (vk_type)_tcstol(aKeys + 2, NULL, 16); // Convert from hex.
}
if (space_pos) // undo the temporary termination
*space_pos = old_char;
*end_pos = '}'; // undo the temporary termination
if (repeat_count < 1)
goto brace_case_end; // Gets rid of one level of indentation. Well worth it.
if (vk || sc)
{
if (key_as_modifiersLR = KeyToModifiersLR(vk, sc)) // Assign
{
if (!aTargetWindow)
{
if (event_type == KEYDOWN) // i.e. make {Shift down} have the same effect {ShiftDown}
{
this_event_modifier_down = vk;
if (key_down_is_persistent) // v1.0.44.05.
sModifiersLR_persistent |= key_as_modifiersLR;
persistent_modifiers_for_this_SendKeys |= key_as_modifiersLR; // v1.0.44.06: Added this line to fix the fact that "DownTemp" should keep the key pressed down after the send.
}
else if (event_type == KEYUP) // *not* KEYDOWNANDUP, since that would be an intentional activation of the Start Menu or menu bar.
{
sModifiersLR_persistent &= ~key_as_modifiersLR;
// By contrast with KEYDOWN, KEYUP should also remove this modifier
// from extra_persistent_modifiers_for_blind_mode if it happens to be
// in there. For example, if "#i::Send {LWin Up}" is a hotkey,
// LWin should become persistently up in every respect.
extra_persistent_modifiers_for_blind_mode &= ~key_as_modifiersLR;
// Fix for v1.0.43: Also remove LControl if this key happens to be AltGr.
// Since key_as_modifiersLR isn't 0, update to reflect any changes made above:
persistent_modifiers_for_this_SendKeys = sModifiersLR_persistent | extra_persistent_modifiers_for_blind_mode;
}
// else must never change sModifiersLR_persistent in response to KEYDOWNANDUP
// because that would break existing scripts. This is because that same
// modifier key may have been pushed down via {ShiftDown} rather than "{Shift Down}".
// In other words, {Shift} should never undo the effects of a prior {ShiftDown}
// or {Shift down}.
}
//else don't add this event to sModifiersLR_persistent because it will not be
// manifest via keybd_event. Instead, it will done via less intrusively
// (less interference with foreground window) via SetKeyboardState() and
// PostMessage(). This change is for ControlSend in v1.0.21 and has been
// documented.
}
// Below: sModifiersLR_persistent stays in effect (pressed down) even if the key
// being sent includes that same modifier. Surprisingly, this is how AutoIt2
// behaves also, which is good. Example: Send, {AltDown}!f ; this will cause
// Alt to still be down after the command is over, even though F is modified
// by Alt.
SendKey(vk, sc, mods_for_next_key, persistent_modifiers_for_this_SendKeys
, repeat_count, event_type, key_as_modifiersLR, aTargetWindow);
}
else if (key_name_length == 1) // No vk/sc means a char of length one is sent via special method.
{
// v1.0.40: SendKeySpecial sends only keybd_event keystrokes, not ControlSend style
// keystrokes.
// v1.0.43.07: Added check of event_type!=KEYUP, which causes something like Send {ð up} to
// do nothing if the curr. keyboard layout lacks such a key. This is relied upon by remappings
// such as F1::ð (i.e. a destination key that doesn't have a VK, at least in English).
if (event_type != KEYUP) // In this mode, mods_for_next_key and event_type are ignored due to being unsupported.
{
if (aTargetWindow)
// Although MSDN says WM_CHAR uses UTF-16, it seems to really do automatic
// translation between ANSI and UTF-16; we rely on this for correct results:
PostMessage(aTargetWindow, WM_CHAR, aKeys[0], 0);
else
SendKeySpecial(aKeys[0], repeat_count);
}
}
// See comment "else must never change sModifiersLR_persistent" above about why
// !aTargetWindow is used below:
else if (vk = TextToSpecial(aKeys, key_text_length, event_type
, persistent_modifiers_for_this_SendKeys, !aTargetWindow)) // Assign.
{
if (!aTargetWindow)
{
if (event_type == KEYDOWN)
this_event_modifier_down = vk;
//else // It must be KEYUP because TextToSpecial() never returns KEYDOWNANDUP.
//DisguiseWinAltIfNeeded(vk);
}
// Since we're here, repeat_count > 0.
// v1.0.42.04: A previous call to SendKey() or SendKeySpecial() might have left modifiers
// in the wrong state (e.g. Send +{F1}{ControlDown}). Since modifiers can sometimes affect
// each other, make sure they're in the state intended by the user before beginning:
SetModifierLRState(persistent_modifiers_for_this_SendKeys
, sSendMode ? sEventModifiersLR : GetModifierLRState()
, aTargetWindow, false, false); // It also does DoKeyDelay(g->PressDuration).
for (int i = 0; i < repeat_count; ++i)
{
// Don't tell it to save & restore modifiers because special keys like this one
// should have maximum flexibility (i.e. nothing extra should be done so that the
// user can have more control):
KeyEvent(event_type, vk, 0, aTargetWindow, true);
if (!sSendMode)
LONG_OPERATION_UPDATE_FOR_SENDKEYS
}
}
else if (key_text_length > 4 && !_tcsnicmp(aKeys, _T("ASC "), 4) && !aTargetWindow) // {ASC nnnnn}
{
// Include the trailing space in "ASC " to increase uniqueness (selectivity).
// Also, sending the ASC sequence to window doesn't work, so don't even try:
SendASC(omit_leading_whitespace(aKeys + 3));
// Do this only once at the end of the sequence:
DoKeyDelay(); // It knows not to do the delay for SM_INPUT.
}
else if (key_text_length > 2 && !_tcsnicmp(aKeys, _T("U+"), 2))
{
// L24: Send a unicode value as shown by Character Map.
wchar_t u_code = (wchar_t) _tcstol(aKeys + 2, NULL, 16);
if (aTargetWindow)
{
// Although MSDN says WM_CHAR uses UTF-16, PostMessageA appears to truncate it to 8-bit.
// This probably means it does automatic translation between ANSI and UTF-16. Since we
// specifically want to send a Unicode character value, use PostMessageW:
PostMessageW(aTargetWindow, WM_CHAR, u_code, 0);
}
else
{
// Use SendInput in unicode mode if available, otherwise fall back to SendASC.
// To know why the following requires sSendMode != SM_PLAY, see SendUnicodeChar.
if (sSendMode != SM_PLAY && g_os.IsWin2000orLater())
{
SendUnicodeChar(u_code, mods_for_next_key | persistent_modifiers_for_this_SendKeys);
}
else // Note that this method generally won't work with Unicode characters except
{ // with specific controls which support it, such as RichEdit (tested on WordPad).
TCHAR asc[8];
*asc = '0';
_itot(u_code, asc + 1, 10);
SendASC(asc);
}
}
DoKeyDelay();
}
//else do nothing since it isn't recognized as any of the above "else if" cases (see below).
// If what's between {} is unrecognized, such as {Bogus}, it's safest not to send
// the contents literally since that's almost certainly not what the user intended.
// In addition, reset the modifiers, since they were intended to apply only to
// the key inside {}. Also, the below is done even if repeat-count is zero.
brace_case_end: // This label is used to simplify the code without sacrificing performance.
aKeys = end_pos; // In prep for aKeys++ done by the loop.
mods_for_next_key = 0;
continue;
} // case '{'
} // switch()
} // if (!aSendRaw && strchr("^+!#{}", *aKeys))
else // Encountered a character other than ^+!#{} ... or we're in raw mode.
{
// Best to call this separately, rather than as first arg in SendKey, since it changes the
// value of modifiers and the updated value is *not* guaranteed to be passed.
// In other words, SendKey(TextToVK(...), modifiers, ...) would often send the old
// value for modifiers.
single_char_string[0] = *aKeys; // String was pre-terminated earlier.
if (vk = TextToVK(single_char_string, &mods_for_next_key, true, true))
// TextToVK() takes no measurable time compared to the amount of time SendKey takes.
SendKey(vk, 0, mods_for_next_key, persistent_modifiers_for_this_SendKeys, 1, KEYDOWNANDUP
, 0, aTargetWindow);
else // Try to send it by alternate means.
{
// In this mode, mods_for_next_key is ignored due to being unsupported.
if (aTargetWindow)
// Although MSDN says WM_CHAR uses UTF-16, it seems to really do automatic
// translation between ANSI and UTF-16; we rely on this for correct results:
PostMessage(aTargetWindow, WM_CHAR, *aKeys, 0);
else
SendKeySpecial(*aKeys, 1);
}
mods_for_next_key = 0; // Safest to reset this regardless of whether a key was sent.
}
} // for()
modLR_type mods_to_set;
if (sSendMode)
{
int final_key_delay = -1; // Set default.
/*
if (!sAbortArraySend && sEventCount > 0) // Check for zero events for performance, but more importantly because playback hook will not operate correctly with zero.
{
// Add more events to the array (prior to sending) to support the following:
// Restore the modifiers to match those the user is physically holding down, but do it as *part*
// of the single SendInput/Play call. The reasons it's done here as part of the array are:
// 1) It avoids the need for #HotkeyModifierTimeout (and it's superior to it) for both SendInput
// and SendPlay.
// 2) The hook will not be present during the SendInput, nor can it be reinstalled in time to
// catch any physical events generated by the user during the Send. Consequently, there is no
// known way to reliably detect physical keystate changes.
// 3) Changes made to modifier state by SendPlay are seen only by the active window's thread.
// Thus, it would be inconsistent and possibly incorrect to adjust global modifier state
// after (or during) a SendPlay.
// So rather than resorting to #HotkeyModifierTimeout, we can restore the modifiers within the
// protection of SendInput/Play's uninterruptibility, allowing the user's buffered keystrokes
// (if any) to hit against the correct modifier state when the SendInput/Play completes.
// For example, if #c:: is a hotkey and the user releases Win during the SendInput/Play, that
// release would hit after SendInput/Play restores Win to the down position, and thus Win would
// not be stuck down. Furthermore, if the user didn't release Win, Win would be in the
// correct/intended position.
// This approach has a few weaknesses (but the strengths appear to outweigh them):
// 1) Hitting SendInput's 5000 char limit would omit the tail-end keystrokes, which would mess up
// all the assumptions here. But hitting that limit should be very rare, especially since it's
// documented and thus scripts will avoid it.
// 2) SendInput's assumed uninterruptibility is false if any other app or script has an LL hook
// installed. This too is documented, so scripts should generally avoid using SendInput when
// they know there are other LL hooks in the system. In any case, there's no known solution
// for it, so nothing can be done.
mods_to_set = persistent_modifiers_for_this_SendKeys
| (sInBlindMode ? 0 : (mods_down_physically_orig & ~mods_down_physically_but_not_logically_orig)); // The last item is usually 0.
// Above: When in blind mode, don't restore physical modifiers. This is done to allow a hotkey
// such as the following to release Shift:
// +space::SendInput/Play {Blind}{Shift up}
// Note that SendPlay can make such a change only from the POV of the target window; i.e. it can
// release shift as seen by the target window, but not by any other thread; so the shift key would
// still be considered to be down for the purpose of firing hotkeys (it can't change global key state
// as seen by GetAsyncKeyState).
// For more explanation of above, see a similar section for the non-array/old Send below.
SetModifierLRState(mods_to_set, sEventModifiersLR, NULL, true, true); // Disguise in case user released or pressed Win/Alt during the Send (seems best to do it even for SendPlay, though it probably needs only Alt, not Win).
// mods_to_set is used further below as the set of modifiers that were explicitly put into effect at the tail end of SendInput.
SendEventArray(final_key_delay, mods_to_set);
}
CleanupEventArray(final_key_delay);
*/
}
else // A non-array send is in effect, so a more elaborate adjustment to logical modifiers is called for.
{
// Determine (or use best-guess, if necessary) which modifiers are down physically now as opposed
// to right before the Send began.
modLR_type mods_down_physically = 0; // As compared to mods_down_physically_orig.
// Restore the state of the modifiers to be those the user is physically holding down right now.
// Any modifiers that are logically "persistent", as detected upon entrance to this function
// (e.g. due to something such as a prior "Send, {LWinDown}"), are also pushed down if they're not already.
// Don't press back down the modifiers that were used to trigger this hotkey if there's
// any doubt that they're still down, since doing so when they're not physically down
// would cause them to be stuck down, which might cause unwanted behavior when the unsuspecting
// user resumes typing.
// v1.0.42.04: Now that SendKey() is lazy about releasing Ctrl and/or Shift (but not Win/Alt),
// the section below also releases Ctrl/Shift if appropriate. See SendKey() for more details.
mods_to_set = persistent_modifiers_for_this_SendKeys; // Set default.
if (sInBlindMode) // This section is not needed for the array-sending modes because they exploit uninterruptibility to perform a more reliable restoration.
{
// At the end of a blind-mode send, modifiers are restored differently than normal. One
// reason for this is to support the explicit ability for a Send to turn off a hotkey's
// modifiers even if the user is still physically holding them down. For example:
// #space::Send {LWin up} ; Fails to release it, by design and for backward compatibility.
// #space::Send {Blind}{LWin up} ; Succeeds, allowing LWin to be logically up even though it's physically down.
modLR_type mods_changed_physically_during_send = mods_down_physically_orig ^ mods_down_physically;
// Fix for v1.0.42.04: To prevent keys from getting stuck down, compensate for any modifiers
// the user physically pressed or released during the Send (especially those released).
// Remove any modifiers physically released during the send so that they don't get pushed back down:
mods_to_set &= ~(mods_changed_physically_during_send & mods_down_physically_orig); // Remove those that changed from down to up.