1 { $Id: Cocoawsmenus.pp 15309 2008-06-04 22:12:59Z vincents $}
2 {
3  *****************************************************************************
4  *                               CocoaWSMenus.pp                             *
5  *                               ------------                                *
6  *                                                                           *
7  *                                                                           *
8  *****************************************************************************
9 
10  *****************************************************************************
11   This file is part of the Lazarus Component Library (LCL)
12 
13   See the file COPYING.modifiedLGPL.txt, included in this distribution,
14   for details about the license.
15  *****************************************************************************
16 }
17 unit CocoaWSMenus;
18 
19 {$mode objfpc}{$H+}
20 {$modeswitch objectivec2}
21 {$include cocoadefines.inc}
22 
23 interface
24 
25 uses
26   // Libs
27   CocoaAll,
28   MacOSAll,
29   // RTL
30   sysutils,
31   // LCL
32   Controls, Forms, Menus, Graphics, LCLType, LMessages, LCLProc, Classes,
33   LCLMessageGlue, LCLStrConsts,
34   // Widgetset
35   WSMenus, WSLCLClasses,
36   // LCL Cocoa
37   Cocoa_extra,
38   CocoaPrivate, CocoaWSCommon, CocoaUtils, CocoaGDIObjects;
39 
40 type
41 
42   IMenuItemCallback = interface(ICommonCallBack)
43     procedure ItemSelected;
MenuItemTargetnull44     function MenuItemTarget: TMenuItem;
45   end;
46 
47   { TLCLMenuItemCallback }
48 
49   TLCLMenuItemCallback = class(TLCLCommonCallback, IMenuItemCallback)
50   private
51     FMenuItemTarget: TMenuItem;
52   public
53     constructor Create(AOwner: NSObject; AMenuItemTarget: TMenuItem); reintroduce;
54     procedure ItemSelected;
MenuItemTargetnull55     function MenuItemTarget: TMenuItem;
56   end;
57 
58   TCocoaMenuItem = objcclass;
59 
60   { TCocoaMenu }
61 
62   TCocoaMenu = objcclass(NSMenu)
63   private
64     appleMenu: TCocoaMenuItem;
65     attachedAppleMenu: Boolean;
66   public
67     procedure lclItemSelected(sender: id); message 'lclItemSelected:';
68     procedure createAppleMenu(); message 'createAppleMenu';
69     procedure overrideAppleMenu(AItem: TCocoaMenuItem); message 'overrideAppleMenu:';
70     procedure attachAppleMenu(); message 'attachAppleMenu';
71   end;
72 
73   { TCocoaMenuItem }
74 
75   TCocoaMenuItem = objcclass(NSMenuItem, NSMenuDelegateProtocol)
76   public
77     menuItemCallback: IMenuItemCallback;
78     attachedAppleMenuItems: Boolean;
79     FMenuItemTarget: TMenuItem;
80     procedure UncheckSiblings(AIsChangingToChecked: LCLObjCBoolean = False); message 'UncheckSiblings:';
GetMenuItemHandlenull81     function GetMenuItemHandle(): TMenuItem; message 'GetMenuItemHandle';
82     procedure lclItemSelected(sender: id); message 'lclItemSelected:';
lclGetCallbacknull83     function lclGetCallback: IMenuItemCallback; override;
84     procedure lclClearCallback; override;
85     procedure attachAppleMenuItems(); message 'attachAppleMenuItems';
isValidAppleMenunull86     function isValidAppleMenu(): LCLObjCBoolean; message 'isValidAppleMenu';
87     // menuWillOpen cannot be used. Because it SHOULD NOT change the contents
88     // of the menu. While LCL allows to modify the menu contents when the submenu
89     // is about to be activated.
90     procedure menuNeedsUpdate(AMenu: NSMenu); message 'menuNeedsUpdate:';
91     //procedure menuDidClose(AMenu: NSMenu); message 'menuDidClose:';
worksWhenModalnull92     function worksWhenModal: LCLObjCBoolean; message 'worksWhenModal';
93   end;
94 
95   TCocoaMenuItem_HideApp = objcclass(NSMenuItem)
96   public
97     procedure lclItemSelected(sender: id); message 'lclItemSelected:';
98   end;
99 
100   TCocoaMenuItem_HideOthers = objcclass(NSMenuItem)
101   public
102     procedure lclItemSelected(sender: id); message 'lclItemSelected:';
103   end;
104 
105   TCocoaMenuItem_ShowAllApp = objcclass(NSMenuItem)
106   public
107     procedure lclItemSelected(sender: id); message 'lclItemSelected:';
108   end;
109 
110   TCocoaMenuItem_Quit = objcclass(NSMenuItem)
111   public
112     procedure lclItemSelected(sender: id); message 'lclItemSelected:';
113   end;
114 
115   { TCocoaWSMenuItem }
116 
117   TCocoaWSMenuItem = class(TWSMenuItem)
118   private
119     class procedure Do_SetCheck(const ANSMenuItem: NSMenuItem; const Checked: boolean);
120     // used from the MenuMadness example
NSMenuCheckmarknull121     class function NSMenuCheckmark: NSImage;
NSMenuRadionull122     class function NSMenuRadio: NSImage;
isSeparatornull123     class function isSeparator(const ACaption: AnsiString): Boolean;
124   published
125     class procedure AttachMenu(const AMenuItem: TMenuItem); override;
CreateHandlenull126     class function  CreateHandle(const AMenuItem: TMenuItem): HMENU; override;
127     class procedure DestroyHandle(const AMenuItem: TMenuItem); override;
128     class procedure SetCaption(const AMenuItem: TMenuItem; const ACaption: string); override;
129     class procedure SetShortCut(const AMenuItem: TMenuItem; const ShortCutK1, ShortCutK2: TShortCut); override;
130     class procedure SetVisible(const AMenuItem: TMenuItem; const Visible: boolean); override;
SetChecknull131     class function SetCheck(const AMenuItem: TMenuItem; const Checked: boolean): boolean; override;
SetEnablenull132     class function SetEnable(const AMenuItem: TMenuItem; const Enabled: boolean): boolean; override;
SetRadioItemnull133     class function SetRadioItem(const AMenuItem: TMenuItem; const RadioItem: boolean): boolean; override;
SetRightJustifynull134     //class function SetRightJustify(const AMenuItem: TMenuItem; const Justified: boolean): boolean; override;
135     class procedure UpdateMenuIcon(const AMenuItem: TMenuItem; const HasIcon: Boolean; const AIcon: TBitmap); override;
136   end;
137 
138   { TCocoaWSMenu }
139 
140   TCocoaWSMenu = class(TWSMenu)
141   published
CreateHandlenull142     class function CreateHandle(const AMenu: TMenu): HMENU; override;
143   end;
144 
145   { TCocoaWSMainMenu }
146 
147   TCocoaWSMainMenu = class(TWSMainMenu)
148   published
CreateHandlenull149     class function CreateHandle(const AMenu: TMenu): HMENU; override;
150   end;
151 
152   { TCocoaWSPopupMenu }
153 
154   TCocoaWSPopupMenu = class(TWSPopupMenu)
155   published
156     class procedure Popup(const APopupMenu: TPopupMenu; const X, Y: Integer); override;
157   end;
158 
159 procedure NSMenuItemSetBitmap(mn: NSMenuItem; bmp: TBitmap);
160 
161 // the returned "Key" should not be released, as it's not memory owned
162 procedure ShortcutToKeyEquivalent(const AShortCut: TShortcut; out Key: NSString; out shiftKeyMask: NSUInteger);
163 
164 procedure ToggleAppMenu(ALogicalEnabled: Boolean);
165 
166 function AllocCocoaMenu(const atitle: string = ''): TCocoaMenu;
167 function LCLMenuItemInit(item: NSMenuItem; const atitle: string; ashortCut: TShortCut): id;
168 function LCLMenuItemInit(item: NSMenuItem; const atitle: string; VKKey: Word = 0; State: TShiftState = []): id;
169 
170 implementation
171 
172 uses
173   CocoaInt;
174 
175 function LCLMenuItemInit(item: NSMenuItem; const atitle: string; ashortCut: TShortCut): id;
176 var
177   key   : NSString;
178   mask  : NSUInteger;
179 begin
180   ShortcutToKeyEquivalent(ashortCut, key, mask);
181 
182   Result := item.initWithTitle_action_keyEquivalent(
183     ControlTitleToNSStr(Atitle),
184     objcselector('lclItemSelected:'), // Selector is Hard-coded, that's why it's LCLMenuItemInit
185     key);
186   NSMenuItem(Result).setKeyEquivalentModifierMask(mask);
187   NSMenuItem(Result).setTarget(Result);
188 end;
189 
190 function LCLMenuItemInit(item: NSMenuItem; const atitle: string; VKKey: Word; State: TShiftState): id;
191 var
192   key   : NSString;
193   mask  : NSUInteger;
194 begin
195   Result := LCLMenuItemInit(item, atitle, ShortCut(VKKey, State));
196 end;
197 
198 function AllocCocoaMenu(const atitle: string = ''): TCocoaMenu;
199 begin
200   Result := TCocoaMenu.alloc.initWithTitle(ControlTitleToNSStr(atitle));
201   Result.setAutoenablesItems(false);
202 end;
203 
204 { TCocoaMenuItem_ShowAllApp }
205 
206 procedure TCocoaMenuItem_ShowAllApp.lclItemSelected(sender: id);
207 begin
208   NSApplication(NSApp).unhideAllApplications(sender);
209 end;
210 
211 { TLCLMenuItemCallback }
212 
213 constructor TLCLMenuItemCallback.Create(AOwner: NSObject; AMenuItemTarget: TMenuItem);
214 begin
215   Owner := AOwner;
216   FMenuItemTarget := AMenuItemTarget;
217 end;
218 
219 procedure TLCLMenuItemCallback.ItemSelected;
220 var
221   Msg:TLMessage;
222 begin
223   FillChar(Msg{%H-}, SizeOf(Msg), 0);
224   Msg.msg := LM_ACTIVATE;
225   // debugln('send LM_Activate');
226   LCLMessageGlue.DeliverMessage(FMenuItemTarget,Msg);
227 end;
228 
MenuItemTargetnull229 function TLCLMenuItemCallback.MenuItemTarget: TMenuItem;
230 begin
231   Result:=FMenuItemTarget;
232 end;
233 
234 { TCocoaMenu }
235 
236 procedure TCocoaMenu.lclItemSelected(sender:id);
237 begin
238 
239 end;
240 
241 // For when there is no menu item with title 
242 procedure TCocoaMenu.createAppleMenu();
243 var
244   nskey, nstitle, nssubmeykey: NSString;
245   lNSSubmenu: NSMenu;
246 begin
247   // create the menu item
248   nstitle := NSStringUtf8('');
249   appleMenu := TCocoaMenuItem.alloc.initWithTitle_action_keyEquivalent(nstitle,
250     objcselector('lclItemSelected:'), NSString.string_);
251   nstitle.release;
252 
253   // add the submenu
254   lNSSubmenu := NSMenu.alloc.initWithTitle(NSString.string_);
255   appleMenu.setSubmenu(lNSSubmenu);
256 
257   appleMenu.attachAppleMenuItems();
258 end;
259 
260 // For when there is a menu item with title 
261 procedure TCocoaMenu.overrideAppleMenu(AItem: TCocoaMenuItem);
262 begin
263   if appleMenu <> nil then
264   begin
265     if indexOfItem(appleMenu) >= 0 then
266       removeItem(appleMenu);
267     appleMenu.release;
268     appleMenu := nil;
269   end;
270   attachedAppleMenu := False;
271   AItem.attachAppleMenuItems();
272 end;
273 
274 procedure TCocoaMenu.attachAppleMenu();
275 begin
276   if attachedAppleMenu then Exit;
277   if appleMenu = nil then Exit;
278   attachedAppleMenu := True;
279   insertItem_atIndex(appleMenu, 0);
280 end;
281 
282 { TCocoaMenuITem }
283 
284 procedure TCocoaMenuItem.UncheckSiblings(AIsChangingToChecked: LCLObjCBoolean);
285 var
286   i: Integer;
287   lMenuItem, lSibling, lParentMenu: TMenuItem;
288   lSiblingHandle: NSMenuItem;
289 begin
290   //lMenuItem := GetMenuItemHandle();
291   lMenuItem := FMenuItemTarget;
292   if lMenuItem = nil then Exit;
293   if not lMenuItem.RadioItem then Exit;
294   if (not AIsChangingToChecked) and (not lMenuItem.Checked) then Exit;
295   lParentMenu := lMenuItem.Parent;
296   if lParentMenu = nil then Exit;
297   for i := 0 to lParentMenu.Count - 1 do
298   begin
299     lSibling := lParentMenu.Items[i];
300     if lSibling = nil then Continue;
301     if lSibling = lMenuItem then Continue;
302 
303     if lSibling.RadioItem and (lSibling.GroupIndex = lMenuItem.GroupIndex) and
304       lSibling.HandleAllocated() then
305     begin
306       lSiblingHandle := NSMenuItem(lSibling.Handle);
307       TCocoaWSMenuItem.Do_SetCheck(lSiblingHandle, False);
308     end;
309   end;
310 end;
311 
GetMenuItemHandlenull312 function TCocoaMenuItem.GetMenuItemHandle(): TMenuItem;
313 begin
314   Result := nil;
315   if menuItemCallback = nil then Exit;
316   Result := menuItemCallback.MenuItemTarget;
317 end;
318 
319 procedure TCocoaMenuItem.lclItemSelected(sender:id);
320 begin
321   menuItemCallback.ItemSelected;
322   UncheckSiblings();
323 end;
324 
lclGetCallbacknull325 function TCocoaMenuItem.lclGetCallback: IMenuItemCallback;
326 begin
327   result:=menuItemCallback;
328 end;
329 
330 procedure TCocoaMenuItem.lclClearCallback;
331 begin
332   menuItemCallback := nil;
333 end;
334 
335 procedure TCocoaMenuItem.attachAppleMenuItems();
336 var
337   item    : NSMenuItem;
338 begin
339   if attachedAppleMenuItems then Exit;
340   if not hasSubmenu() then Exit;
341 
342   // Separator
343   submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count);
344 
345   // Services
346   item := LCLMenuItemInit( TCocoaMenuItem.alloc, rsMacOSMenuServices);
347   item.setTarget(nil);
348   item.setAction(nil);
349   submenu.insertItem_atIndex(item, submenu.itemArray.count);
350   item.setSubmenu(NSMenu.alloc.initWithTitle( ControlTitleToNSStr(rsMacOSMenuServices)));
351   NSApplication(NSApp).setServicesMenu(item.submenu);
352 
353   // Separator
354   submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count);
355 
356   // Hide App     Meta-H
357   item := LCLMenuItemInit( TCocoaMenuItem_HideApp.alloc, Format(rsMacOSMenuHide, [Application.Title]), VK_H, [ssMeta]);
358   submenu.insertItem_atIndex(item, submenu.itemArray.count);
359 
360   // Hide Others  Meta-Alt-H
361   item := LCLMenuItemInit( TCocoaMenuItem_HideOthers.alloc, rsMacOSMenuHideOthers, VK_H, [ssMeta, ssAlt]);
362   submenu.insertItem_atIndex(item, submenu.itemArray.count);
363 
364   // Show All
365   item := LCLMenuItemInit( TCocoaMenuItem_ShowAllApp.alloc, rsMacOSMenuShowAll);
366   submenu.insertItem_atIndex(item, submenu.itemArray.count);
367 
368   // Separator
369   submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count);
370 
371   // Quit   Meta-Q
372   item := LCLMenuItemInit( TCocoaMenuItem_Quit.alloc, Format(rsMacOSMenuQuit, [Application.Title]), VK_Q, [ssMeta]);
373   submenu.insertItem_atIndex(item, submenu.itemArray.count);
374 
375   attachedAppleMenuItems := True;
376 end;
377 
isValidAppleMenunull378 function TCocoaMenuItem.isValidAppleMenu(): LCLObjCBoolean;
379 begin
380   Result := hasSubmenu() and (submenu() <> nil);
381   Result := Result and ('' = NSStringToString(title));
382 end;
383 
384 procedure TCocoaMenuItem.menuNeedsUpdate(AMenu: NSMenu);
385 begin
386   if not Assigned(menuItemCallback) then Exit;
387   //todo: call "measureItem"
388   menuItemCallback.ItemSelected;
389 end;
390 
worksWhenModalnull391 function TCocoaMenuItem.worksWhenModal: LCLObjCBoolean;
392 begin
393   // refer to NSMenuItem.target (Apple) documentation
394   // the method must be implemented in target and return TRUE
395   // otherwise it won't work for modal!
396   //
397   // The method COULD be used to protect the main menu from being clicked
398   // if a modal window doesn't have a menu.
399   // But LCL disables (is it?) the app menu manually on modal
400   Result := true;
401 end;
402 
403 {  menuDidClose should not change the structure of the menu.
404    The restructuring is causing issues on Apple's special menus (i.e. HELP menu)
405    See bug #35625
406 
407 procedure TCocoaMenuItem.menuDidClose(AMenu: NSMenu);
408 var
409   par : NSMenu;
410   idx : NSInteger;
411   mn  : NSMenuItem;
412 begin
413   // the only purpose of this code is to "invalidate" the submenu of the item.
414   // an invalidated menu will call menuNeedsUpdate.
415   // There's no other way in Cocoa to do the "invalidate"
416   par := amenu.supermenu;
417   if Assigned(par) then
418   begin
419     idx := par.indexOfItemWithSubmenu(AMenu);
420     if idx<>NSNotFound then
421     begin
422       mn := par.itemAtIndex(idx);
423       mn.setSubmenu(nil);
424       mn.setSubmenu(AMenu);
425     end;
426   end;
427 end;
428 }
429 
430 procedure TCocoaMenuItem_HideApp.lclItemSelected(sender: id);
431 begin
432   // Applicaiton.Minimize, calls WidgetSet.AppMinimize;
433   // which calls NSApplication.hide() anyway
434   Application.Minimize;
435 end;
436 
437 procedure TCocoaMenuItem_HideOthers.lclItemSelected(sender: id);
438 begin
439   NSApplication(NSApp).hideOtherApplications(sender);
440 end;
441 
442 procedure TCocoaMenuItem_Quit.lclItemSelected(sender: id);
443 begin
444   {$ifdef COCOALOOPHIJACK}
445   // see bug #36265. if hot-key (Cmd+Q) is used the menu item
446   // would be called once. 1) in LCL controlled loop 2) after the loop finished
447   // The following if statement prevents "double" form close
448   if LoopHiJackEnded then Exit;
449   {$endif}
450   // Should be used instead of Application.Terminate to allow events to be sent, see bug 32148
451   Application.MainForm.Close;
452 end;
453 
454 { TCocoaWSMenu }
455 
456 {------------------------------------------------------------------------------
457   Method:  TCocoaWSMenu.CreateHandle
458   Params:  AMenu - LCL menu
459   Returns: Handle to the menu in Cocoa interface
460 
461   Creates new menu in Cocoa interface
462  ------------------------------------------------------------------------------}
463 class function TCocoaWSMenu.CreateHandle(const AMenu: TMenu): HMENU;
464 begin
465   //WriteLn(':>[TCocoaWSMenu.CreateHandle]');
466   Result := HMENU(AllocCocoaMenu);
467 end;
468 
469 { TCocoaWSMainMenu }
470 
471 class function TCocoaWSMainMenu.CreateHandle(const AMenu: TMenu): HMENU;
472 begin
473   Result := HMENU(AllocCocoaMenu);
474   TCocoaMenu(Result).createAppleMenu();
475 end;
476 
477 { TCocoaWSMenuItem }
478 
479 class procedure TCocoaWSMenuItem.Do_SetCheck(const ANSMenuItem: NSMenuItem; const Checked: boolean);
480 const
481   menustate : array [Boolean] of NSInteger = (NSOffState, NSOnState);
482 begin
483   ANSMenuItem.setState( menustate[Checked] );
484 end;
485 
486 // used from the MenuMadness example
487 class function TCocoaWSMenuItem.NSMenuCheckmark: NSImage;
488 begin
489   Result:=NSImage.imageNamed(NSStringUtf8('NSMenuCheckmark'));
490 end;
491 
492 class function TCocoaWSMenuItem.NSMenuRadio: NSImage;
493 begin
494   Result:=NSImage.imageNamed(NSStringUtf8('NSMenuRadio'))
495 end;
496 
497 class function TCocoaWSMenuItem.isSeparator(const ACaption: AnsiString): Boolean;
498 begin
499   Result:=ACaption='-';
500 end;
501 
502 {------------------------------------------------------------------------------
503   Method:  TCocoaWSMenuItem.AttachMenu
504   Params:  AMenuItem - LCL menu item
505 
506   Attaches menu item to its parent menu in Cocoa interface
507  ------------------------------------------------------------------------------}
508 class procedure TCocoaWSMenuItem.AttachMenu(const AMenuItem: TMenuItem);
509 var
510   ParObj  : NSObject;
511   Parent  : TCocoaMenu;
512   item    : NSMenuItem;
513   MenuObj : NSObject;
514   Menu    : NSMenu;
515   idx     : Integer;
516 begin
517   if not Assigned(AMenuItem) or (AMenuItem.Handle=0) or not Assigned(AMenuItem.Parent) or (AMenuItem.Parent.Handle=0) then Exit;
518   ParObj:=NSObject(AMenuItem.Parent.Handle);
519   item:=NSMenuItem(AMenuItem.Handle);
520 
521   if ParObj.isKindOfClass(NSMenuItem) then
522   begin
523     if not NSMenuItem(ParObj).hasSubmenu then
524     begin
525       Parent := AllocCocoaMenu(AMenuItem.Parent.Caption);
526       Parent.setDelegate(TCocoaMenuItem(ParObj));
527       NSMenuItem(ParObj).setSubmenu(Parent);
528 
529       // no longer respond to clicks. LCL might still need to get an event
530       // yet the menu should not close
531       NSMenuItem(ParObj).setAction(nil);
532     end
533     else
534       Parent:=TCocoaMenu(NSMenuItem(ParObj).submenu);
535   end else if ParObj.isKindOfClass(NSMenu) then
536     Parent:=TCocoaMenu(ParObj)
537   else
538     Exit;
539 
540   item := nil;
541   MenuObj := NSObject(AMenuItem.Handle);
542   if MenuObj.isKindOfClass(NSMenuItem) then
543     item := NSMenuItem(MenuObj)
544   else if MenuObj.isKindOfClass(NSMenu) then
545   begin
546     Menu := NSMenu(MenuObj);
547     item := NSMenuItem(NSMenuItem.alloc).initWithTitle_action_keyEquivalent(
548       ControlTitleToNSStr(AMenuItem.Caption), nil, NSString.string_ );
549     item.setSubmenu( Menu );
550   end;
551 
552   if Assigned(item) then
553   begin
554     idx := AMenuItem.MenuVisibleIndex;
555     if idx < 0 then idx := Parent.numberOfItems;
556     Parent.insertItem_atIndex(NSMenuItem(item), idx)
557   end;
558 end;
559 
560 {------------------------------------------------------------------------------
561   Method:  TCocoaWSMenuItem.CreateHandle
562   Params:  AMenuItem - LCL menu item
563   Returns: Handle to the menu item in Cocoa interface
564 
565   Creates new menu item in Cocoa interface
566  ------------------------------------------------------------------------------}
567 class function TCocoaWSMenuItem.CreateHandle(const AMenuItem: TMenuItem): HMENU;
568 var
569   item    : NSMenuItem;
570   ANSMenu : NSMenu;
571 begin
572   if not Assigned(AMenuItem) then
573   begin
574     Result:=0;
575     Exit;
576   end;
577 
578   // A handle of TMenu.fItems (TMenuItem) could be recreated.
579   // in this case LCL calls TCocoaWSMenuItem.CreateHandle
580   // instead of the proper owner.
581   if (AMenuItem.Owner is TMainMenu) and (TMainMenu(AMenuItem.Owner).Items = AMenuItem) then begin
582     Result:=TCocoaWSMainMenu.CreateHandle(TMenu(AMenuItem.Owner));
583     Exit;
584   end else if (AMenuItem.Owner is TMenu) and (TMenu(AMenuItem.Owner).Items = AMenuItem) then begin
585     Result:=TCocoaWSMenu.CreateHandle(TMenu(AMenuItem.Owner));
586     Exit;
587   end;
588 
589   if AMenuItem.Caption = '-' then
590   begin
591     item := NSMenuItem.separatorItem;
592   end
593   else
594   begin
595     item := LCLMenuItemInit(TCocoaMenuItem.alloc, AMenuItem.Caption, AMenuItem.ShortCut);
596     TCocoaMenuItem(item).FMenuItemTarget := AMenuItem;
597 
598     if AMenuItem.IsInMenuBar then
599     begin
600       ANSMenu := AllocCocoaMenu(AMenuItem.Caption);
601       ANSMenu.setDelegate(TCocoaMenuItem(item));
602       item.setSubmenu(ANSMenu);
603     end;
604 
605     TCocoaMenuItem(item).menuItemCallback:=TLCLMenuItemCallback.Create(item, AMenuItem);
606 
607     // initial set of properties
608     {$ifdef BOOLFIX}
609     item.setEnabled_(Ord(AMenuItem.Enabled));
610     {$else}
611     item.setEnabled(AMenuItem.Enabled);
612     {$endif}
613 
614     if AMenuItem.RadioItem then
615       item.setOnStateImage( NSMenuRadio )
616     else
617       item.setOnStateImage(NSMenuCheckmark);
618 
619     Do_SetCheck(item, AMenuItem.Checked);
620 
621     if AMenuItem.HasIcon and ((AMenuItem.ImageIndex>=0) or (AMenuItem.HasBitmap)) then
622       NSMenuItemSetBitmap(item, AMenuItem.Bitmap);
623   end;
624 
625   Result:=HMENU(item);
626 end;
627 
628 {------------------------------------------------------------------------------
629   Method:  TCocoaWSMenuItem.DestroyHandle
630   Params:  AMenuItem - LCL menu item
631 
632   Destroys menu item in Cocoa interface
633  ------------------------------------------------------------------------------}
634 class procedure TCocoaWSMenuItem.DestroyHandle(const AMenuItem: TMenuItem);
635 var
636   callback: IMenuItemCallback;
637   callbackObject: TObject;
638   item     : NSObject;
639   menuitem : TCocoaMenuItem;
640   nsitem   : NSMenuItem;
641 begin
642   item:=NSObject(AMenuItem.Handle);
643   if item.isKindOfClass_(TCocoaMenuItem) then
644   begin
645     menuitem := TCocoaMenuItem(item);
646     callback := menuitem.lclGetCallback;
647     if Assigned(callback) then
648     begin
649       callbackObject := callback.GetCallbackObject;
650       callback := nil;
651       menuitem.lclClearCallback;
652       callbackObject.Free;
653     end;
654     if Assigned(menuitem.menu) then
655       menuitem.menu.removeItem(menuitem);
656     AMenuItem.Handle := 0;
657     menuitem.release; // TCocoaMenuItems are "alloced" - thus should be released;
658   end else if item.isKindOfClass_(NSMenuItem) then begin
659     nsitem := NSMenuItem(item);
660     if nsitem.isSeparatorItem and Assigned(nsitem.menu) then
661       nsitem.menu.removeItem(nsitem);
662     // separator items are not "alloced", thus should not be released
663   end;
664 
665 end;
666 
667 {------------------------------------------------------------------------------
668   Method:  TCocoaWSMenuItem.SetCaption
669   Params:  AMenuItem - LCL menu item
670            ACaption  - Menu item caption
671 
672   Sets the caption of menu item in Cocoa interface
673  ------------------------------------------------------------------------------}
674 class procedure TCocoaWSMenuItem.SetCaption(const AMenuItem: TMenuItem; const ACaption: string);
675 var
676   ns : NSString;
677   s: string;
678 begin
679   if not Assigned(AMenuItem) or (AMenuItem.Handle=0) then Exit;
680   if NSMenuItem(AMenuItem.Handle).isSeparatorItem <> (ACaption='-') then
681     AMenuItem.RecreateHandle
682   else
683   begin
684     s := ACaption;
685     DeleteAmpersands(s);
686     ns:=NSStringUtf8(s);
687     NSMenuItem(AMenuItem.Handle).setTitle(ns);
688     if NSMenuItem(AMenuItem.Handle).hasSubmenu then
689       NSMenuItem(AMenuItem.Handle).submenu.setTitle(ns);
690     ns.release;
691   end;
692 end;
693 
694 {------------------------------------------------------------------------------
695   Method:  TCocoaWSMenuItem.SetShortCut
696   Params:  AMenuItem   - LCL menu item
697            ShortCutK1 and ShortCutK2 - New shortcut key1 and key2
698 
699   Sets the shortcut of menu item in Cocoa interface
700  ------------------------------------------------------------------------------}
701 class procedure TCocoaWSMenuItem.SetShortCut(const AMenuItem: TMenuItem;
702   const ShortCutK1, ShortCutK2: TShortCut);
703 var
704   ShiftState: NSUInteger;
705   ns: NSString;
706 begin
707   ShortcutToKeyEquivalent(ShortCutK1, ns, ShiftState);
708   TCocoaMenuItem(AMenuItem.Handle).setKeyEquivalentModifierMask(ShiftState);
709   TCocoaMenuItem(AMenuItem.Handle).setKeyEquivalent(ns);
710 end;
711 
712 {------------------------------------------------------------------------------
713   Method:  TCocoaWSMenuItem.SetVisible
714   Params:  AMenuItem - LCL menu item
715            Visible   - Menu item visibility
716 
717   Sets the visibility of menu item in Cocoa interface
718  ------------------------------------------------------------------------------}
719 class procedure TCocoaWSMenuItem.SetVisible(const AMenuItem: TMenuItem;
720   const Visible: boolean);
721 begin
722   if not Assigned(AMenuItem) or (AMenuItem.Handle=0) then Exit;
723   {$ifdef BOOLFIX}
724   NSMenuItem(AMenuItem.Handle).setHidden_( Ord(not Visible) );
725   {$else}
726   NSMenuItem(AMenuItem.Handle).setHidden( not Visible );
727   {$endif}
728 end;
729 
730 {------------------------------------------------------------------------------
731   Method:  TCocoaWSMenuItem.SetCheck
732   Params:  AMenuItem - LCL menu item
733            Checked   - Menu item checked
734   Returns: If the function succeeds
735 
736   Sets the check of menu item in Cocoa interface
737  ------------------------------------------------------------------------------}
738 class function TCocoaWSMenuItem.SetCheck(const AMenuItem: TMenuItem;
739   const Checked: boolean): boolean;
740 var
741   lHandle: NSMenuItem;
742   lCocoaHandle: TCocoaMenuItem absolute lHandle;
743 begin
744   Result := Assigned(AMenuItem) and AMenuItem.HandleAllocated() and (AMenuItem.Handle<>0);
745   if not Result then Exit;
746   lHandle := NSMenuItem(AMenuItem.Handle);
747   Result := Result and lHandle.isKindOfClass_(TCocoaMenuItem);
748   if not Result then Exit;
749   TCocoaWSMenuItem.Do_SetCheck(lHandle, Checked);
750   lCocoaHandle.UncheckSiblings(True);
751 end;
752 
753 {------------------------------------------------------------------------------
754   Method:  TCocoaWSMenuItem.SetEnable
755   Params:  AMenuItem - LCL menu item
756            Enabled   - Menu item enabled
757   Returns: If the function succeeds
758 
759   Sets the enabled of menu item in Cocoa interface
760  ------------------------------------------------------------------------------}
761 class function TCocoaWSMenuItem.SetEnable(const AMenuItem: TMenuItem;
762   const Enabled: boolean): boolean;
763 begin
764   Result:=Assigned(AMenuItem) and (AMenuItem.Handle<>0);
765   if not Result then Exit;
766   {$ifdef BOOLFIX}
767   NSMenuItem(AMenuItem.Handle).setEnabled_( Ord(Enabled) );
768   {$else}
769   NSMenuItem(AMenuItem.Handle).setEnabled( Enabled );
770   {$endif}
771 end;
772 
773 {------------------------------------------------------------------------------
774   Method:  TCocoaWSMenuItem.SetRadioItem
775   Params:  AMenuItem - LCL menu item
776            RadioItem - Menu item has radio
777   Returns: If the function succeeds
778 
779   Sets the radio behaviour of menu item in Cocoa interface
780  ------------------------------------------------------------------------------}
781 class function TCocoaWSMenuItem.SetRadioItem(const AMenuItem: TMenuItem;
782   const RadioItem: boolean): boolean;
783 const
784   menustate : array [Boolean] of NSInteger = (NSOffState, NSOnState);
785 begin
786   Result:=Assigned(AMenuItem) and (AMenuItem.Handle<>0);
787   if not Result then Exit;
788   //todo: disable relative radio items
789   if RadioItem then
790     NSMenuItem(AMenuItem.Handle).setOnStateImage( NSMenuRadio )
791   else
792     NSMenuItem(AMenuItem.Handle).setOnStateImage(NSMenuCheckmark);
793 
794   NSMenuItem(AMenuItem.Handle).setState( menustate[RadioItem] );
795 end;
796 
797 procedure NSMenuItemSetBitmap(mn: NSMenuItem; bmp: TBitmap);
798 begin
799   if not Assigned(mn) then Exit;
800   if not Assigned(bmp) or (bmp.Handle = 0) then
801     mn.setImage(nil)
802   else
803     mn.setImage(TCocoaBitmap(bmp.Handle).Image);
804 end;
805 
806 class procedure TCocoaWSMenuItem.UpdateMenuIcon(const AMenuItem: TMenuItem;
807   const HasIcon: Boolean; const AIcon: TBitmap);
808 var
809   mn : NSMenuItem;
810 begin
811   if not Assigned(AMenuItem) or (AMenuItem.Handle=0) then Exit;
812 
813   if NSObject(AMenuItem.Handle).isKindOfClass(NSMenuItem) then
814     NSMenuItemSetBitmap( NSMenuItem(AMenuItem.Handle), AIcon);
815 end;
816 
817 { TCocoaWSPopupMenu }
818 
819 {------------------------------------------------------------------------------
820   Method:  TCocoaWSPopupMenu.Popup
821   Params:  APopupMenu - LCL popup menu
822            X, Y       - Screen coordinates to popup
823 
824   Popups menu in Cocoa interface
825  ------------------------------------------------------------------------------}
826 class procedure TCocoaWSPopupMenu.Popup(const APopupMenu: TPopupMenu; const X,
827   Y: Integer);
828 var
829   res : Boolean;
830   mnu : NSMenuItem;
831   view : NSView;
832   w : NSWindow;
833   px, py: Integer;
834 begin
835   if Assigned(APopupMenu) and (APopupMenu.Handle<>0) then
836   begin
837     // old method which doesn't consider position but supports 10.0+ (useless since we target 10.6+)
838     {w:=NSApp.keyWindow;
839     if Assigned(w) then
840     begin
841       NSMenu.popUpContextMenu_withEvent_forView( TCocoaMenu(APopupMenu.Handle),
842         NSApp.currentEvent, NSView(w.contentView));
843     end;}
844 
845     // New method for 10.6+
846     px := x;
847     py := y;
848     view := nil;
849     w :=NSApp.keyWindow;
850     if Assigned(w) then
851     begin
852       view := w.contentView;
853       if Assigned(view) then
854       begin
855         view.lclScreenToLocal(px, py);
856         // have to flip again, because popUpMenuPositioningItem expects point
857         // to be in View coordinates and it does respect Flipped flag
858         if not view.isFlipped then
859           py := Round(view.frame.size.height - py);
860       end;
861     end;
862     res := TCocoaMenu(APopupMenu.Handle).popUpMenuPositioningItem_atLocation_inView(
863       nil, NSMakePoint(px, py), view);
864     APopupMenu.Close; // notify LCL popup menu
865   end;
866 end;
867 
868 procedure ShortcutToKeyEquivalent(const AShortCut: TShortcut; out Key: NSString; out shiftKeyMask: NSUInteger);
869 var
870   w: word;
871   s: TShiftState;
872 begin
873   ShortCutToKey(AShortCut, w, s);
874   key := VirtualKeyCodeToMacString(w);
875   shiftKeyMask := 0;
876   if ssShift in s then
877     ShiftKeyMask := ShiftKeyMask + NSShiftKeyMask;
878   if ssAlt in s then
879     ShiftKeyMask := ShiftKeyMask + NSAlternateKeyMask;
880   if ssCtrl in s then
881     ShiftKeyMask := ShiftKeyMask + NSControlKeyMask;
882   if ssMeta in s then
883     ShiftKeyMask := ShiftKeyMask + NSCommandKeyMask;
884 end;
885 
886 procedure ToggleAppNSMenu(mn: NSMenu; ALogicalEnabled: Boolean);
887 var
888   it  : NSMenuItem;
889   obj : NSObject;
890   enb : Boolean;
891 begin
892   if not Assigned(mn) then Exit;
893   for obj in mn.itemArray do begin
894     if not obj.isKindOfClass(NSMenuItem) then continue;
895     it := NSMenuItem(obj);
896     enb := ALogicalEnabled;
897     if enb and (it.isKindOfClass(TCocoaMenuItem)) then
898     begin
899       enb := not Assigned(TCocoaMenuItem(it).FMenuItemTarget)
900          or ( TCocoaMenuItem(it).FMenuItemTarget.Enabled );
901     end;
902     {$ifdef BOOLFIX}
903     it.setEnabled_( Ord(enb));
904     {$else}
905     it.setEnabled(enb);
906     {$endif}
907     if (it.hasSubmenu) then
908     begin
909       ToggleAppNSMenu(it.submenu, ALogicalEnabled);
910     end;
911   end;
912 end;
913 
914 procedure ToggleAppMenu(ALogicalEnabled: Boolean);
915 begin
916   ToggleAppNSMenu( NSApplication(NSApp).mainMenu, ALogicalEnabled );
917 end;
918 
919 end.
920