-
Notifications
You must be signed in to change notification settings - Fork 901
/
hotkey.cpp
2792 lines (2538 loc) · 128 KB
/
hotkey.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" // pre-compiled headers
#include "hotkey.h"
#include "globaldata.h" // For g_os and other global vars.
#include "window.h" // For MsgBox()
//#include "application.h" // For ExitApp()
#include "script_func_impl.h"
// Initialize static members:
HookType Hotkey::sWhichHookNeeded = 0;
HookType Hotkey::sWhichHookAlways = 0;
DWORD Hotkey::sTimePrev = {0};
DWORD Hotkey::sTimeNow = {0};
Hotkey **Hotkey::shk = NULL;
int Hotkey::shkMax = 0;
HotkeyIDType Hotkey::sNextID = 0;
const HotkeyIDType &Hotkey::sHotkeyCount = Hotkey::sNextID;
bool Hotkey::sJoystickHasHotkeys[MAX_JOYSTICKS] = {false};
DWORD Hotkey::sJoyHotkeyCount = 0;
HWND HotCriterionAllowsFiring(HotkeyCriterion *aCriterion, LPTSTR aHotkeyName)
// This is a global function because it's used by both hotkeys and hotstrings.
// In addition to being called by the hook thread, this can now be called by the main thread.
// That happens when a WM_HOTKEY message arrives (for non-hook hotkeys, i.e. RegisterHotkey).
// Returns a non-NULL HWND if firing is allowed. However, if it's a global criterion or
// a "not-criterion" such as #HotIf Not WinActive(), (HWND)1 is returned rather than a genuine HWND.
{
HWND found_hwnd;
if (!aCriterion)
return (HWND)1; // Always allow hotkey to fire.
switch(aCriterion->Type)
{
case HOT_IF_ACTIVE:
case HOT_IF_NOT_ACTIVE:
found_hwnd = WinActive(g_default, aCriterion->WinTitle, aCriterion->WinText, _T(""), _T(""), false); // Thread-safe.
break;
case HOT_IF_EXIST:
case HOT_IF_NOT_EXIST:
found_hwnd = WinExist(g_default, aCriterion->WinTitle, aCriterion->WinText, _T(""), _T(""), false, false); // Thread-safe.
break;
// L4: Handling of #HotIf (expression) hotkey variants.
case HOT_IF_CALLBACK:
// Expression evaluation must be done in the main thread. If the message times out, the hotkey/hotstring is not allowed to fire.
DWORD_PTR res;
return (SendMessageTimeout(g_hWnd, AHK_HOT_IF_EVAL, (WPARAM)aCriterion, (LPARAM)aHotkeyName, SMTO_BLOCK | SMTO_ABORTIFHUNG, g_HotExprTimeout, &res) && res == CONDITION_TRUE) ? (HWND)1 : NULL;
}
return (aCriterion->Type == HOT_IF_ACTIVE || aCriterion->Type == HOT_IF_EXIST) ? found_hwnd : (HWND)!found_hwnd;
}
FResult SetHotkeyCriterion(HotCriterionType aType, LPCTSTR aWinTitle, LPCTSTR aWinText)
// Returns FR_FAIL if memory couldn't be allocated (and an error was raised), or OK otherwise.
// This is a global function because it's used by both hotkeys and hotstrings.
{
HotkeyCriterion *cp = nullptr;
if ( (*aWinTitle || *aWinText)
&& !(cp = FindHotkeyCriterion(aType, aWinTitle, aWinText))
&& !(cp = AddHotkeyCriterion(aType, aWinTitle, aWinText)) )
return FR_FAIL;
g->HotCriterion = cp;
return OK;
}
HotkeyCriterion *FindHotkeyCriterion(HotCriterionType aType, LPCTSTR aWinTitle, LPCTSTR aWinText)
{
// Storing combinations of WinTitle+WinText doesn't have as good a best-case memory savings as
// have a separate linked list for Title vs. Text. But it does save code size, and in the vast
// majority of scripts, the memory savings would be insignificant.
HotkeyCriterion *cp;
for (cp = g_FirstHotCriterion; cp; cp = cp->NextCriterion)
if (cp->Type == aType && !_tcscmp(cp->WinTitle, aWinTitle) && !_tcscmp(cp->WinText, aWinText)) // Case insensitive.
return cp;
return NULL;
}
HotkeyCriterion *AddHotkeyCriterion(HotCriterionType aType, LPCTSTR aWinTitle, LPCTSTR aWinText)
{
HotkeyCriterion *cp;
cp = SimpleHeap::Alloc<HotkeyCriterion>();
cp->Type = aType;
cp->OriginalExpr = nullptr;
if (*aWinTitle)
{
if ( !(cp->WinTitle = SimpleHeap::Malloc(aWinTitle)) )
return NULL;
}
else
cp->WinTitle = _T("");
if (*aWinText)
{
if ( !(cp->WinText = SimpleHeap::Malloc(aWinText)) )
return NULL;
}
else
cp->WinText = _T("");
return AddHotkeyCriterion(cp);
}
HotkeyCriterion *AddHotkeyCriterion(HotkeyCriterion *cp)
{
cp->NextCriterion = NULL;
if (!g_FirstHotCriterion)
g_FirstHotCriterion = g_LastHotCriterion = cp;
else
{
g_LastHotCriterion->NextCriterion = cp;
// This must be done after the above:
g_LastHotCriterion = cp;
}
return cp;
}
HotkeyCriterion *AddHotkeyIfExpr()
{
HotkeyCriterion *cp = SimpleHeap::Alloc<HotkeyCriterion>();
cp->NextExpr = NULL;
cp->OriginalExpr = nullptr;
if (g_LastHotExpr)
g_LastHotExpr->NextExpr = cp;
else
g_FirstHotExpr = cp;
g_LastHotExpr = cp;
return cp;
}
HotkeyCriterion *FindHotkeyIfExpr(LPCTSTR aExpr)
{
for (HotkeyCriterion* cp = g_FirstHotExpr; cp; cp = cp->NextExpr)
if (cp->OriginalExpr && !_tcscmp(aExpr, cp->OriginalExpr)) // Case-sensitive since the expression might be.
return cp;
return NULL;
}
void Script::PreparseHotkeyIfExpr(Line* aLine)
// Optimize simple #HotIf expressions into the more specific HOT_IF_ types so that they can be
// evaluated by the hook directly, without synchronizing with the main thread.
{
ExprTokenType *postfix = aLine->mArg[0].postfix;
if (postfix[0].symbol != SYM_OBJECT)
return;
auto fn = dynamic_cast<BuiltInFunc*>(postfix[0].object);
if (!fn || fn->mBIF != &BIF_WinExistActive)
return; // Not WinExist() or WinActive().
++postfix;
int param_count = 0;
while (postfix[param_count].symbol == SYM_STRING)
++param_count;
if (postfix[param_count].symbol != SYM_FUNC // Not a function call, or it doesn't only accept strings.
|| param_count > 2) // Too many parameters.
return;
bool invert = postfix[param_count+1].symbol == SYM_LOWNOT || postfix[param_count+1].symbol == SYM_HIGHNOT;
if (postfix[param_count+1+invert].symbol != SYM_INVALID)
return; // There's more to the expression.
// Otherwise, it was a single call to WinExist() or WinActive() where each parameter
// was exactly one literal string and the result was optionally inverted with "not" or "!".
HotkeyCriterion *hc = (HotkeyCriterion *)aLine->mAttribute;
// Change the parameters of this criterion. FindHotkeyIfExpr() will still be able to
// find it based on the expression text since it only relies on ExprLine.
if (ctoupper(fn->mName[3]) == 'A')
hc->Type = invert ? HOT_IF_NOT_ACTIVE : HOT_IF_ACTIVE;
else
hc->Type = invert ? HOT_IF_NOT_EXIST : HOT_IF_EXIST;
hc->WinTitle = param_count > 0 ? postfix[0].marker : _T("");
hc->WinText = param_count > 1 ? postfix[1].marker : _T("");
// The following adds a duplicate in the event that there are two different expressions
// which resolve to the same criterion, such as WinExist("x","") and WinExist("x", "").
// In that case, only the first criterion can be referenced by its WinTitle & WinText
// (but each can be referenced by its unique expression text). This seems unavoidable
// since variants are only unique to a given expression, and trying to work around that
// here would cause inconsistency since this only applies to very specific expressions.
// At this stage, the only criterion in the list are those added by the following line:
AddHotkeyCriterion(hc);
}
void Hotkey::ManifestAllHotkeysHotstringsHooks()
// This function examines all hotkeys and hotstrings to determine:
// - Which hotkeys to register/unregister, or activate/deactivate in the hook.
// - Which hotkeys to be changed from HK_NORMAL to HK_KEYBD_HOOK (or vice versa).
// - In pursuit of the above, also assess the interdependencies between hotkeys: the presence or
// absence of a given hotkey can sometimes impact whether other hotkeys need to be converted from
// HK_NORMAL to HK_KEYBD_HOOK. For example, a newly added/enabled global hotkey variant can
// cause a HK_KEYBD_HOOK hotkey to become HK_NORMAL, and the converse is also true.
// - Based on the above, decide whether the keyboard and/or mouse hooks need to be (de)activated.
{
// v1.0.37.05: A prefix key such as "a" in "a & b" should cause any use of "a" as a suffix
// (such as ^!a) also to be a hook hotkey. Otherwise, the ^!a hotkey won't fire because the
// hook prevents the OS's hotkey monitor from seeing that the hotkey was pressed. NOTE:
// This is done only for virtual keys because prefix keys that are done by scan code (mModifierSC)
// should already be hook hotkeys when used as suffix keys (there may be a few unusual exceptions,
// but they seem too rare to justify the extra code size).
// Update for v1.0.40: This first pass through the hotkeys now also checks things for hotkeys
// that can affect other hotkeys. If this weren't done in the first pass, it might be possible
// for a hotkey to make some other hotkey into a hook hotkey, but then the hook might not be
// installed if that hotkey had already been processed earlier in the second pass. Similarly,
// a hotkey processed earlier in the second pass might have been registered when in fact it
// should have become a hook hotkey due to something learned only later in the second pass.
// Doing these types of things in the first pass resolves such situations.
// Update for v1.1.27: Doing the above in the first pass doesn't work correctly, as mType is
// reset to default during the first pass (even if a previous iteration might has set it to
// HK_KEYBD_HOOK, such as when it is eclipsed by a wildcard hotkey). One workaround would
// be to set mKeybdHookMandatory = true, but that would prevent the hotkey from reverting to
// HK_NORMAL when it no longer needs the hook. Instead, there are now three passes.
bool vk_is_prefix[VK_ARRAY_COUNT] = {false};
bool *hk_is_inactive = (bool *)_alloca(sHotkeyCount * sizeof(bool)); // No init needed. Currently limited to around 16k (HOTKEY_ID_MAX).
HotkeyVariant *vp;
int i, j;
// FIRST PASS THROUGH THE HOTKEYS:
for (i = 0; i < sHotkeyCount; ++i)
{
Hotkey &hot = *shk[i]; // For performance and convenience.
if ( hk_is_inactive[i] = ((g_IsSuspended && !hot.IsExemptFromSuspend())
|| hot.IsCompletelyDisabled()) ) // Listed last for short-circuit performance.
{
// In the cases above, nothing later below can change the fact that this hotkey should
// now be in an unregistered state.
if (hot.mIsRegistered)
{
hot.Unregister();
// In case the hotkey's thread is already running, it seems best to cancel any repeat-run
// that has already been scheduled. Older comment: CT_SUSPEND, at least, relies on us to do this.
for (vp = hot.mFirstVariant; vp; vp = vp->mNextVariant)
vp->mRunAgainAfterFinished = false; // Applies to all hotkey types, not just registered ones.
}
continue;
}
// Otherwise, this hotkey will be in effect, so check its attributes.
if (hot.mKeybdHookMandatory)
{
// v1.0.44: The following is relied upon by some things like the Hotkey constructor and the tilde prefix
// (the latter can set mKeybdHookMandatory for a hotkey sometime after the first variant is added [such
// as for a subsequent variant]). This practice also improves maintainability.
if (HK_TYPE_CAN_BECOME_KEYBD_HOOK(hot.mType)) // To ensure it hasn't since become a joystick/mouse/mouse-and-keyboard hotkey.
hot.mType = HK_KEYBD_HOOK;
}
else // Hook isn't mandatory, so set any non-mouse/joystick/both hotkey to normal for possibly overriding later below.
{
// v1.0.42: The following is done to support situations in which a hotkey can be a hook hotkey sometimes,
// but after a (de)suspend or after a change to other hotkeys via the Hotkey command, might no longer
// require the hook. Examples include:
// 1) A hotkey can't be registered because some other app is using it, but later
// that condition changes.
// 2) Suspend or the Hotkey command changes wildcard hotkeys so that non-wildcard
// hotkeys that have the same suffix are no longer eclipsed, and thus don't need
// the hook. The same type of thing can happen if a key-up hotkey is disabled,
// which would allow it's key-down hotkey to become non-hook. Similarly, if a
// all of a prefix key's hotkeys become disabled, and that prefix is also a suffix,
// those suffixes no longer need to be hook hotkeys.
// 3) There may be other ways, especially in the future involving #HotIf WinActive/Exist
// keys whose criteria change.
if (hot.mType == HK_KEYBD_HOOK)
hot.mType = HK_NORMAL; // To possibly be overridden back to HK_KEYBD_HOOK later below; but if not, it will be registered later below.
}
if (hot.mModifierVK)
vk_is_prefix[hot.mModifierVK] = true;
} // End of first pass loop.
// SECOND PASS THROUGH THE HOTKEYS:
// Check for hotkeys that can affect other hotkeys, such as wildcard or key-up hotkeys.
// This is separate to the other passes for reasons described at the top of the function.
for (i = 0; i < sHotkeyCount; ++i)
{
if (hk_is_inactive[i])
continue;
Hotkey &hot = *shk[i]; // For performance and convenience.
if (hot.mKeyUp && hot.mVK) // No need to do the below for mSC hotkeys since their down hotkeys would already be handled by the hook.
{
// For each key-up hotkey, search for any its counterpart that's a down-hotkey (if any).
// Such a hotkey should also be handled by the hook because if registered, such as
// "#5" (reg) and "#5 up" (hook), the hook would suppress the down event because it
// is unaware that down-hotkey exists (it's suppressed to prevent the key from being
// stuck in a logically down state).
for (j = 0; j < sHotkeyCount; ++j)
{
// No need to check the following because they are already hook hotkeys:
// mModifierVK/SC
// mAllowExtraModifiers
// mNoSuppress
// In addition, there is no need to check shk[j]->mKeyUp because that can't be
// true if it's mType is HK_NORMAL:
// Also, g_IsSuspended and IsCompletelyDisabled() aren't checked
// because it's harmless to operate on disabled hotkeys in this way.
if (shk[j]->mVK == hot.mVK && HK_TYPE_CAN_BECOME_KEYBD_HOOK(shk[j]->mType) // Ordered for short-circuit performance.
&& shk[j]->mModifiersConsolidatedLR == hot.mModifiersConsolidatedLR)
{
shk[j]->mType = HK_KEYBD_HOOK;
// And if it's currently registered, it will be unregistered later below.
}
}
}
// v1.0.40: If this is a wildcard hotkey, any hotkeys it eclipses (i.e. includes as subsets)
// should be made into hook hotkeys also, because otherwise they would be overridden by hook.
// The following criteria are checked:
// 1) Exclude those that have a ModifierSC/VK because in those cases, mAllowExtraModifiers is
// ignored.
// 2) Exclude those that lack an mVK because those with mSC can't eclipse registered hotkeys
// (since any would-be eclipsed mSC hotkey is already a hook hotkey due to is SC nature).
// 3) It must not have any mModifiersLR because such hotkeys can't completely eclipse
// registered hotkeys since they always have neutral vs. left/right-specific modifiers.
// For example, if *<^a is a hotkey, ^a can still be a registered hotkey because it could
// still be activated by pressing RControl+a.
// 4) For maintainability, it doesn't check mNoSuppress because the hook is needed anyway,
// so might as well handle eclipsed hotkeys with it too.
if (hot.mAllowExtraModifiers && hot.mVK && !hot.mModifiersLR && !(hot.mModifierSC || hot.mModifierVK))
{
for (j = 0; j < sHotkeyCount; ++j)
{
// If it's not of type HK_NORMAL, there's no need to change its type regardless
// of the values of its other members. Also, if the wildcard hotkey (hot) has
// any neutral modifiers, this hotkey must have at least those neutral modifiers
// too or else it's not eclipsed (and thus registering it is okay). In other words,
// the wildcard hotkey's neutral modifiers must be a perfect subset of this hotkey's
// modifiers for this one to be eclipsed by it. Note: Neither mModifiersLR nor
// mModifiersConsolidated is checked for simplicity and also because it seems to add
// flexibility. For example, *<^>^a would require both left AND right ctrl to be down,
// not EITHER. In other words, mModifiersLR can never in effect contain a neutral modifier.
if (shk[j]->mVK == hot.mVK && HK_TYPE_CAN_BECOME_KEYBD_HOOK(shk[j]->mType) // Ordered for short-circuit performance.
&& (hot.mModifiers & shk[j]->mModifiers) == hot.mModifiers)
{
// Note: No need to check mModifiersLR because it would already be a hook hotkey in that case;
// that is, the check of shk[j]->mType precludes it. It also precludes the possibility
// of shk[j] being a key-up hotkey, wildcard hotkey, etc.
shk[j]->mType = HK_KEYBD_HOOK;
// And if it's currently registered, it will be unregistered later below.
}
}
}
} // End of second pass loop.
// THIRD PASS THROUGH THE HOTKEYS:
// v1.0.42: Reset sWhichHookNeeded because it's now possible that the hook was on before but no longer
// needed due to changing of a hotkey from hook to registered (for various reasons described above):
sWhichHookNeeded = 0;
for (i = 0; i < sHotkeyCount; ++i)
{
if (hk_is_inactive[i])
continue; // v1.0.40: Treat disabled hotkeys as though they're not even present.
Hotkey &hot = *shk[i]; // For performance and convenience.
// HK_MOUSE_HOOK hotkeys, and most HK_KEYBD_HOOK hotkeys, are handled by the hotkey constructor.
// What we do here upgrade any NORMAL/registered hotkey to HK_KEYBD_HOOK if there are other
// hotkeys that interact or overlap with it in such a way that the hook is preferred.
// This evaluation is done here because only now that hotkeys are about to be activated do
// we know which ones are disabled or suspended, and thus don't need to be taken into account.
if (HK_TYPE_CAN_BECOME_KEYBD_HOOK(hot.mType))
{
if (vk_is_prefix[hot.mVK])
// If it's a suffix that is also used as a prefix, use hook (this allows ^!a to work without $ when "a & b" is a hotkey).
// v1.0.42: This was fixed so that mVK_WasSpecifiedByNumber dosn't affect it. That is, a suffix that's
// also used as a prefix should become a hook hotkey even if the suffix is specified as "vkNNN::".
hot.mType = HK_KEYBD_HOOK;
// And if it's currently registered, it will be unregistered later below.
else
{
// v1.0.42: Any #HotIf keyboard hotkey must use the hook if it lacks an enabled,
// non-suspended, global variant. Under those conditions, the hotkey is either:
// 1) Single-variant hotkey that has criteria (non-global).
// 2) Multi-variant hotkey but all variants have criteria (non-global).
// 3) A hotkey with a non-suppressed (~) variant (always, for code simplicity): already handled by AddVariant().
// In both cases above, the hook must handle the hotkey because there can be
// situations in which the hook should let the hotkey's keystroke pass through
// to the active window (i.e. the hook is needed to dynamically disable the hotkey).
// mHookAction isn't checked here since those hotkeys shouldn't reach this stage (since they're always hook hotkeys).
for (hot.mType = HK_KEYBD_HOOK, vp = hot.mFirstVariant; vp; vp = vp->mNextVariant)
{
if ( !vp->mHotCriterion && vp->mEnabled // It's a global variant (no criteria) and it's enabled...
&& (!g_IsSuspended || vp->mSuspendExempt) )
// ... and this variant isn't suspended (we already know IsCompletelyDisabled()==false from an earlier check).
{
hot.mType = HK_NORMAL; // Reset back to how it was before this loop started. Hook not needed.
break;
}
}
// If the above promoted it from NORMAL to HOOK but the hotkey is currently registered,
// it will be unregistered later below.
}
}
// Check if this mouse hotkey also requires the keyboard hook (e.g. #LButton).
// Some mouse hotkeys, such as those with normal modifiers, don't require it
// since the mouse hook has logic to handle that situation. But those that
// are composite hotkeys such as "RButton & Space" or "Space & RButton" need
// the keyboard hook:
if (hot.mType == HK_MOUSE_HOOK && (
hot.mModifierSC || hot.mSC // i.e. since it's an SC, the modifying key isn't a mouse button.
|| hot.mHookAction // v1.0.25.05: At least some alt-tab actions require the keyboard hook. For example, a script consisting only of "MButton::AltTabAndMenu" would not work properly otherwise.
// v1.0.25.05: The line below was added to prevent the Start Menu from appearing, which
// requires the keyboard hook. ALT hotkeys don't need it because the mouse hook sends
// a CTRL keystroke to disguise them, a trick that is unfortunately not reliable for
// when it happens while the while key is down (though it does disguise a Win-up).
|| ((hot.mModifiersConsolidatedLR & (MOD_LWIN|MOD_RWIN)) && !(hot.mModifiersConsolidatedLR & (MOD_LALT|MOD_RALT)))
// For v1.0.30, above has been expanded to include Win+Shift and Win+Control modifiers.
|| (hot.mVK && !IsMouseVK(hot.mVK)) // e.g. "RButton & Space"
|| (hot.mModifierVK && !IsMouseVK(hot.mModifierVK))) ) // e.g. "Space & RButton"
hot.mType = HK_BOTH_HOOKS; // Needed by ChangeHookState().
// For the above, the following types of mouse hotkeys do not need the keyboard hook:
// 1) mAllowExtraModifiers: Already handled since the mouse hook fetches the modifier state
// manually when the keyboard hook isn't installed.
// 2) mModifiersConsolidatedLR (i.e. the mouse button is modified by a normal modifier
// such as CTRL): Same reason as #1.
// 3) As a subset of #2, mouse hotkeys that use WIN as a modifier will not have the
// Start Menu suppressed unless the keyboard hook is installed. It's debatable,
// but that seems a small price to pay (esp. given how rare it is just to have
// the mouse hook with no keyboard hook) to avoid the overhead of the keyboard hook.
// If the hotkey is normal, try to register it. If the register fails, use the hook to try
// to override any other script or program that might have it registered (as documented):
if (hot.mType == HK_NORMAL)
{
if (!hot.Register()) // Can't register it, usually due to some other application or the OS using it.
hot.mType = HK_KEYBD_HOOK;
}
else // mType isn't NORMAL (possibly due to something above changing it), so ensure it isn't registered.
if (hot.mIsRegistered) // Improves typical performance since this hotkey could be mouse, joystick, etc.
// Although the hook effectively overrides registered hotkeys, they should be unregistered anyway
// to prevent the Send command from triggering the hotkey, and perhaps other side-effects.
hot.Unregister();
switch (hot.mType)
{
case HK_KEYBD_HOOK: sWhichHookNeeded |= HOOK_KEYBD; break;
case HK_MOUSE_HOOK: sWhichHookNeeded |= HOOK_MOUSE; break;
case HK_BOTH_HOOKS: sWhichHookNeeded |= HOOK_KEYBD|HOOK_MOUSE; break;
}
} // for()
// Check if anything else requires the hook.
// But do this part outside of the above block because these values may have changed since
// this function was first called. By design, the Num/Scroll/CapsLock AlwaysOn/Off setting
// stays in effect even when Suspend in ON.
if ( Hotstring::sEnabledCount
|| g_input // v1.0.91: Hook is needed for collecting input.
|| !(g_ForceNumLock == NEUTRAL && g_ForceCapsLock == NEUTRAL && g_ForceScrollLock == NEUTRAL) )
sWhichHookNeeded |= HOOK_KEYBD;
if (g_BlockMouseMove || (g_HSResetUponMouseClick && Hotstring::sEnabledCount))
sWhichHookNeeded |= HOOK_MOUSE;
// Install or deinstall either or both hooks, if necessary, based on these param values.
ChangeHookState(shk, sHotkeyCount, sWhichHookNeeded, sWhichHookAlways);
// Fix for v1.0.34: If the auto-execute section uses the Hotkey command but returns before doing
// something that calls MsgSleep, the main timer won't have been turned on. For example:
// Hotkey, Joy1, MySubroutine
// ;Sleep 1 ; This was a workaround before this fix.
// return
// By putting the following check here rather than in AutoHotkey.cpp, that problem is resolved.
// In addition...
if (sJoyHotkeyCount) // Joystick hotkeys require the timer to be always on.
SET_MAIN_TIMER
}
void Hotkey::MaybeUninstallHook()
// Caller knows that one of the users of the keyboard hook no longer requires it,
// and wants it uninstalled if it is no longer needed by anything else.
{
// Do some quick checks to avoid scanning all hotkeys unnecessarily:
if (g_input || Hotstring::sEnabledCount || (sWhichHookAlways & HOOK_KEYBD))
return;
// Do more thorough checking to determine whether the hook is still needed:
ManifestAllHotkeysHotstringsHooks();
}
void Hotkey::AllDestruct()
{
// MSDN: "Before terminating, an application must call the UnhookWindowsHookEx function to free
// system resources associated with the hook."
AddRemoveHooks(0); // Remove all hooks. By contrast, registered hotkeys are unregistered below.
if (g_PlaybackHook) // Would be unusual for this to be installed during exit, but should be checked for completeness.
UnhookWindowsHookEx(g_PlaybackHook);
for (int i = 0; i < sHotkeyCount; ++i)
delete shk[i]; // Unregisters before destroying.
}
bool Hotkey::PrefixHasNoEnabledSuffixes(int aVKorSC, bool aIsSC, bool &aSuppress)
// aVKorSC contains the virtual key or scan code of the specified prefix key (it's a scan code if aIsSC is true).
// Returns true if this prefix key has no suffixes that can possibly fire. Each such suffix is prevented from
// firing by one or more of the following:
// 1) Hotkey is completely disabled via IsCompletelyDisabled().
// 2) Hotkey has criterion and those criterion do not allow the hotkey to fire.
// Caller is expected to set aSuppress to a default of false.
{
// v1.0.44: Added aAsModifier so that a pair of hotkeys such as:
// LControl::tooltip LControl
// <^c::tooltip ^c
// ...works as it did in versions prior to 1.0.41, namely that LControl fires on key-up rather than
// down because it is considered a prefix key for the <^c hotkey .
modLR_type aAsModifier = KeyToModifiersLR(aIsSC ? 0 : aVKorSC, aIsSC ? aVKorSC : 0, NULL);
bool has_enabled_suffix = false;
for (int i = 0; i < sHotkeyCount; ++i)
{
Hotkey &hk = *shk[i];
if (aVKorSC != (aIsSC ? hk.mModifierSC : hk.mModifierVK) && !(aAsModifier & hk.mModifiersLR)
|| hk.IsCompletelyDisabled())
continue; // This hotkey isn't enabled or it doesn't use the specified key as a prefix. No further checking for it.
if (hk.mHookAction)
{
if (g_IsSuspended)
// An alt-tab hotkey (non-NULL mHookAction) is always suspended when g_IsSuspended==true because
// alt-tab hotkeys have no subroutine capable of making them exempt. So g_IsSuspended is checked
// for alt-tab hotkeys here; and for other types of hotkeys, it's checked further below.
continue;
//else // This alt-tab hotkey is currently active.
if ((hk.mNoSuppress & NO_SUPPRESS_PREFIX) || aSuppress)
return false; // Since any stored mHotCriterion are ignored for alt-tab hotkeys, no further checking is needed.
has_enabled_suffix = true;
continue; // Still need to check other hotkeys for NO_SUPPRESS_PREFIX.
}
if (has_enabled_suffix && !(hk.mNoSuppress & NO_SUPPRESS_PREFIX))
continue; // No need to evaluate this hotkey's variants.
// Otherwise, find out if any of its variants is eligible to fire. If so, immediately return
// false because even one eligible hotkey means this prefix is enabled.
for (HotkeyVariant *vp = hk.mFirstVariant; vp; vp = vp->mNextVariant)
// v1.0.42: Fixed to take into account whether the hotkey is suspended (previously it only checked
// whether the hotkey was enabled (above), which isn't enough):
if ( vp->mEnabled // This particular variant within its parent hotkey is enabled.
&& (!g_IsSuspended || vp->mSuspendExempt) // This variant isn't suspended...
&& (!vp->mHotCriterion || HotCriterionAllowsFiring(vp->mHotCriterion, hk.mName)) ) // ... and its criteria allow it to fire.
{
if ((vp->mNoSuppress & NO_SUPPRESS_PREFIX) || aSuppress)
return false; // At least one of this prefix's suffixes is eligible for firing.
has_enabled_suffix = true;
if (!(hk.mNoSuppress & NO_SUPPRESS_PREFIX))
break; // None of this hotkey's variants have NO_SUPPRESS_PREFIX.
// Keep checking to ensure no other enabled variants have NO_SUPPRESS_PREFIX.
}
}
// Since above didn't return, either no hotkeys were found for this prefix that are capable of firing,
// or no variants were found with the NO_SUPPRESS_PREFIX flag.
aSuppress = has_enabled_suffix;
return !has_enabled_suffix;
}
HotkeyVariant *Hotkey::CriterionAllowsFiring(HWND *aFoundHWND, ULONG_PTR aExtraInfo, LPTSTR aSingleChar)
// Caller must not call this for AltTab hotkeys IDs because this will always return NULL in such cases.
// Returns the address of the first matching non-global hotkey variant that is allowed to fire.
// If there is no non-global one eligible, the global one is returned (or NULL if none).
// If non-NULL, aFoundHWND is an output variable for the caller, but it is only set if a
// non-global/criterion variant is found; that is, it isn't changed when no match is found or
// when the match is a global variant. Even when set, aFoundHWND will be (HWND)1 for
// "not-criteria" such as #HotIf Not WinActive().
{
// Check mParentEnabled in case the hotkey became disabled between the time the message was posted
// and the time it arrived. A similar check is done for "suspend" later below (since "suspend"
// is a per-variant attribute).
if (!mParentEnabled) // IsCompletelyDisabled() isn't called because the loop below checks all the mEnabled flags, no need to do it twice.
return NULL;
HWND unused;
HWND &found_hwnd = aFoundHWND ? *aFoundHWND : unused; // To simplify other things.
found_hwnd = NULL; // Set default output parameter for caller (in case loop never sets it).
HotkeyVariant *vp, *vp_to_fire;
// aHookAction isn't checked because this should never be called for alt-tab hotkeys (see other comments above).
for (vp_to_fire = NULL, vp = mFirstVariant; vp; vp = vp->mNextVariant)
{
// Technically, g_IsSuspended needs to be checked only if our caller is TriggerJoyHotkeys()
// because other callers would never have received the hotkey message in the first place.
// However, since it's possible for a hotkey to become suspended between the time its hotkey
// message is posted and the time it is fetched and processed, aborting the firing seems
// like the best choice for the widest variety of circumstances (even though it's a departure
// from the behavior in previous versions). Another reason to check g_IsSuspended unconditionally
// is for maintainability and code size reduction. Finally, it's unlikely to significantly
// impact performance since the vast majority of hotkeys have either one or just a few variants.
if ( vp->mEnabled // This particular variant within its parent hotkey is enabled.
&& (!g_IsSuspended || vp->mSuspendExempt) // This variant isn't suspended...
&& HotInputLevelAllowsFiring(vp->mInputLevel, aExtraInfo, aSingleChar) // ... its #InputLevel allows it to fire...
&& (!vp->mHotCriterion || (found_hwnd = HotCriterionAllowsFiring(vp->mHotCriterion, mName))) ) // ... and its criteria allow it to fire.
{
if (vp->mHotCriterion) // Since this is the first criteria hotkey, it takes precedence.
return vp;
//else since this is variant has no criteria, let the first criteria variant in the list
// take precedence over it (if there is one). If none is found, the vp_to_fire will stay
// set as the non-criterion variant.
vp_to_fire = vp;
}
}
return vp_to_fire; // Either NULL or the variant found by the loop.
}
bool HotInputLevelAllowsFiring(SendLevelType inputLevel, ULONG_PTR aEventExtraInfo, LPTSTR aKeyHistoryChar)
{
if (InputLevelFromInfo(aEventExtraInfo) <= inputLevel)
{
if (aKeyHistoryChar)
*aKeyHistoryChar = 'i'; // Mark as ignored in KeyHistory
return false;
}
return true;
}
HotkeyVariant *Hotkey::CriterionFiringIsCertain(HotkeyIDType &aHotkeyIDwithFlags, bool aKeyUp, ULONG_PTR aExtraInfo
, bool &aFireWithNoSuppress, LPTSTR aSingleChar)
// v1.0.44: Caller has ensured that aFireWithNoSuppress is true if has already been decided and false if undecided.
// Upon return, caller can assume that the value in it is now decided rather than undecided.
// v1.0.42: Caller must not call this for AltTab hotkeys IDs, but this will always return NULL in such cases.
// *aSingleChar is sometimes modified for the caller here (if aSingleChar isn't NULL).
// Caller has ensured that aHotkeyIDwithFlags contains a valid/existing hotkey ID.
// Technically, aHotkeyIDwithMask can be with or without the flags in the high bits.
// If present, they're removed.
{
// aHookAction isn't checked because this should never be called for alt-tab hotkeys (see other comments above).
HotkeyIDType hotkey_id = aHotkeyIDwithFlags & HOTKEY_ID_MASK;
// The following check is for maintainability, since caller should have already checked and
// handled HOTKEY_ID_ALT_TAB and similar. Less-than-zero check not necessary because it's unsigned.
if (hotkey_id >= sHotkeyCount)
return NULL; // Special alt-tab hotkey quasi-ID used by the hook.
Hotkey &hk = *shk[hotkey_id]; // For convenience and performance.
if (aFireWithNoSuppress // Caller has already determined its value with certainty...
|| (hk.mNoSuppress & NO_SUPPRESS_SUFFIX_VARIES) != NO_SUPPRESS_SUFFIX_VARIES) // ...or its value is easy to determine, so do it now (compare to itself since it's a bitwise union).
{
// Since aFireWithNoSuppress can now be easily determined for the caller (or was already determined by the caller
// itself), it's possible to take advantage of the following optimization, which is especially important in cases
// where TitleMatchMode is "slow":
// For performance, the following returns without having called WinExist/Active if it sees that one of this
// hotkey's variant's will certainly fire due to the fact that it has a non-suspended global variant.
// This reduces the number of situations in which double the number of WinExist/Active() calls are made
// (once in the hook to determine whether the hotkey keystroke should be passed through to the active window,
// and again upon receipt of the message for reasons explained there).
for (HotkeyVariant *vp = hk.mFirstVariant; vp; vp = vp->mNextVariant)
if (!vp->mHotCriterion && vp->mEnabled && (!g_IsSuspended || vp->mSuspendExempt)
&& HotInputLevelAllowsFiring(vp->mInputLevel, aExtraInfo, aSingleChar))
{
// Fix for v1.0.47.02: The following section (above "return") was moved into this block
// from above the for() because only when this for() returns is it certain that this
// hk/hotkey_id is actually the right one, and thus its attributes can be used to determine
// aFireWithNoSuppress for the caller.
// Since this hotkey has variants only of one type (tilde or non-tilde), this variant must be of that type.
if (!aFireWithNoSuppress) // Caller hasn't yet determined its value with certainty (currently, this statement might always be true).
aFireWithNoSuppress = (hk.mNoSuppress & AT_LEAST_ONE_VARIANT_HAS_TILDE); // Due to other checks, this means all variants are tilde.
return vp; // Caller knows this isn't necessarily the variant that will fire since !vp->mHotCriterion.
}
}
// Since above didn't return, a slower method is needed to find out which variant of this hotkey (if any)
// should fire.
HotkeyVariant *vp;
if (vp = hk.CriterionAllowsFiring(NULL, aExtraInfo, aSingleChar))
{
if (!aFireWithNoSuppress) // Caller hasn't yet determined its value with certainty (currently, this statement might always be true).
aFireWithNoSuppress = (vp->mNoSuppress & AT_LEAST_ONE_VARIANT_HAS_TILDE);
return vp; // It found an eligible variant to fire.
}
// Since above didn't find any variant of the hotkey than can fire, check for other eligible hotkeys.
if (!hk.mHookAction) // Rule out those that aren't susceptible to the bug.
{
// Custom combos are no longer ruled out by the above since they allow extra modifiers and
// are capable of obscuring non-custom combos; e.g. LCtrl & a:: obscures <^a::, ^+a:: and so on.
// Fix for v1.0.46.13: Although the section higher above found no variant to fire for the
// caller-specified hotkey ID, it's possible that some other hotkey (one with a wildcard) is
// eligible to fire due to the eclipsing behavior of wildcard hotkeys. For example:
// #HotIf Not WinActive("Untitled")
// q::tooltip ThisHotkey . " Non-notepad"
// #HotIf WinActive("Untitled")
// *q::tooltip ThisHotkey . " Notepad"
// However, the logic here might not be a perfect solution because it fires the first available
// hotkey that has a variant whose criteria are met (which might not be exactly the desired rules
// of precedence). However, I think it's extremely rare that there would be more than one hotkey
// that matches the original hotkey (VK, SC, has-wildcard) etc. Even in the rare cases that there
// is more than one, the rarity is compounded by the rarity of the bug even occurring, which probably
// makes the odds vanishingly small. That's why the following simple, high-performance loop is used
// rather than more a more complex one that "locates the smallest (most specific) eclipsed wildcard
// hotkey", or "the uppermost variant among all eclipsed wildcards that is eligible to fire".
// UPDATE: This now uses a linked list of hotkeys which share the same suffix key, in the order of
// sort_most_general_before_least, which might solve the concern about precedence.
mod_type modifiers = ConvertModifiersLR(g_modifiersLR_logical_non_ignored); // Neutral modifiers.
for (HotkeyIDType candidate_id = hk.mNextHotkey; candidate_id != HOTKEY_ID_INVALID; )
{
Hotkey &hk2 = *shk[candidate_id]; // For performance and convenience.
candidate_id = hk2.mNextHotkey;
// Non-wildcard hotkeys are eligible for the workaround in cases like ^+a vs <^+a vs ^<+a, where
// the neutral modifier acts as a sort of wildcard (it permits left, right or both). This also
// increases support for varying names, such as Esc vs. Escape vs. vk1B (which already partially
// worked if wildcards were used).
// However, must ensure only the allowed modifiers are down when !mAllowExtraModifiers.
// mVK and mSC aren't checked since the linked list only includes hotkeys for this same suffix key.
// This also allows the workaround to be partially applied to LCtrl vs. Ctrl and similar (as suffixes).
if ( (hk2.mAllowExtraModifiers || !(~hk2.mModifiersConsolidatedLR & g_modifiersLR_logical_non_ignored))
&& hk2.mKeyUp == hk.mKeyUp // Seems necessary that up/down nature is the same in both.
&& !hk2.mModifierVK // Avoid accidental matching of normal hotkeys with custom-combo "&"
&& !hk2.mModifierSC // hotkeys that happen to have the same mVK/SC.
&& !hk2.mHookAction // Might be unnecessary to check this; but just in case.
&& hk2.mID != hotkey_id // Don't consider the original hotkey because it was already found ineligible.
&& !(hk2.mModifiers & ~modifiers) // All neutral modifiers required by the candidate are pressed.
&& !(hk2.mModifiersLR & ~g_modifiersLR_logical_non_ignored) // All left-right specific modifiers required by the candidate are pressed.
//&& hk2.mType != HK_JOYSTICK // Seems unnecessary since joystick hotkeys don't call us and even if they did, probably shouldn't be included.
//&& hk2.mParentEnabled ) // CriterionAllowsFiring() will check this for us.
)
{
// The following section is similar to one higher above, so maintain them together:
if (vp = hk2.CriterionAllowsFiring(NULL, aExtraInfo, aSingleChar))
{
if (!aFireWithNoSuppress) // Caller hasn't yet determined its value with certainty (currently, this statement might always be true).
aFireWithNoSuppress = (vp->mNoSuppress & AT_LEAST_ONE_VARIANT_HAS_TILDE);
aHotkeyIDwithFlags = hk2.mID; // Caller currently doesn't need the flags put onto it, so they're omitted.
return vp; // It found an eligible variant to fire.
}
}
}
}
// Otherwise, this hotkey has no variants that can fire. Caller wants a few things updated in that case.
// v1.1.37: The following isn't done anymore because it makes logic elsewhere harder to follow,
// and was causing a bug where the key-up event of a custom prefix key wasn't suppressed if the
// key had an ineligible key-down hotkey and an eligible key-up hotkey. Another reason not to
// do it is that some callers will consider alternative hotkeys after we return false, so the
// proper value of fire_with_no_suppress can only be known when firing IS certain. The simple
// and logical solution to the issue mentioned below is for certain callers to check our return
// value, and if false, don't suppress.
//if (!aFireWithNoSuppress) // Caller hasn't yet determined its value with certainty.
// aFireWithNoSuppress = true; // Fix for v1.0.47.04: Added this line and the one above to fix the fact that a context-sensitive hotkey like "a UP::" would block the down-event of that key even when the right window/criteria aren't met.
if (aSingleChar && *aSingleChar != 'i') // 'i' takes precedence because it's used to detect when #InputLevel prevented the hotkey from firing, to prevent it from being suppressed.
*aSingleChar = '#'; // '#' in KeyHistory to indicate this hotkey is disabled due to #HotIf WinActive/Exist() criterion.
return NULL;
}
HotkeyIDType Hotkey::FindPairedHotkey(HotkeyIDType aFirstID, modLR_type aModsLR, bool aKeyUp)
{
mod_type modifiers = ConvertModifiersLR(aModsLR); // Neutral modifiers.
for (HotkeyIDType candidate_id = aFirstID; candidate_id != HOTKEY_ID_INVALID; )
{
Hotkey &hk2 = *shk[candidate_id]; // For performance and convenience.
candidate_id = hk2.mNextHotkey;
if ( (hk2.mAllowExtraModifiers || !(~hk2.mModifiersConsolidatedLR & aModsLR))
&& hk2.mKeyUp == aKeyUp
&& !hk2.mModifierVK // Avoid accidental matching of normal hotkeys with custom-combo "&"
&& !hk2.mModifierSC // hotkeys that happen to have the same mVK/SC.
&& !hk2.mHookAction // Might be unnecessary to check this; but just in case.
&& !(hk2.mModifiers & ~modifiers) // All neutral modifiers required by the candidate are pressed.
&& !(hk2.mModifiersLR & ~aModsLR) // All left-right specific modifiers required by the candidate are pressed.
//&& hk2.mParentEnabled // CriterionAllowsFiring() will check this for us.
)
return aKeyUp ? (hk2.mID | HOTKEY_KEY_UP) : hk2.mID;
}
return HOTKEY_ID_INVALID;
}
modLR_type Hotkey::HotkeyRequiresModLR(HotkeyIDType aHotkeyID, modLR_type aModLR)
{
if (aHotkeyID >= sHotkeyCount)
return 0;
return shk[aHotkeyID]->mModifiersConsolidatedLR & aModLR;
}
void Hotkey::TriggerJoyHotkeys(int aJoystickID, DWORD aButtonsNewlyDown)
{
for (int i = 0; i < sHotkeyCount; ++i)
{
Hotkey &hk = *shk[i]; // For performance and convenience.
// Fix for v1.0.34: If hotkey isn't enabled, or hotkeys are suspended and this one isn't
// exempt, don't fire it. These checks are necessary only for joystick hotkeys because
// normal hotkeys are completely deactivated when turned off or suspended, but the joystick
// is still polled even when some joystick hotkeys are disabled. UPDATE: In v1.0.42, Suspend
// is checked upon receipt of the message, not here upon sending.
if (hk.mType == HK_JOYSTICK && hk.mVK == aJoystickID
&& (aButtonsNewlyDown & ((DWORD)0x01 << (hk.mSC - JOYCTRL_1)))) // This hotkey's button is among those newly pressed.
{
// Criteria are checked, and variant determined, upon arrival of message rather than when sending
// ("suspend" is also checked then). This is because joystick button presses are never hidden
// from the active window (the concept really doesn't apply), so not checking here avoids the
// performance loss of a second check (the loss can be significant in the case of
// "SetTitleMatchMode Slow").
//
// Post it to the thread because the message pump itself (not the WindowProc) will handle it.
// UPDATE: Posting to NULL would have a risk of discarding the message if a MsgBox pump or
// pump other than MsgSleep() were running. The only reason it doesn't is that this function
// is only ever called from MsgSleep(), which is careful to process all messages (at least
// those that aren't kept queued due to the message filter) prior to returning to its caller.
// But for maintainability, it seems best to change this to g_hWnd vs. NULL to make joystick
// hotkeys behave more like standard hotkeys.
PostMessage(g_hWnd, WM_HOTKEY, (WPARAM)i, 0);
}
//else continue the loop in case the user has newly pressed more than one joystick button.
}
}
void Hotkey::PerformInNewThreadMadeByCaller(HotkeyVariant &aVariant)
// Caller is responsible for having called PerformIsAllowed() before calling us.
// Caller must have already created a new thread for us, and must close the thread when we return.
{
static bool sDialogIsDisplayed = false; // Prevents double-display caused by key buffering.
if (sDialogIsDisplayed) // Another recursion layer is already displaying the warning dialog below.
return; // Don't allow new hotkeys to fire during that time.
// Help prevent runaway hotkeys (infinite loops due to recursion in bad script files):
static UINT throttled_key_count = 0; // This var doesn't belong in struct since it's used only here.
UINT time_until_now;
int display_warning;
if (!sTimePrev)
sTimePrev = GetTickCount();
++throttled_key_count;
sTimeNow = GetTickCount();
// Calculate the amount of time since the last reset of the sliding interval.
// Note: A tickcount in the past can be subtracted from one in the future to find
// the true difference between them, even if the system's uptime is greater than
// 49 days and the future one has wrapped but the past one hasn't. This is
// due to the nature of DWORD subtraction. The only time this calculation will be
// unreliable is when the true difference between the past and future
// tickcounts itself is greater than about 49 days:
time_until_now = (sTimeNow - sTimePrev);
if (display_warning = (throttled_key_count > g_MaxHotkeysPerInterval
&& time_until_now < g_HotkeyThrottleInterval))
{
// The moment any dialog is displayed, hotkey processing is halted since this
// app currently has only one thread.
TCHAR error_text[2048];
// Using %f with wsprintf() yields a floating point runtime error dialog.
// UPDATE: That happens if you don't cast to float, or don't have a float var
// involved somewhere. Avoiding floats altogether may reduce EXE size
// and maybe other benefits (due to it not being "loaded")?
sntprintf(error_text, _countof(error_text), _T("%u hotkeys have been received in the last %ums.\n\n")
_T("Do you want to continue?\n(see A_MaxHotkeysPerInterval in the help file)") // In case its stuck in a loop.
, throttled_key_count, time_until_now);
// Turn off any RunAgain flags that may be on, which in essence is the same as de-buffering
// any pending hotkey keystrokes that haven't yet been fired:
ResetRunAgainAfterFinished();
// This is now needed since hotkeys can still fire while a messagebox is displayed.
// Seems safest to do this even if it isn't always necessary:
sDialogIsDisplayed = true;
g_AllowInterruption = FALSE;
if (MsgBox(error_text, MB_YESNO) == IDNO)
g_script.ExitApp(EXIT_CLOSE); // Might not actually Exit if there's an OnExit function.
g_AllowInterruption = TRUE;
sDialogIsDisplayed = false;
}
// The display_warning var is needed due to the fact that there's an OR in this condition:
if (display_warning || time_until_now > g_HotkeyThrottleInterval)
{
// Reset the sliding interval whenever it expires. Doing it this way makes the
// sliding interval more sensitive than alternate methods might be.
// Also reset it if a warning was displayed, since in that case it didn't expire.
throttled_key_count = 0;
sTimePrev = sTimeNow;
}
if (display_warning)
// At this point, even though the user chose to continue, it seems safest
// to ignore this particular hotkey event since it might be WinClose or some
// other command that would have unpredictable results due to the displaying
// of the dialog itself.
return;
// This is stored as an attribute of the script (semi-globally) rather than passed
// as a parameter to ExecUntil (and from their on to any calls to SendKeys() that it
// makes) because it's possible for SendKeys to be called asynchronously, namely
// by a timed subroutine, while A_HotkeyModifierTimeout is still in effect,
// in which case we would want SendKeys() to take note of these modifiers even
// if it was called from an ExecUntil() other than ours here:
g_script.mThisHotkeyModifiersLR = mModifiersConsolidatedLR;
// LAUNCH HOTKEY SUBROUTINE:
++aVariant.mExistingThreads; // This is the thread count for this particular hotkey only.
ExprTokenType params = { mName };
ResultType result = aVariant.mCallback->ExecuteInNewThread(g_script.mThisHotkeyName, ¶ms, 1);
--aVariant.mExistingThreads;
if (result == FAIL)
aVariant.mRunAgainAfterFinished = false; // Ensure this is reset due to the error.
else if (aVariant.mRunAgainAfterFinished)
{
// But MsgSleep() can change it back to true again, when called by the above call
// to ExecUntil(), to keep it auto-repeating:
aVariant.mRunAgainAfterFinished = false; // i.e. this "run again" ticket has now been used up.
if (GetTickCount() - aVariant.mRunAgainTime <= 1000)
{
// v1.0.44.14: Post a message rather than directly running the above ExecUntil again.
// This fixes unreported bugs in previous versions where the thread isn't reinitialized before
// the launch of one of these buffered hotkeys, which caused settings such as SetKeyDelay
// not to start off at their defaults. Also, there are quite a few other things that the main
// event loop does to prep for the launch of a hotkey. Rather than copying them here or
// trying to put them into a shared function (which would be difficult due to their nature),
// it's much more maintainable to post a message, and in most cases, it shouldn't measurably
// affect response time (this feature is rarely used anyway).
PostMessage(g_hWnd, WM_HOTKEY, (WPARAM)mID, 0);
}
//else it was posted too long ago, so don't do it. This is because most users wouldn't
// want a buffered hotkey to stay pending for a long time after it was pressed, because
// that might lead to unexpected behavior.
}
}
FResult Hotkey::IfExpr(IObject *aExprObj)
// HotIf ; Set null criterion.
// HotIf FunctionObject
{
if (aExprObj)
{
HotkeyCriterion *cp;
for (cp = g_FirstHotExpr; ; cp = cp->NextExpr)
{
if (!cp) // End of the list and it wasn't found.
{
auto fr = ValidateFunctor(aExprObj, 1);
if (fr != OK)
return fr;
if ( !(cp = AddHotkeyIfExpr()) )
return FR_E_OUTOFMEM;
aExprObj->AddRef();
cp->Type = HOT_IF_CALLBACK;
cp->Callback = aExprObj;
cp->WinTitle = _T("");
cp->WinText = _T("");
break;
}
if (cp->Type == HOT_IF_CALLBACK && cp->Callback == aExprObj)
break;
}
g->HotCriterion = cp;
}
else
{
g->HotCriterion = nullptr;
}
return OK;
}
FResult Hotkey::IfExpr(LPCTSTR aExpr)
// HotIf ; Set null criterion.
// HotIf "Exact-expression-text"
{
if (!aExpr || !*aExpr)
{
g->HotCriterion = nullptr;
}
else
{
HotkeyCriterion *cp = FindHotkeyIfExpr(aExpr);
if (!cp) // Expression not found.
return FValueError(ERR_HOTKEY_IF_EXPR, aExpr);
g->HotCriterion = cp;
}
return OK;
}
FResult Hotkey::Dynamic(LPCTSTR aHotkeyName, LPCTSTR aOptions, IObject *aCallback, HookActionType aHookAction)
// Creates, updates, enables, or disables a hotkey dynamically (while the script is running).