-
Notifications
You must be signed in to change notification settings - Fork 1
/
script_menu.cpp
1434 lines (1287 loc) · 59.1 KB
/
script_menu.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 ([email protected])
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 "script.h"
#include "globaldata.h" // for a lot of things
#include "application.h" // for MsgSleep()
#include "window.h" // for SetForegroundWindowEx()
ResultType Script::PerformMenu(char *aMenu, char *aCommand, char *aParam3, char *aParam4, char *aOptions)
{
/*
if (mMenuUseErrorLevel)
g_ErrorLevel->Assign(ERRORLEVEL_NONE); // Set default, which is "none" for the Menu command.
#define RETURN_MENU_ERROR(msg, info) return mMenuUseErrorLevel ? g_ErrorLevel->Assign(ERRORLEVEL_ERROR) \
: ScriptError(msg ERR_ABORT, info)
#define RETURN_IF_NOT_TRAY if (!is_tray) RETURN_MENU_ERROR(ERR_MENUTRAY, aMenu)
MenuCommands menu_command = Line::ConvertMenuCommand(aCommand);
if (menu_command == MENU_CMD_INVALID)
RETURN_MENU_ERROR(ERR_PARAM2_INVALID, aCommand);
bool is_tray = !stricmp(aMenu, "tray");
// Handle early on anything that doesn't require the menu to be found or created:
switch(menu_command)
{
case MENU_CMD_USEERRORLEVEL:
mMenuUseErrorLevel = (Line::ConvertOnOff(aParam3) != TOGGLED_OFF);
// Even though the state may have changed by the above, it doesn't seem necessary
// to adjust on the fly for the purpose of this particular return. In other words,
// the old mode will be in effect for this one return:
return OK;
case MENU_CMD_TIP:
RETURN_IF_NOT_TRAY;
if (*aParam3)
{
if (!mTrayIconTip)
mTrayIconTip = SimpleHeap::Malloc(sizeof(mNIC.szTip)); // SimpleHeap improves avg. case mem load.
if (mTrayIconTip)
strlcpy(mTrayIconTip, aParam3, sizeof(mNIC.szTip));
}
else // Restore tip to default.
if (mTrayIconTip)
*mTrayIconTip = '\0';
if (mNIC.hWnd) // i.e. only update the tip if the tray icon exists (can't work otherwise).
{
UPDATE_TIP_FIELD
Shell_NotifyIcon(NIM_MODIFY, &mNIC); // Currently not checking its result (e.g. in case a shell other than Explorer is running).
}
return OK;
case MENU_CMD_ICON:
{
RETURN_IF_NOT_TRAY;
bool mIconFrozen_prev = mIconFrozen;
if (*aOptions) // i.e. if it's blank, don't change the current setting of mIconFrozen.
mIconFrozen = (ATOI(aOptions) == 1);
if (!*aParam3)
{
g_NoTrayIcon = false;
if (!mNIC.hWnd) // The icon doesn't exist, so create it.
{
CreateTrayIcon();
UpdateTrayIcon(true); // Force the icon into the correct pause/suspend state.
}
else if (!mIconFrozen && mIconFrozen_prev) // To cause "Menu Tray, Icon,,, 0" to update the icon while the script is suspended.
UpdateTrayIcon(true);
return OK;
}
// Otherwise, user has specified a custom icon:
if (*aParam3 == '*' && !*(aParam3 + 1)) // Restore the standard icon.
{
if (mCustomIcon)
{
GuiType::DestroyIconIfUnused(mCustomIcon); // v1.0.37.07: Solves reports of Gui windows losing their icons.
// If the above doesn't destroy the icon, the GUI window(s) still using it are responsible for
// destroying it later.
mCustomIcon = NULL; // To indicate that there is no custom icon.
if (mCustomIconFile)
*mCustomIconFile = '\0';
mCustomIconNumber = 0;
UpdateTrayIcon(true); // Need to use true in this case too.
}
return OK;
}
// v1.0.43.03: Load via LoadPicture() vs. ExtractIcon() because ExtractIcon harms the quality
// of 16x16 icons inside .ico files by first scaling them to 32x32 (which then has to be scaled
// back to 16x16 for the tray and for the SysMenu icon). I've visually confirmed that the
// distortion occurs at least when a 16x16 icon is loaded by ExtractIcon() then put into the
// tray. It might not be the scaling itself that distorts the icon: the pixels are all in the
// right places, it's just that some are the wrong color/shade. This implies that some kind of
// unwanted interpolation or color tweaking is being done by ExtractIcon (and probably LoadIcon),
// but not by LoadImage.
// Also, load the icon at actual size so that when/if this icon is used for a GUI window, its
// appearance in the alt-tab menu won't be unexpectedly poor due to having been scaled from its
// native size down to 16x16.
int icon_number;
if (*aParam4)
{
icon_number = ATOI(aParam4);
if (icon_number < 1) // Must validate for use in two places below.
icon_number = 1; // Must be >0 to tell LoadPicture that "icon must be loaded, never a bitmap".
}
else
icon_number = 1; // One vs. Zero tells LoadIcon: "must load icon, never a bitmap (e.g. no gif/jpg/png)".
int image_type;
HICON new_icon;
if ( !(new_icon = (HICON)LoadPicture(aParam3, 0, 0, image_type, icon_number, false)) ) // Called with icon_number > 0, it guarantees return of an HICON/HCURSOR, never an HBITMAP.
RETURN_MENU_ERROR("Can't load icon.", aParam3);
GuiType::DestroyIconIfUnused(mCustomIcon); // This destroys it if non-NULL and it's not used by an GUI windows.
mCustomIcon = new_icon;
mCustomIconNumber = icon_number;
// Allocate the full MAX_PATH in case the contents grow longer later.
// SimpleHeap improves avg. case mem load:
if (!mCustomIconFile)
mCustomIconFile = SimpleHeap::Malloc(MAX_PATH);
if (mCustomIconFile)
{
// Get the full path in case it's a relative path. This is documented and it's done in case
// the script ever changes its working directory:
char full_path[MAX_PATH], *filename_marker;
if (GetFullPathName(aParam3, sizeof(full_path) - 1, full_path, &filename_marker))
strlcpy(mCustomIconFile, full_path, MAX_PATH);
else
strlcpy(mCustomIconFile, aParam3, MAX_PATH);
}
if (!g_NoTrayIcon)
UpdateTrayIcon(true); // Need to use true in this case too.
return OK;
}
case MENU_CMD_NOICON:
RETURN_IF_NOT_TRAY;
g_NoTrayIcon = true;
if (mNIC.hWnd) // Since it exists, destroy it.
{
Shell_NotifyIcon(NIM_DELETE, &mNIC); // Remove it.
mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
// but don't do DestroyMenu() on mTrayMenu->mMenu (if non-NULL) since it may have been
// changed by the user to have the custom items on top of the standard items,
// for example, and we don't want to lose that ordering in case the script turns
// the icon back on at some future time during this session.
}
return OK;
case MENU_CMD_CLICK:
RETURN_IF_NOT_TRAY;
mTrayMenu->mClickCount = ATOI(aParam3);
if (mTrayMenu->mClickCount < 1)
mTrayMenu->mClickCount = 1; // Single-click to activate menu's default item.
else if (mTrayMenu->mClickCount > 2)
mTrayMenu->mClickCount = 2; // Double-click.
return OK;
case MENU_CMD_MAINWINDOW:
RETURN_IF_NOT_TRAY;
#ifdef AUTOHOTKEYSC
if (!g_AllowMainWindow)
{
g_AllowMainWindow = true;
EnableOrDisableViewMenuItems(GetMenu(g_hWnd), MF_ENABLED); // Added as a fix in v1.0.47.06.
// Rather than using InsertMenu() to insert the item in the right position,
// which makes the code rather unmaintainable, it seems best just to recreate
// the entire menu. This will result in the standard menu items going back
// up to the top of the menu if the user previously had them at the bottom,
// but it seems too rare to worry about, especially since it's easy to
// work around that:
if (mTrayMenu->mIncludeStandardItems)
mTrayMenu->Destroy(); // It will be recreated automatically the next time the user displays it.
// else there's no need.
}
#endif
return OK;
case MENU_CMD_NOMAINWINDOW:
RETURN_IF_NOT_TRAY;
#ifdef AUTOHOTKEYSC
if (g_AllowMainWindow)
{
g_AllowMainWindow = false;
EnableOrDisableViewMenuItems(GetMenu(g_hWnd), MF_DISABLED | MF_GRAYED); // Added as a fix in v1.0.47.06.
// See comments in the prior case above for why it's done this way vs. using DeleteMenu():
if (mTrayMenu->mIncludeStandardItems)
mTrayMenu->Destroy(); // It will be recreated automatically the next time the user displays it.
// else there's no need.
}
#endif
return OK;
} // switch()
// Now that most opportunities to return an error have passed, find or create the menu, since
// all the commands that haven't already been fully handled above will need it:
UserMenu *menu = FindMenu(aMenu);
if (!menu)
{
// Menus can be created only in conjuction with the ADD command. Update: As of v1.0.25.12, they can
// also be created with the "Menu, MyMenu, Standard" command.
if (menu_command != MENU_CMD_ADD && menu_command != MENU_CMD_STANDARD)
RETURN_MENU_ERROR(ERR_MENU, aMenu);
if ( !(menu = AddMenu(aMenu)) )
RETURN_MENU_ERROR("Menu name too long.", aMenu); // Could also be "out of mem" but that's too rare to display.
}
// The above has found or added the menu for use below.
switch(menu_command)
{
case MENU_CMD_SHOW:
return menu->Display(true, *aParam3 ? ATOI(aParam3) : COORD_UNSPECIFIED, *aParam4 ? ATOI(aParam4) : COORD_UNSPECIFIED);
case MENU_CMD_ADD:
if (*aParam3) // Since a menu item name was given, it's not a separator line.
break; // Let a later switch() handle it.
if (!menu->AddItem("", 0, NULL, NULL, ""))
RETURN_MENU_ERROR(ERR_OUTOFMEM, ""); // Out of mem should be the only possibility in this case.
return OK;
case MENU_CMD_DELETE:
if (*aParam3) // Since a menu item name was given, an item is being deleted, not the whole menu.
break; // Let a later switch() handle it.
if (menu == mTrayMenu)
RETURN_MENU_ERROR("Tray menu must not be deleted.", "");
if (!ScriptDeleteMenu(menu))
RETURN_MENU_ERROR("Can't delete menu (in use?).", menu->mName); // Possibly in use as a menu bar.
return OK;
case MENU_CMD_DELETEALL:
if (!menu->DeleteAllItems())
RETURN_MENU_ERROR("Can't delete items (in use?).", menu->mName); // Possibly in use as a menu bar.
return OK;
case MENU_CMD_DEFAULT:
if (*aParam3) // Since a menu item has been specified, let a later switch() handle it.
break;
//else no menu item, so it's the same as NoDefault: fall through to the next case.
case MENU_CMD_NODEFAULT:
return menu->SetDefault();
case MENU_CMD_STANDARD:
menu->IncludeStandardItems(); // Since failure is very rare, no check of its return value is done.
return OK;
case MENU_CMD_NOSTANDARD:
menu->ExcludeStandardItems(); // Since failure is very rare, no check of its return value is done.
return OK;
case MENU_CMD_COLOR:
menu->SetColor(aParam3, stricmp(aParam4, "Single"));
return OK;
}
// All the remaining commands need a menu item to operate upon, or some other requirement met below.
char *new_name = "";
if (menu_command == MENU_CMD_RENAME) // aParam4 contains the menu item's new name in this case.
{
new_name = aParam4;
aParam4 = "";
}
// The above has handled all cases that don't require a menu item to be found or added,
// including the adding separator lines. So at the point, it is necessary to either find
// or create a menu item. The latter only occurs for the ADD command.
if (!*aParam3)
RETURN_MENU_ERROR("Parameter #3 must not be blank in this case.", "");
// Find the menu item name AND its previous item (needed for the DELETE command) in the linked list:
UserMenuItem *mi, *menu_item = NULL, *menu_item_prev = NULL; // Set defaults.
for (menu_item = menu->mFirstMenuItem
; menu_item
; menu_item_prev = menu_item, menu_item = menu_item->mNextMenuItem)
if (!lstrcmpi(menu_item->mName, aParam3)) // Match found (case insensitive).
break;
// Whether an existing menu item's options should be updated without updating its submenu or label:
bool update_exiting_item_options = (menu_command == MENU_CMD_ADD && menu_item && !*aParam4 && *aOptions);
// Seems best to avoid performance enhancers such as (Label *)mAttribute here, since the "Menu"
// command has so many modes of operation that would be difficult to parse at load-time:
Label *target_label = NULL; // Set default.
UserMenu *submenu = NULL; // Set default.
if (menu_command == MENU_CMD_ADD && !update_exiting_item_options) // Labels and submenus are only used in conjuction with the ADD command.
{
if (!*aParam4) // Allow the label/submenu to default to the menu name.
aParam4 = aParam3; // Note that aParam3 will be blank in the case of a separator line.
if (*aParam4)
{
if (*aParam4 == ':') // It's a submenu.
{
++aParam4;
if ( !(submenu = FindMenu(aParam4)) )
RETURN_MENU_ERROR(ERR_SUBMENU, aParam4);
// Before going further: since a submenu has been specified, make sure that the parent
// menu is not included anywhere in the nested hierarchy of that submenu's submenus.
// The OS doesn't seem to like that, creating empty or strange menus if it's attempted:
if ( submenu && (submenu == menu || submenu->ContainsMenu(menu)) )
RETURN_MENU_ERROR("Submenu must not contain its parent menu.", aParam4);
}
else // It's a label.
if ( !(target_label = FindLabel(aParam4)) )
RETURN_MENU_ERROR(ERR_NO_LABEL, aParam4);
}
}
if (!menu_item) // menu item doesn't exist, so create it (but only if the command is ADD).
{
if (menu_command != MENU_CMD_ADD)
// Seems best not to create menu items on-demand like this because they might get put into
// an incorrect position (i.e. it seems better than menu changes be kept separate from
// menu additions):
RETURN_MENU_ERROR("Nonexistent menu item.", aParam3);
// Otherwise: Adding a new item that doesn't yet exist.
// Need to find a menuID that isn't already in use by one of the other menu items.
// But also need to conserve menu items since only a relatively small number of IDs is available.
// Can't simply use ID_USER_FIRST + mMenuItemCount because: 1) There might be more than one
// user defined menu; 2) a menu item in the middle of the list may have been deleted,
// in which case that value would already be in use by the last item.
// Update: Now using caching of last successfully found free-ID to greatly improve avg.
// performance, especially for menus that contain thousands of items and submenus, such as
// ones that are built to mirror an entire nested directory structure. Caching should
// improve performance even after all menu IDs within the available range have been
// allocated once (via adding and deleting menus + menu items) since large blocks of free IDs
// should be free, and on average, the caching will exploit these large free blocks. However,
// if large amounts of menus and menu items are continually deleted and re-added by a script,
// the pool of free IDs will become fragmented over time, which will reduce performance.
// Since that kind of script behavior seems very rare, no attempt is made to "defragment".
// If more performance is needed in the future (seems unlikely for 99.9999% of scripts),
// could maintain an field of ~64000 bits, each bit representing whether a menu item ID is
// free. Then, every time a menu or one or more of its IDs is deleted or added, the corresponding
// ID could be marked as free/taken. That would add quite a bit of complexity to the menu
// delete code, however, and it would reduce the overall maintainability. So it definitely
// doesn't seem worth it, especially since Windows XP seems to have trouble even displaying
// menus larger than around 15000-25000 items.
static UINT sLastFreeID = ID_USER_FIRST - 1;
// Increment by one for each new search, both due to the above line and because the
// last-found free ID has a high likelyhood of still being in use:
++sLastFreeID;
bool id_in_use;
UserMenu *m;
// Note that the i variable is used to force the loop to complete exactly one full
// circuit through all available IDs, regardless of where the starting/cached value:
for (int i = 0; i < (ID_USER_LAST - ID_USER_FIRST + 1); ++i, ++sLastFreeID) // FOR EACH ID
{
if (sLastFreeID > ID_USER_LAST)
sLastFreeID = ID_USER_FIRST; // Wrap around to the beginning so that one complete circuit is made.
id_in_use = false; // Reset the default each iteration (overridden if the below finds a match).
for (m = mFirstMenu; m; m = m->mNextMenu) // FOR EACH MENU
{
for (mi = m->mFirstMenuItem; mi; mi = mi->mNextMenuItem) // FOR EACH MENU ITEM
{
if (mi->mMenuID == sLastFreeID)
{
id_in_use = true;
break;
}
}
if (id_in_use) // No point in searching the other menus, since it's now known to be in use.
break;
}
if (!id_in_use) // Break before the loop increments sLastFreeID.
break;
}
if (id_in_use) // All ~64000 IDs are in use!
RETURN_MENU_ERROR("Too many menu items.", aParam3); // Short msg since so rare.
if (!menu->AddItem(aParam3, sLastFreeID, target_label, submenu, aOptions))
RETURN_MENU_ERROR("Menu item name too long.", aParam3); // Can also happen due to out-of-mem, but that's too rare to display.
return OK; // Item has been successfully added with the correct properties.
} // if (!menu_item)
// Above has found the correct menu_item to operate upon (it already returned if
// the item was just created). Since the item was found, the UserMenu's popup
// menu must already exist because a UserMenu object can't have menu items unless
// its menu exists.
switch (menu_command)
{
case MENU_CMD_ADD:
// This is only reached if the ADD command is being used to update the label, submenu, or
// options of an existing menu item (since it would have returned above if the item was
// just newly created).
return menu->ModifyItem(menu_item, target_label, submenu, aOptions);
case MENU_CMD_RENAME:
if (!menu->RenameItem(menu_item, new_name))
RETURN_MENU_ERROR("Menu item name already in use (or too long).", new_name);
return OK;
case MENU_CMD_CHECK:
return menu->CheckItem(menu_item);
case MENU_CMD_UNCHECK:
return menu->UncheckItem(menu_item);
case MENU_CMD_TOGGLECHECK:
return menu->ToggleCheckItem(menu_item);
case MENU_CMD_ENABLE:
return menu->EnableItem(menu_item);
case MENU_CMD_DISABLE: // Disables and grays the item.
return menu->DisableItem(menu_item);
case MENU_CMD_TOGGLEENABLE:
return menu->ToggleEnableItem(menu_item);
case MENU_CMD_DEFAULT:
return menu->SetDefault(menu_item);
case MENU_CMD_DELETE:
return menu->DeleteItem(menu_item, menu_item_prev);
} // switch()
return FAIL; // Should never be reached, but avoids compiler warning and improves bug detection.
*/
}
UserMenu *Script::FindMenu(char *aMenuName)
// Returns the UserMenu whose name matches aMenuName, or NULL if not found.
{
/*
if (!aMenuName || !*aMenuName) return NULL;
for (UserMenu *menu = mFirstMenu; menu != NULL; menu = menu->mNextMenu)
if (!lstrcmpi(menu->mName, aMenuName)) // Match found.
return menu;
return NULL; // No match found.
*/
}
UserMenu *Script::AddMenu(char *aMenuName)
// Caller must have already ensured aMenuName doesn't exist yet in the list.
// Returns the newly created UserMenu object.
{
/*
if (!aMenuName || !*aMenuName) return NULL;
size_t length = strlen(aMenuName);
if (length > MAX_MENU_NAME_LENGTH)
return NULL; // Caller should show error if desired.
// After mem is allocated, the object takes charge of its later deletion:
char *name_dynamic = new char[length + 1]; // +1 for terminator.
if (!name_dynamic)
return NULL; // Caller should show error if desired.
strcpy(name_dynamic, aMenuName);
UserMenu *menu = new UserMenu(name_dynamic);
if (!menu)
{
delete name_dynamic;
return NULL; // Caller should show error if desired.
}
if (!mFirstMenu)
mFirstMenu = mLastMenu = menu;
else
{
mLastMenu->mNextMenu = menu;
// This must be done after the above:
mLastMenu = menu;
}
++mMenuCount; // Only after memory has been successfully allocated.
return menu;
*/
}
ResultType Script::ScriptDeleteMenu(UserMenu *aMenu)
// Deletes a UserMenu object and all the UserMenuItem objects that belong to it.
// Any UserMenuItem object that has a submenu attached to it does not result in
// that submenu being deleted, even if no other menus are using that submenu
// (i.e. the user must delete all menus individually). Any menus which have
// aMenu as one of their submenus will have that menu item deleted from their
// menus to avoid any chance of problems due to non-existent or NULL submenus.
{
/*
// Delete any other menu's menu item that has aMenu as its attached submenu:
UserMenuItem *mi, *mi_prev, *mi_to_delete;
for (UserMenu *m = mFirstMenu; m; m = m->mNextMenu)
if (m != aMenu) // Don't bother with this menu even if it's submenu of itself, since it will be destroyed anyway.
for (mi = m->mFirstMenuItem, mi_prev = NULL; mi;)
{
mi_to_delete = mi;
mi = mi->mNextMenuItem;
if (mi_to_delete->mSubmenu == aMenu)
m->DeleteItem(mi_to_delete, mi_prev);
else
mi_prev = mi_to_delete;
}
// Remove aMenu from the linked list. First find the item that occurs prior the aMenu in the list:
UserMenu *aMenu_prev;
for (aMenu_prev = mFirstMenu; aMenu_prev; aMenu_prev = aMenu_prev->mNextMenu)
if (aMenu_prev->mNextMenu == aMenu)
break;
if (aMenu == mLastMenu)
mLastMenu = aMenu_prev; // Can be NULL if the list will now be empty.
if (aMenu_prev) // there is another item prior to aMenu in the linked list.
aMenu_prev->mNextMenu = aMenu->mNextMenu; // Can be NULL if aMenu was the last one.
else // aMenu was the first one in the list.
mFirstMenu = aMenu->mNextMenu; // Can be NULL if the list will now be empty.
// Do this last when its contents are no longer needed. Its destructor will delete all
// the items in the menu and destroy the OS menu itself:
aMenu->DeleteAllItems(); // This also calls Destroy() to free the menu's resources.
if (aMenu->mBrush) // Free the brush used for the menu's background color.
DeleteObject(aMenu->mBrush);
delete aMenu->mName; // Since it was separately allocated.
delete aMenu;
--mMenuCount;
return OK;
*/
}
// Macros for use with the below methods:
#define aMenuItem_ID (aMenuItem->mSubmenu ? GetSubmenuPos(aMenuItem->mSubmenu->mMenu) : aMenuItem->mMenuID)
#define aMenuItem_MF_BY (aMenuItem->mSubmenu ? MF_BYPOSITION : MF_BYCOMMAND)
#define UPDATE_GUI_MENU_BARS(menu_type, hmenu) \
if (menu_type == MENU_TYPE_BAR && GuiType::sGuiCount)\
GuiType::UpdateMenuBars(hmenu); // Above: If it's not a popup, it's probably a menu bar.
#ifdef AUTOHOTKEYSC
#define CHANGE_DEFAULT_IF_NEEDED \
if (mDefault == aMenuItem)\
{\
if (mMenu)\
{\
if (this == g_script.mTrayMenu)\
SetMenuDefaultItem(mMenu, mIncludeStandardItems && g_AllowMainWindow ? ID_TRAY_OPEN : -1, FALSE);\
else\
SetMenuDefaultItem(mMenu, -1, FALSE);\
}\
mDefault = NULL;\
}
#else
#define CHANGE_DEFAULT_IF_NEEDED \
if (mDefault == aMenuItem)\
{\
if (mMenu)\
{\
if (this == g_script.mTrayMenu)\
SetMenuDefaultItem(mMenu, mIncludeStandardItems ? ID_TRAY_OPEN : -1, FALSE);\
else\
SetMenuDefaultItem(mMenu, -1, FALSE);\
}\
mDefault = NULL;\
}
#endif
ResultType UserMenu::AddItem(char *aName, UINT aMenuID, Label *aLabel, UserMenu *aSubmenu, char *aOptions)
// Caller must have already ensured that aName does not yet exist as a user-defined menu item
// in this->mMenu.
{
/*
size_t length = strlen(aName);
if (length > MAX_MENU_NAME_LENGTH)
return FAIL; // Caller should show error if desired.
// After mem is allocated, the object takes charge of its later deletion:
char *name_dynamic;
if (length)
{
if ( !(name_dynamic = new char[length + 1]) ) // +1 for terminator.
return FAIL; // Caller should show error if desired.
strcpy(name_dynamic, aName);
}
else
name_dynamic = Var::sEmptyString; // So that it can be detected as a non-allocated empty string.
UserMenuItem *menu_item = new UserMenuItem(name_dynamic, length + 1, aMenuID, aLabel, aSubmenu, this);
if (!menu_item) // Should also be very rare.
{
if (name_dynamic != Var::sEmptyString)
delete name_dynamic;
return FAIL; // Caller should show error if desired.
}
if (!mFirstMenuItem)
mFirstMenuItem = mLastMenuItem = menu_item;
else
{
mLastMenuItem->mNextMenuItem = menu_item;
// This must be done after the above:
mLastMenuItem = menu_item;
}
++mMenuItemCount; // Only after memory has been successfully allocated.
if (*aOptions)
UpdateOptions(menu_item, aOptions);
return OK;
*/
}
UserMenuItem::UserMenuItem(char *aName, size_t aNameCapacity, UINT aMenuID, Label *aLabel, UserMenu *aSubmenu, UserMenu *aMenu)
// UserMenuItem Constructor.
: mName(aName), mNameCapacity(aNameCapacity), mMenuID(aMenuID), mLabel(aLabel), mSubmenu(aSubmenu), mMenu(aMenu)
, mPriority(0) // default priority = 0
, mEnabled(true), mChecked(false), mNextMenuItem(NULL)
{
/*
if (aMenu->mMenu)
{
if (aSubmenu) // Ensure the menu is created so that AppendMenu() will function properly.
aSubmenu->Create();
AppendMenu(aMenu->mMenu, (*aName ? MF_STRING : MF_SEPARATOR) | (aSubmenu ? MF_POPUP : 0)
, aSubmenu ? (UINT_PTR)aSubmenu->mMenu : aMenuID, aName);
UPDATE_GUI_MENU_BARS(aMenu->mMenuType, aMenu->mMenu)
}
*/
}
ResultType UserMenu::DeleteItem(UserMenuItem *aMenuItem, UserMenuItem *aMenuItemPrev)
{
/*
// Remove this menu item from the linked list:
if (aMenuItem == mLastMenuItem)
mLastMenuItem = aMenuItemPrev; // Can be NULL if the list will now be empty.
if (aMenuItemPrev) // there is another item prior to aMenuItem in the linked list.
aMenuItemPrev->mNextMenuItem = aMenuItem->mNextMenuItem; // Can be NULL if aMenuItem was the last one.
else // aMenuItem was the first one in the list.
mFirstMenuItem = aMenuItem->mNextMenuItem; // Can be NULL if the list will now be empty.
CHANGE_DEFAULT_IF_NEEDED // Should do this before freeing aMenuItem's memory.
if (mMenu) // Delete the item from the menu.
DeleteMenu(mMenu, aMenuItem_ID, aMenuItem_MF_BY);
if (aMenuItem->mName != Var::sEmptyString)
delete aMenuItem->mName; // Since it was separately allocated.
delete aMenuItem; // Do this last when its contents are no longer needed.
--mMenuItemCount;
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Verified as being necessary.
return OK;
*/
}
ResultType UserMenu::DeleteAllItems()
{
/*
if (!mFirstMenuItem)
return OK; // If there are no user-defined menu items, it's already in the correct state.
// Remove all menu items from the linked list and from the menu. First destroy the menu since
// it's probably better to start off fresh than have the destructor individually remove each
// menu item as the items in the linked list are deleted. In addition, this avoids the need
// to find any submenus by position:
if (!Destroy()) // if mStandardMenuItems is true, the menu will be recreated later when needed.
// If menu can't be destroyed, it's probably due to it being attached as a menu bar to an existing
// GUI window. In this case, when the window is destroyed, the menu bar will be too, so it's
// probably best to do nothing. If we were called as a result of "menu, MenuName, Delete", it
// is documented that this will fail in this case.
return FAIL;
// The destructor relies on the fact that the above destroys the menu but does not recreate it.
// This is because popup menus, not being affiliated with a window (unless they're attached to
// a menu bar, but that isn't the case with our GUI windows, which detach such menus prior to
// when the GUI window is destroyed in case the menu is in use by another window), must be
// destroyed with DestroyMenu() to ensure a clean exit (resources freed).
UserMenuItem *menu_item_to_delete;
for (UserMenuItem *mi = mFirstMenuItem; mi;)
{
menu_item_to_delete = mi;
mi = mi->mNextMenuItem;
if (menu_item_to_delete->mName != Var::sEmptyString)
delete menu_item_to_delete->mName; // Since it was separately allocated.
delete menu_item_to_delete;
}
mFirstMenuItem = mLastMenuItem = NULL;
mMenuItemCount = 0;
mDefault = NULL; // i.e. there can't be a *user-defined* default item anymore, even if this is the tray.
return OK;
*/
}
ResultType UserMenu::ModifyItem(UserMenuItem *aMenuItem, Label *aLabel, UserMenu *aSubmenu, char *aOptions)
// Modify the label, submenu, or options of a menu item (exactly one of these should be NULL and the
// other not except when updating only the options).
// If a menu item becomes a submenu, we don't relinquish its ID in case it's ever made a normal item
// again (avoids the need to re-lookup a unique ID).
{
/*
if (*aOptions)
UpdateOptions(aMenuItem, aOptions);
if (!aLabel && !aSubmenu) // We were called only to update this item's options.
return OK;
aMenuItem->mLabel = aLabel; // This will be NULL if this menu item is a separator or submenu.
if (aMenuItem->mSubmenu == aSubmenu) // Below relies on this check.
return OK;
if (!mMenu)
{
aMenuItem->mSubmenu = aSubmenu; // Just set the indicator for when the menu is later created.
return OK;
}
// Otherwise, since the OS menu exists, one of these is to be done to aMenuItem in it:
// 1) Change a submenu to point to a different menu.
// 2) Change a submenu so that it becomes a normal menu item.
// 3) Change a normal menu item into a submenu.
// Since Create() ensures that aSubmenu is non-null whenever this->mMenu is non-null, this is just
// an extra safety check in case some other method destroyed aSubmenu since then:
if (aSubmenu)
if (!aSubmenu->Create()) // Create if needed. No error msg since so rare.
return FAIL;
MENUITEMINFO mii;
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_SUBMENU | MIIM_ID;
mii.hSubMenu = aSubmenu ? aSubmenu->mMenu : NULL;
// If this submenu is being back into a normal menu item, the ID must be re-specified
// because an item that was formerly submenu will not have a real ID due to OS behavior:
mii.wID = aMenuItem->mMenuID;
if (SetMenuItemInfo(mMenu, aMenuItem_ID, aMenuItem->mSubmenu != NULL, &mii))
{
// Submenu was just made into a different submenu or converted into a normal menu item.
// Since the OS (as an undocumented side effect) sometimes destroys the menu itself when
// a submenu is changed in this way, update our state to indicate that the menu handle
// is no longer valid:
if (aMenuItem->mSubmenu && aMenuItem->mSubmenu->mMenu && !IsMenu(aMenuItem->mSubmenu->mMenu))
{
UserMenu *temp = aMenuItem->mSubmenu;
aMenuItem->mSubmenu = aSubmenu; // Should be done before the below so that Destroy() sees the change.
// The following shouldn't fail because submenus are popup menus, and popup menus can't be
// menu bars. Update: Even if it does fail due to causing a cascade-destroy upward toward any
// menu bar that happens to own it, it seems okay because the real purpose here is simply to
// update that fact that "temp" was already destroyed indirectly by the OS, as evidenced by
// the fact that IsMenu() returned FALSE above.
temp->Destroy();
}
else
aMenuItem->mSubmenu = aSubmenu;
}
// else no error msg and return OK so that the thread will continue. This may help catch
// bugs in the course of normal use of this feature.
return OK;
*/
}
void UserMenu::UpdateOptions(UserMenuItem *aMenuItem, char *aOptions)
{
/*
if (toupper(*aOptions) == 'P')
aMenuItem->mPriority = atoi(aOptions + 1);
*/
}
ResultType UserMenu::RenameItem(UserMenuItem *aMenuItem, char *aNewName)
// Caller should specify "" for aNewName to convert aMenuItem into a separator.
// Returns FAIL if the new name conflicts with an existing name.
{
/*
if (strlen(aNewName) > MAX_MENU_NAME_LENGTH)
return FAIL; // Caller should diplay error if desired.
if (!mMenu) // Just update the member variables for later use when the menu is created.
return UpdateName(aMenuItem, aNewName);
MENUITEMINFO mii;
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_TYPE;
mii.dwTypeData = aNewName;
if (*aNewName)
{
// Names must be unique only within each menu:
for (UserMenuItem *mi = mFirstMenuItem; mi; mi = mi->mNextMenuItem)
if (!lstrcmpi(mi->mName, aNewName)) // Match found (case insensitive).
return FAIL; // Caller should display an error message.
mii.fType = MFT_STRING;
}
else // converting into a separator
{
// Notes about the below macro:
// ID_TRAY_OPEN is not set to be the default for the self-contained version, since it lacks that menu item.
CHANGE_DEFAULT_IF_NEEDED
mii.fType = MFT_SEPARATOR;
if (aMenuItem->mSubmenu) // Converting submenu into a separator.
{
mii.fMask |= MIIM_SUBMENU;
mii.hSubMenu = NULL;
}
}
// Failure is rare enough in the below that no attempt is made to undo the above:
BOOL result = SetMenuItemInfo(mMenu, aMenuItem_ID, aMenuItem->mSubmenu != NULL, &mii);
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Verified as being necessary.
return result ? UpdateName(aMenuItem, aNewName) : FAIL;
*/
}
ResultType UserMenu::UpdateName(UserMenuItem *aMenuItem, char *aNewName)
// Caller should already have ensured that aMenuItem is not too long.
{
/*
size_t new_length = strlen(aNewName);
if (new_length)
{
if (new_length >= aMenuItem->mNameCapacity) // Too small, so reallocate.
{
// Use a temp var. so that mName will never wind up being NULL (relied on by other things).
// This also retains the original menu name if the allocation fails:
char *temp = new char[new_length + 1]; // +1 for terminator.
if (!temp)
return FAIL;
// Otherwise:
if (aMenuItem->mName != Var::sEmptyString) // Since it was previously new'd, delete it.
delete aMenuItem->mName;
aMenuItem->mName = temp;
aMenuItem->mNameCapacity = new_length + 1;
}
strcpy(aMenuItem->mName, aNewName);
}
else // It will become a separator.
{
*aMenuItem->mName = '\0'; // Safe because even if it's capacity is 1 byte, it's a writable byte.
aMenuItem->mMenuID = 0; // Free up an ID since separators currently can't be converted back into items.
}
return OK;
*/
}
ResultType UserMenu::CheckItem(UserMenuItem *aMenuItem)
// Note that items in a menu bar apparently cannot be checked, so it is not necessary to call
// UPDATE_GUI_MENU_BARS here or in the next few functions below.
{
/*
aMenuItem->mChecked = true;
if (mMenu)
CheckMenuItem(mMenu, aMenuItem_ID, aMenuItem_MF_BY | MF_CHECKED);
return OK;
*/
}
ResultType UserMenu::UncheckItem(UserMenuItem *aMenuItem)
{
/*
aMenuItem->mChecked = false;
if (mMenu)
CheckMenuItem(mMenu, aMenuItem_ID, aMenuItem_MF_BY | MF_UNCHECKED);
return OK;
*/
}
ResultType UserMenu::ToggleCheckItem(UserMenuItem *aMenuItem)
{
/*
aMenuItem->mChecked = !aMenuItem->mChecked;
if (mMenu)
CheckMenuItem(mMenu, aMenuItem_ID, aMenuItem_MF_BY | (aMenuItem->mChecked ? MF_CHECKED : MF_UNCHECKED));
return OK;
*/
}
ResultType UserMenu::EnableItem(UserMenuItem *aMenuItem)
{
/*
aMenuItem->mEnabled = true;
if (mMenu)
{
EnableMenuItem(mMenu, aMenuItem_ID, aMenuItem_MF_BY | MF_ENABLED); // Automatically ungrays it too.
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Verified as being necessary.
}
return OK;
*/
}
ResultType UserMenu::DisableItem(UserMenuItem *aMenuItem)
{
/*
aMenuItem->mEnabled = false;
if (mMenu)
{
EnableMenuItem(mMenu,aMenuItem_ID, aMenuItem_MF_BY | MF_DISABLED | MF_GRAYED);
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Verified as being necessary.
}
return OK;
*/
}
ResultType UserMenu::ToggleEnableItem(UserMenuItem *aMenuItem)
{
/*
aMenuItem->mEnabled = !aMenuItem->mEnabled;
if (mMenu)
{
EnableMenuItem(mMenu, aMenuItem_ID, aMenuItem_MF_BY | (aMenuItem->mEnabled ? MF_ENABLED
: (MF_DISABLED | MF_GRAYED)));
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Verified as being necessary.
}
return OK;
*/
}
ResultType UserMenu::SetDefault(UserMenuItem *aMenuItem)
{
/*
if (mDefault == aMenuItem)
return OK;
mDefault = aMenuItem;
if (!mMenu) // No further action required: the new setting will be in effect when the menu is created.
return OK;
if (aMenuItem) // A user-defined menu item is being made the default.
SetMenuDefaultItem(mMenu, aMenuItem_ID, aMenuItem->mSubmenu != NULL); // This also ensures that only one is default at a time.
else
{
// Otherwise, a user-defined item that was previously the default is no longer the default.
// Provide a new default if this is the tray menu, the standard items are present, and a default
// action is called for:
if (this == g_script.mTrayMenu) // Necessary for proper operation of the self-contained version:
#ifdef AUTOHOTKEYSC
SetMenuDefaultItem(mMenu, g_AllowMainWindow && mIncludeStandardItems ? ID_TRAY_OPEN : -1, FALSE);
#else
SetMenuDefaultItem(mMenu, mIncludeStandardItems ? ID_TRAY_OPEN : -1, FALSE);
#endif
else
SetMenuDefaultItem(mMenu, -1, FALSE);
}
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Testing shows that menu bars themselves can have default items, and that this is necessary.
return OK;
*/
}
ResultType UserMenu::IncludeStandardItems()
{
/*
if (mIncludeStandardItems)
return OK;
// In this case, immediately create the menu to support having the standard menu items on the
// bottom or middle rather than at the top (which is the default). Older comment: Only do
// this if it was false beforehand so that the standard menu items will be appended to whatever
// the user has already added to the tray menu (increases flexibility).
if (!Create()) // It may already exist, in which case this returns OK.
return FAIL; // No error msg since so rare.
return AppendStandardItems();
*/
}
ResultType UserMenu::ExcludeStandardItems()
{
/*
if (!mIncludeStandardItems)
return OK;
mIncludeStandardItems = false;
return Destroy(); // It will be recreated automatically the next time the user displays it.
*/
}
ResultType UserMenu::Create(MenuTypeType aMenuType)
// Menu bars require non-popup menus (CreateMenu vs. CreatePopupMenu). Rather than maintain two
// different types of HMENUs on the rare chance that a script might try to use a menu both as
// a popup and a menu bar, it seems best to have only one type to keep the code simple and reduce
// resources used for the menu. This has been documented in the help file.
// Note that a menu bar's submenus can be (perhaps must be) of the popup type, so we only need
// to worry about the distinction for the menu bar itself. The caller tells us which is which.
{
/*
if (mMenu)
{
// Since menu already exists, check if it's the right type. If caller left the type unspecified,
// assume it is the right type:
if (aMenuType == MENU_TYPE_NONE || aMenuType == mMenuType)
return OK;
else // It exists but it's the wrong type. Destroy and recreate it (but keep TRAY always as popup type).