/
hotkey.cpp
2497 lines (2297 loc) · 127 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-2008 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()
// Initialize static members:
HookType Hotkey::sWhichHookNeeded = 0;
HookType Hotkey::sWhichHookAlways = 0;
DWORD Hotkey::sTimePrev = {0};
DWORD Hotkey::sTimeNow = {0};
Hotkey *Hotkey::shk[MAX_HOTKEYS] = {NULL};
HotkeyIDType Hotkey::sNextID = 0;
const HotkeyIDType &Hotkey::sHotkeyCount = Hotkey::sNextID;
bool Hotkey::sJoystickHasHotkeys[MAX_JOYSTICKS] = {false};
DWORD Hotkey::sJoyHotkeyCount = 0;
HWND HotCriterionAllowsFiring(HotCriterionType aHotCriterion, char *aWinTitle, char *aWinText)
// 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 that is non-hook (such as for Win9x).
// Returns a non-NULL HWND if firing is allowed. However, if it's a global criterion or
// a "not-criterion" such as #IfWinNotActive, (HWND)1 is returned rather than a genuine HWND.
{
HWND found_hwnd;
switch(aHotCriterion)
{
case HOT_IF_ACTIVE:
case HOT_IF_NOT_ACTIVE:
found_hwnd = WinActive(g_default, aWinTitle, aWinText, "", "", false); // Thread-safe.
break;
case HOT_IF_EXIST:
case HOT_IF_NOT_EXIST:
found_hwnd = WinExist(g_default, aWinTitle, aWinText, "", "", false, false); // Thread-safe.
break;
default: // HOT_NO_CRITERION (listed last because most callers avoids calling here by checking this value first).
return (HWND)1; // Always allow hotkey to fire.
}
return (aHotCriterion == HOT_IF_ACTIVE || aHotCriterion == HOT_IF_EXIST) ? found_hwnd : (HWND)!found_hwnd;
}
ResultType SetGlobalHotTitleText(char *aWinTitle, char *aWinText)
// Allocate memory for aWinTitle/Text (if necessary) and update g_HotWinTitle/Text to point to it.
// Returns FAIL if memory couldn't be allocated, or OK otherwise.
// This is a global function because it's used by both hotkeys and hotstrings.
{
// Caller relies on this check:
if (!(*aWinTitle || *aWinText)) // In case of something weird but legit like: #IfWinActive, ,
{
g_HotCriterion = HOT_NO_CRITERION; // Don't allow blank title+text to avoid having it interpreted as the last-found-window.
g_HotWinTitle = ""; // Helps maintainability and some things might rely on it.
g_HotWinText = ""; //
return OK;
}
// 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 insignficant.
HotkeyCriterion *cp;
for (cp = g_FirstHotCriterion; cp; cp = cp->mNextCriterion)
if (!strcmp(cp->mHotWinTitle, aWinTitle) && !strcmp(cp->mHotWinText, aWinText)) // Case insensitive.
{
// Match found, so point to the existing memory.
g_HotWinTitle = cp->mHotWinTitle;
g_HotWinText = cp->mHotWinText;
return OK;
}
// Since above didn't return, there is no existing entry in the linked list that matches this exact
// combination of Title and Text. So create a new item and allocate memory for it.
if ( !(cp = (HotkeyCriterion *)SimpleHeap::Malloc(sizeof(HotkeyCriterion))) )
return FAIL;
cp->mNextCriterion = NULL;
if (*aWinTitle)
{
if ( !(cp->mHotWinTitle = SimpleHeap::Malloc(aWinTitle)) )
return FAIL;
}
else
cp->mHotWinTitle = "";
if (*aWinText)
{
if ( !(cp->mHotWinText = SimpleHeap::Malloc(aWinText)) )
return FAIL;
}
else
cp->mHotWinText = "";
g_HotWinTitle = cp->mHotWinTitle;
g_HotWinText = cp->mHotWinText;
// Update the linked list:
if (!g_FirstHotCriterion)
g_FirstHotCriterion = g_LastHotCriterion = cp;
else
{
g_LastHotCriterion->mNextCriterion = cp;
// This must be done after the above:
g_LastHotCriterion = cp;
}
return OK;
}
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 vitual 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.
bool vk_is_prefix[VK_ARRAY_COUNT] = {false};
bool hk_is_inactive[MAX_HOTKEYS]; // No init needed.
bool is_win9x = g_os.IsWin9x(); // Might help performance a little by avoiding calls in loops.
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 Relies 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 #IfWin 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;
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.
{
// The following is done even for Win9x because "no functionality" might be preferable to
// partial functionality. This might be relied upon by the remapping feature to prevent a
// remapping such as a::b from partially become active and having unintended side-effects
// (could use more review).
// 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
// (except on Win9x, for which the repercussions of changing or fixing
// it to take these into effect have not been evaluated yet):
// 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; // Done even for Win9x (see comments above).
// And if it's currently registered, it will be unregistered later below.
}
}
// v1.0.42: The following is now not done for Win9x because it seems inappropriate for
// a wilcard hotkey (which will be attempt-registered due to Win9x's lack of hook) to also
// disable hotkeys that share the same suffix as the wildcard.
// 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 && !is_win9x && 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 first pass loop.
// SECOND 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):
for (sWhichHookNeeded = 0, 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) && !is_win9x)
{
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: Since the above has ascertained that the OS isn't Win9x, any #IfWin
// 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 critiera (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->mJumpToLabel->IsExemptFromSuspend()) )
// ... and this variant isn't suspended (we already know IsCompletelyDisabled()==false from an earlier check).
{
hot.mType = HK_NORMAL; // Override the default. 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.
if (!is_win9x) // If the OS supports the hook, it can be used to override the other application.
hot.mType = HK_KEYBD_HOOK;
//else it's Win9x (v1.0.42), so don't force type to hook in case this hotkey is temporarily
// in use by some other application. That way, the registration will be retried next time
// the Suspend or Hotkey command calls this function.
// The old Win9x warning dialog was removed to reduce code size (since usage of
// Win9x is becoming very rare).
}
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();
// If this is a hook hotkey and the OS is Win9x, the old warning has been removed to
// reduce code size (since usage of Win9x is becoming very rare). Older comment:
// Since it's flagged as a hook in spite of the fact that the OS is Win9x, it means
// that some previous logic determined that it's not even worth trying to register
// it because it's just plain not supported.
switch (hot.mType) // It doesn't matter if the OS is Win9x because in that case, other sections just ignore hook hotkeys.
{
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. The Win9x warning message was removed so that scripts can be
// run on multiple OSes without a continual warning message just because it happens to be running
// on Win9x. By design, the Num/Scroll/CapsLock AlwaysOn/Off setting stays in effect even when
// Suspend in ON.
if ( Hotstring::mAtLeastOneEnabled
|| !(g_ForceNumLock == NEUTRAL && g_ForceCapsLock == NEUTRAL && g_ForceScrollLock == NEUTRAL) )
sWhichHookNeeded |= HOOK_KEYBD;
if (g_BlockMouseMove || (g_HSResetUponMouseClick && Hotstring::mAtLeastOneEnabled))
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::AllDestructAndExit(int aExitCode)
{
// PostQuitMessage() might be needed to prevent hang-on-exit. Once this is done, no message boxes or
// other dialogs can be displayed. MSDN: "The exit value returned to the system must be the wParam
// parameterof the WM_QUIT message." In our case, PostQuitMessage() should announce the same exit code
// that we will eventually call exit() with:
PostQuitMessage(aExitCode);
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.
// Do this only at the last possible moment prior to exit() because otherwise
// it may free memory that is still in use by objects that depend on it.
// This is actually kinda wrong because when exit() is called, the destructors
// of static, global, and main-scope objects will be called. If any of these
// destructors try to reference memory freed() by DeleteAll(), there could
// be trouble.
// It's here mostly for traditional reasons. I'm 99.99999 percent sure that there would be no
// penalty whatsoever to omitting this, since any modern OS will reclaim all
// memory dynamically allocated upon program termination. Indeed, omitting
// deletes and free()'s for simple objects will often improve the reliability
// and performance since the OS is far more efficient at reclaiming the memory
// than us doing it manually (which involves a potentially large number of deletes
// due to all the objects and sub-objects to be destructed in a typical C++ program).
// UPDATE: In light of the first paragraph above, it seems best not to do this at all,
// instead letting all implicitly-called destructors run prior to program termination,
// at which time the OS will reclaim all remaining memory:
//SimpleHeap::DeleteAll();
// In light of the comments below, and due to the fact that anyone using this app
// is likely to want the anti-focus-stealing measure to always be disabled, I
// think it's best not to bother with this ever, since its results are
// unpredictable:
/* if (g_os.IsWin98orLater() || g_os.IsWin2000orLater())
// Restore the original timeout value that was set by WinMain().
// Also disables the compiler warning for the PVOID cast.
// Note: In many cases, this call will fail to set the value (perhaps even if
// SystemParametersInfo() reports success), probably because apps aren't
// supposed to change this value unless they currently have the input
// focus or something similar (and this app probably doesn't meet the criteria
// at this stage). So I think what will happen is: the value set
// in WinMain() will stay in effect until the user reboots, at which time
// the default value store in the registry will once again be in effect.
// This limitation seems harmless. Indeed, it's probably a good thing not to
// set it back afterward so that windows behave more consistently for the user
// regardless of whether this program is currently running.
#ifdef _MSC_VER
#pragma warning( disable : 4312 )
#endif
SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, (PVOID)g_OriginalTimeout, SPIF_SENDCHANGE);
#ifdef _MSC_VER
#pragma warning( default : 4312 )
#endif
*/
// I know this isn't the preferred way to exit the program. However, due to unusual
// conditions such as the script having MsgBoxes or other dialogs displayed on the screen
// at the time the user exits (in which case our main event loop would be "buried" underneath
// the event loops of the dialogs themselves), this is the only reliable way I've found to exit
// so far. The caller has already called PostQuitMessage(), which might not help but it doesn't hurt:
exit(aExitCode); // exit() is insignificant in code size. It does more than ExitProcess(), but perhaps nothing more that this application actually requires.
// By contrast to _exit(), exit() flushes all file buffers before terminating the process. It also
// calls any functions registered via atexit or _onexit.
}
bool Hotkey::PrefixHasNoEnabledSuffixes(int aVKorSC, bool aIsSC)
// 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. 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.
{
// 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);
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.
return false; // Since any stored mHotCriterion are ignored for alt-tab hotkeys, no further checking is needed.
}
// 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->mJumpToLabel->IsExemptFromSuspend()) // This variant isn't suspended...
&& (!vp->mHotCriterion || HotCriterionAllowsFiring(vp->mHotCriterion
, vp->mHotWinTitle, vp->mHotWinText)) ) // ... and its critieria allow it to fire.
return false; // At least one of this prefix's suffixes is eligible for firing.
}
// Since above didn't return, no hotkeys were found for this prefix that are capable of firing.
return true;
}
HotkeyVariant *Hotkey::CriterionAllowsFiring(HWND *aFoundHWND)
// 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 #IfWinNotActive.
{
// 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->mJumpToLabel->IsExemptFromSuspend()) // This variant isn't suspended...
&& (!vp->mHotCriterion || (found_hwnd = HotCriterionAllowsFiring(vp->mHotCriterion
, vp->mHotWinTitle, vp->mHotWinText))) ) // ... and its critieria 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 critieria, 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-critierion variant.
vp_to_fire = vp;
}
}
return vp_to_fire; // Either NULL or the variant found by the loop.
}
bool Hotkey::CriterionFiringIsCertain(HotkeyIDType &aHotkeyIDwithFlags, bool aKeyUp, UCHAR &aNoSuppress
, bool &aFireWithNoSuppress, char *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.
// aHotkeyToFireUponRelease is sometimes modified for the caller here, as is *aSingleChar (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 false; // 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->mJumpToLabel->IsExemptFromSuspend()))
{
// 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 true;
}
}
// 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())
{
if (!aFireWithNoSuppress) // Caller hasn't yet determined its value with certainty (currently, this statement might always be true).
aFireWithNoSuppress = vp->mNoSuppress;
return true; // 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.mModifierVK || hk.mModifierSC || hk.mHookAction)) // Rule out those that aren't susceptible to the bug due to their lack of support for wildcards.
{
// 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:
// #IfWinNotActive Untitled
// q::tooltip %A_ThisHotkey% Non-notepad
// #IfWinActive Untitled
// *q::tooltip %A_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".
for (int i = 0; i < sHotkeyCount; ++i) // This loop is undesirable; but its performance impact probably isn't measurable in the majority of cases.
{
Hotkey &hk2 = *shk[i]; // For performance and convenience.
if ( hk2.mVK == hk.mVK // VK and SC (one of which is typically zero) must both match for
&& hk2.mSC == hk.mSC // this bug to have wrongly eclipsed a qualified variant of some other hotkey.
&& hk2.mAllowExtraModifiers // To be eclipsable by the original hotkey, a candidate must have a wildcard.
&& hk2.mKeyUp == aKeyUp // 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's was already found ineligible.
&& (hk.mAllowExtraModifiers // Either the original hotkey must allow extra modifiers or the candidate must not have any modifiers present on the original (otherwise the user probably isn't holding down the right keys to trigger this hotkey).
|| !((hk.mModifiersConsolidatedLR ^ hk2.mModifiersConsolidatedLR) & hk2.mModifiersConsolidatedLR)) // "The modifiers that are different intersected with those present on the candidate", which are those present on the candidate that are absent from this original.
//&& hk2.mType != HK_JOYSTICK // Seems unnecessary since joystick hotkeys don't call us and even if they did, probably should 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())
{
if (!aFireWithNoSuppress) // Caller hasn't yet determined its value with certainty (currently, this statement might always be true).
aFireWithNoSuppress = vp->mNoSuppress;
aHotkeyIDwithFlags = hk2.mID; // Caller currently doesn't need the flags put onto it, so they're omitted.
return true; // 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.
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 this is a key-down hotkey:
// Leave aHotkeyToFireUponRelease set to whatever it was so that the critieria are
// evaluated later, at the time of release. It seems more correct that way, though the actual
// change (hopefully improvement) in usability is unknown.
// Since the down-event of this key won't be suppressed, it seems best never to suppress the
// key-up hotkey (if it has one), if nothing else than to be sure the logical key state of that
// key as shown by GetAsyncKeyState() returns the correct value (for modifiers, this is even more
// important since them getting stuck down causes undesirable behavior). If it doesn't have a
// key-up hotkey, the up-keystroke should wind up being non-suppressed anyway due to default
// processing).
if (!aKeyUp)
aNoSuppress |= NO_SUPPRESS_NEXT_UP_EVENT; // Update output parameter for the caller.
if (aSingleChar)
*aSingleChar = '#'; // '#' in KeyHistory to indicate this hotkey is disabled due to #IfWin criterion.
return false;
}
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::Perform(HotkeyVariant &aVariant)
// Caller is reponsible for having called PerformIsAllowed() before calling us.
{
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 math. 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 > (DWORD)g_MaxHotkeysPerInterval
&& time_until_now < (DWORD)g_HotkeyThrottleInterval))
{
// The moment any dialog is displayed, hotkey processing is halted since this
// app currently has only one thread.
char 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")?
snprintf(error_text, sizeof(error_text), "%u hotkeys have been received in the last %ums.\n\n"
"Do you want to continue?\n(see #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 essense 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_CRITICAL); // Might not actually Exit if there's an OnExit subroutine.
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 > (DWORD)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 #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;
bool unregistered_during_thread = mUnregisterDuringThread && mIsRegistered;
// LAUNCH HOTKEY SUBROUTINE:
// For v1.0.23, the below allows the $ hotkey prefix to unregister the hotkey on
// Windows 9x, which allows the send command to send the hotkey itself without
// causing an infinite loop of keystrokes. For simplicity, the hotkey is kept
// unregistered during the entire duration of the thread, rather than trying to
// selectively do it before and after each Send command of the thread:
if (unregistered_during_thread) // Do it every time through the loop in case the hotkey is re-registered by its own subroutine.
Unregister(); // This takes care of other details for us.
++aVariant.mExistingThreads; // This is the thread count for this particular hotkey only.
ResultType result = aVariant.mJumpToLabel->Execute();
--aVariant.mExistingThreads;
if (unregistered_during_thread)
Register();
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.
}
}
ResultType Hotkey::Dynamic(char *aHotkeyName, char *aLabelName, char *aOptions, Label *aJumpToLabel)
// Creates, updates, enables, or disables a hotkey dynamically (while the script is running).
// Returns OK or FAIL.
{
if (!strnicmp(aHotkeyName, "IfWin", 5)) // Seems reasonable to assume that anything starting with "IfWin" can't be the name of a hotkey.
{
HotCriterionType hot_criterion;
bool invert = !strnicmp(aHotkeyName + 5, "Not", 3);
if (!strnicmp(aHotkeyName + (invert ? 8 : 5), "Active", 6)) // It matches #IfWin[Not]Active.
hot_criterion = invert ? HOT_IF_NOT_ACTIVE : HOT_IF_ACTIVE;
else if (!strnicmp(aHotkeyName + (invert ? 8 : 5), "Exist", 5))
hot_criterion = invert ? HOT_IF_NOT_EXIST : HOT_IF_EXIST;
else // It starts with IfWin but isn't Active or Exist: Don't alter the current criterion.
return g_ErrorLevel->Assign(ERRORLEVEL_ERROR);
if (!(*aLabelName || *aOptions)) // This check is done only after detecting bad spelling of IfWin above.
g_HotCriterion = HOT_NO_CRITERION;
else if (SetGlobalHotTitleText(aLabelName, aOptions)) // Currently, it only fails upon out-of-memory.
g_HotCriterion = hot_criterion; // Only set at the last minute so that previous criteria will stay in effect if it fails.
else
return g_ErrorLevel->Assign(ERRORLEVEL_ERROR);
return g_ErrorLevel->Assign(ERRORLEVEL_NONE); // Indicate success.
}
// For maintainability (and script readability), don't support "U" as a substitute for "UseErrorLevel",
// since future options might contain the letter U as a "parameter" that immediately follows an option-letter.
bool use_errorlevel = strcasestr(aOptions, "UseErrorLevel");
#define RETURN_HOTKEY_ERROR(level, msg, info) return use_errorlevel ? g_ErrorLevel->Assign(level) \
: g_script.ScriptError(msg ERR_ABORT, info)
HookActionType hook_action = 0; // Set default.
if (!aJumpToLabel) // It wasn't provided by caller (resolved at load-time).
if ( !(hook_action = ConvertAltTab(aLabelName, true)) )
if ( *aLabelName && !(aJumpToLabel = g_script.FindLabel(aLabelName)) )
RETURN_HOTKEY_ERROR(HOTKEY_EL_BADLABEL, ERR_NO_LABEL, aLabelName);
// Above has ensured that aJumpToLabel and hook_action can't both be non-zero. Furthermore,
// both can be zero/NULL only when the caller is updating an existing hotkey to have new options
// (i.e. it's retaining its current label).
bool suffix_has_tilde;
Hotkey *hk = FindHotkeyByTrueNature(aHotkeyName, suffix_has_tilde); // NULL if not found.
HotkeyVariant *variant = hk ? hk->FindVariant() : NULL;
bool update_all_hotkeys = false; // This method avoids multiple calls to ManifestAllHotkeysHotstringsHooks() (which is high-overhead).
bool variant_was_just_created = false;
switch (hook_action)
{
case HOTKEY_ID_ON:
case HOTKEY_ID_OFF:
case HOTKEY_ID_TOGGLE:
if (!hk)
RETURN_HOTKEY_ERROR(HOTKEY_EL_NOTEXIST, ERR_NONEXISTENT_HOTKEY, aHotkeyName);
if (!(variant || hk->mHookAction)) // mHookAction (alt-tab) hotkeys don't need a variant that matches the current criteria.
// To avoid ambiguity and also allow the script to use ErrorLevel to detect whether a variant
// already exists, it seems best to strictly require a matching variant rather than falling back
// onto some "default variant" such as the global variant (if any).
RETURN_HOTKEY_ERROR(HOTKEY_EL_NOTEXISTVARIANT, ERR_NONEXISTENT_VARIANT, aHotkeyName);
if (hook_action == HOTKEY_ID_TOGGLE)
hook_action = hk->mHookAction
? (hk->mParentEnabled ? HOTKEY_ID_OFF : HOTKEY_ID_ON) // Enable/disable parent hotkey (due to alt-tab being a global hotkey).
: (variant->mEnabled ? HOTKEY_ID_OFF : HOTKEY_ID_ON); // Enable/disable individual variant.
if (hook_action == HOTKEY_ID_ON)
{
if (hk->mHookAction ? hk->EnableParent() : hk->Enable(*variant))
update_all_hotkeys = true; // Do it this way so that any previous "true" value isn't lost.
}
else
if (hk->mHookAction ? hk->DisableParent() : hk->Disable(*variant))
update_all_hotkeys = true; // Do it this way so that any previous "true" value isn't lost.
break;
default: // hook_action is 0 or an AltTab action. COMMAND: Hotkey, Name, Label|AltTabAction
if (!hk) // No existing hotkey of this name, so create a new hotkey.
{
if (hook_action) // COMMAND (create hotkey): Hotkey, Name, AltTabAction
hk = AddHotkey(NULL, hook_action, aHotkeyName, suffix_has_tilde, use_errorlevel);
else // COMMAND (create hotkey): Hotkey, Name, LabelName [, Options]
{
if (!aJumpToLabel) // Caller is trying to set new aOptions for a nonexistent hotkey.
RETURN_HOTKEY_ERROR(HOTKEY_EL_NOTEXIST, ERR_NONEXISTENT_HOTKEY, aHotkeyName);
hk = AddHotkey(aJumpToLabel, 0, aHotkeyName, suffix_has_tilde, use_errorlevel);
}
if (!hk)
return use_errorlevel ? OK : FAIL; // AddHotkey() already displayed the error (or set ErrorLevel).
variant = hk->mLastVariant; // Update for use with the options-parsing section further below.
update_all_hotkeys = true;
variant_was_just_created = true;
}
else // Hotkey already exists (though possibly not the required variant). Update the hotkey if appropriate.
{
if (hk->mHookAction != hook_action) // COMMAND: Change to/from alt-tab hotkey.
{
// LoadIncludedFile() contains logic and comments similar to this, so maintain them together.
// If hook_action isn't zero, the caller is converting this hotkey into a global alt-tab
// hotkey (alt-tab hotkeys are never subject to #IfWin, as documented). Thus, variant can
// be NULL because making a hotkey become alt-tab doesn't require the creation or existence
// of a variant matching the current #IfWin criteria. However, continue on to process the
// Options parameter in case it contains "On" or some other keyword applicable to alt-tab.
hk->mHookAction = hook_action;
if (!hook_action)
// Since this hotkey is going from alt-tab to non-alt-tab, make sure it's not disabled
// because currently, mParentEnabled is only actually used by alt-tab hotkeys (though it
// may have other uses in the future, which is why it's implemented and named the way it is).
hk->mParentEnabled = true;
else // This hotkey is becoming alt-tab.
{
// Make the hook mandatory for this hotkey. Known limitation: For maintainability and code size,
// this is never undone (even if the hotkey is changed back to non-alt-tab) because there are
// many other reasons a hotkey's mKeybdHookMandatory could be true, so can't change it back to
// false without checking all those other things.
if (HK_TYPE_CAN_BECOME_KEYBD_HOOK(hk->mType))
hk->mKeybdHookMandatory = true; // Causes mType to be set to HK_KEYBD_HOOK by ManifestAllHotkeysHotstringsHooks().
}
// Even if it's still an alt-tab action (just a different one), hook's data structures still
// need to be updated. Otherwise, this is a change from an alt-tab type to a non-alt-tab type,
// or vice versa: Due to the complexity of registered vs. hook hotkeys, for now just start from
// scratch so that there is high confidence that the hook and all registered hotkeys, including
// their interdependencies, will be re-initialized correctly.
update_all_hotkeys = true;
}
// If the above changed the action from an Alt-tab type to non-alt-tab, there may be a label present
// to be applied to the existing variant (or created as a new variant).
if (aJumpToLabel) // COMMAND (update hotkey): Hotkey, Name, LabelName [, Options]
{
// If there's a matching variant, update it's label. Otherwise, create a new variant.
if (variant) // There's an existing variant...
{
if (aJumpToLabel != variant->mJumpToLabel) // ...and it's label is being changed.
{
// If the new label's exempt status is different than the old's, re-manifest all hotkeys
// in case the new presence/absence of this variant impacts other hotkeys.
// v1.0.42: However, this only needs to be done if Suspend is currently turned on,
// since otherwise the change in exempt status can't change whether this variant is
// currently in effect.
if (variant->mEnabled && g_IsSuspended && aJumpToLabel->IsExemptFromSuspend() != variant->mJumpToLabel->IsExemptFromSuspend())
update_all_hotkeys = true;
variant->mJumpToLabel = aJumpToLabel; // Must be done only after the above has finished using the old mJumpToLabel.
// Older comment:
// If this hotkey is currently a static hotkey (one not created by the Hotkey command):
// Even though it's about to be transformed into a dynamic hotkey via the Hotkey command,
// mName can be left pointing to the original Label::mName memory because that should
// never change; it will always contain the true name of this hotkey, namely its
// keystroke+modifiers (e.g. ^!c).
}
}
else // No existing variant matching current #IfWin critieria, so create a new variant.
{
if ( !(variant = hk->AddVariant(aJumpToLabel, suffix_has_tilde)) ) // Out of memory.