From 365360bd9082ff82f84507889784012d5e3c9eca Mon Sep 17 00:00:00 2001 From: rich2014 Date: Sun, 19 Nov 2023 15:25:06 +0800 Subject: [PATCH] Cocoa/CrossRef: Remove unit cross-references between CocoaInt and CocoaWSMenus --- lcl/interfaces/cocoa/cocoaint.pas | 2 +- lcl/interfaces/cocoa/cocoamenus.pas | 527 +++++++++++++++++++++++ lcl/interfaces/cocoa/cocoawsdialogs.pas | 2 +- lcl/interfaces/cocoa/cocoawsextctrls.pas | 2 +- lcl/interfaces/cocoa/cocoawsforms.pas | 2 +- lcl/interfaces/cocoa/cocoawsmenus.pas | 509 +--------------------- 6 files changed, 534 insertions(+), 510 deletions(-) create mode 100644 lcl/interfaces/cocoa/cocoamenus.pas diff --git a/lcl/interfaces/cocoa/cocoaint.pas b/lcl/interfaces/cocoa/cocoaint.pas index 16040f514b..213520ee91 100644 --- a/lcl/interfaces/cocoa/cocoaint.pas +++ b/lcl/interfaces/cocoa/cocoaint.pas @@ -34,7 +34,7 @@ uses LCLPlatformDef, InterfaceBase, GraphType, // private CocoaAll, CocoaConfig, CocoaPrivate, CocoaUtils, CocoaGDIObjects, CocoaCursor, - cocoa_extra, CocoaWSMenus, CocoaWindows, CocoaScrollers, + cocoa_extra, CocoaMenus, CocoaWindows, CocoaScrollers, CocoaWSClipboard, CocoaTextEdits, CocoaWSCommon, // LCL LMessages, LCLProc, LCLIntf, LCLType, diff --git a/lcl/interfaces/cocoa/cocoamenus.pas b/lcl/interfaces/cocoa/cocoamenus.pas new file mode 100644 index 0000000000..34e5acc554 --- /dev/null +++ b/lcl/interfaces/cocoa/cocoamenus.pas @@ -0,0 +1,527 @@ +unit CocoaMenus; + +{$mode ObjFPC}{$H+} +{$modeswitch objectivec2} +{$include cocoadefines.inc} + +interface + +uses + // RTL + sysutils, + // LCL + Forms, Menus, LCLType, Classes, LCLStrConsts, + // LCL Cocoa + CocoaAll, CocoaPrivate, CocoaUtils; + +type + IMenuItemCallback = interface(ICommonCallBack) + procedure ItemSelected; + function MenuItemTarget: TMenuItem; + end; + + TCocoaMenuItem = objcclass; + + { TCocoaMenu } + + TCocoaMenu = objcclass(NSMenu) + private + appleMenu: TCocoaMenuItem; + attachedAppleMenu: Boolean; + isKeyEq: Boolean; + public + procedure dealloc; override; + procedure lclItemSelected(sender: id); message 'lclItemSelected:'; + procedure createAppleMenu(); message 'createAppleMenu'; + procedure overrideAppleMenu(AItem: TCocoaMenuItem); message 'overrideAppleMenu:'; + procedure attachAppleMenu(); message 'attachAppleMenu'; + function performKeyEquivalent(theEvent: NSEvent): LCLObjCBoolean; override; + function lclIsKeyEquivalent: LCLObjCBoolean; message 'lclIsKeyEquivalent'; + end; + + { TCocoaMenuItem } + + TCocoaMenuItem = objcclass(NSMenuItem, NSMenuDelegateProtocol) + public + menuItemCallback: IMenuItemCallback; + attachedAppleMenuItems: Boolean; + FMenuItemTarget: TMenuItem; + procedure UncheckSiblings(AIsChangingToChecked: LCLObjCBoolean = False); message 'UncheckSiblings:'; + function GetMenuItemHandle(): TMenuItem; message 'GetMenuItemHandle'; + procedure lclItemSelected(sender: id); message 'lclItemSelected:'; + function lclGetCallback: IMenuItemCallback; override; + procedure lclClearCallback; override; + procedure attachAppleMenuItems(); message 'attachAppleMenuItems'; + function isValidAppleMenu(): LCLObjCBoolean; message 'isValidAppleMenu'; + // menuWillOpen cannot be used. Because it SHOULD NOT change the contents + // of the menu. While LCL allows to modify the menu contents when the submenu + // is about to be activated. + procedure menuNeedsUpdate(AMenu: NSMenu); message 'menuNeedsUpdate:'; + procedure menuDidClose(AMenu: NSMenu); message 'menuDidClose:'; + //procedure menuDidClose(AMenu: NSMenu); message 'menuDidClose:'; + function worksWhenModal: LCLObjCBoolean; message 'worksWhenModal'; + end; + +const + isMenuEnabled : Boolean = true; + +procedure MenuTrackStarted(mn: NSMenu); +procedure MenuTrackEnded(mn: NSMenu); +procedure MenuTrackCancelAll; + +// the returned "Key" should not be released, as it's not memory owned +procedure ShortcutToKeyEquivalent(const AShortCut: TShortcut; out Key: NSString; out shiftKeyMask: NSUInteger); + +function LCLMenuItemInit(item: NSMenuItem; const atitle: string; ashortCut: TShortCut): id; +function LCLMenuItemInit(item: NSMenuItem; const atitle: string; VKKey: Word = 0; State: TShiftState = []): id; +function ToggleAppMenu(ALogicalEnabled: Boolean): Boolean; +procedure Do_SetCheck(const ANSMenuItem: NSMenuItem; const Checked: boolean); + +implementation + +type + TCocoaMenuItem_HideApp = objcclass(NSMenuItem) + public + procedure lclItemSelected(sender: id); message 'lclItemSelected:'; + end; + + TCocoaMenuItem_HideOthers = objcclass(NSMenuItem) + public + procedure lclItemSelected(sender: id); message 'lclItemSelected:'; + end; + + TCocoaMenuItem_ShowAllApp = objcclass(NSMenuItem) + public + procedure lclItemSelected(sender: id); message 'lclItemSelected:'; + end; + + TCocoaMenuItem_Quit = objcclass(NSMenuItem) + public + procedure lclItemSelected(sender: id); message 'lclItemSelected:'; + end; + +var + // menuTrack is needed due to the LCL architecture vs Cocoa menu handling. + // + // (below "Modal" refers to LCL "modal" state. Not verified with Cocoa modal) + // + // All Menu hangling in Cocoa is done within a "tracking" loop. (similar to Modal even loop) + // The major issue, is that WS code never knows that a menu was clicked, + // outside of the tracking loop. (The delegate is being notified already within the loop) + // + // If LCL handler is calling for any modal window at the time, + // the menu tracking should stop, and the model window is the only be processed + // + // In order to track "opened" menus, menuTrack list is used. + // If modal window is called MenuTrackCancelAll() will terminate all openned sub-menus + // + // The issue of the conflict between "Menu tracking" and "Modal tracking" + // is only affecting Node-Menus (menu items with submenus) + // because we call LM_ACTIVATE within tracking loop. + // The Leaf-menuitems (menu items w/o submenuis) are calling "itemSElected" (LM_ACTIVATE) + // when the tracking loop is over + // + // See topic: https://forum.lazarus.freepascal.org/index.php/topic,56419.0.html + menuTrack : NSMutableArray; + +procedure ShortcutToKeyEquivalent(const AShortCut: TShortcut; out Key: NSString; out shiftKeyMask: NSUInteger); +var + w: word; + s: TShiftState; +begin + ShortCutToKey(AShortCut, w, s); + key := VirtualKeyCodeToMacString(w); + shiftKeyMask := 0; + if ssShift in s then + ShiftKeyMask := ShiftKeyMask + NSShiftKeyMask; + if ssAlt in s then + ShiftKeyMask := ShiftKeyMask + NSAlternateKeyMask; + if ssCtrl in s then + ShiftKeyMask := ShiftKeyMask + NSControlKeyMask; + if ssMeta in s then + ShiftKeyMask := ShiftKeyMask + NSCommandKeyMask; +end; + +procedure ToggleAppNSMenu(mn: NSMenu; ALogicalEnabled: Boolean); +var + it : NSMenuItem; + obj : NSObject; + enb : Boolean; +begin + if not Assigned(mn) then Exit; + for obj in mn.itemArray do begin + if not obj.isKindOfClass(NSMenuItem) then continue; + it := NSMenuItem(obj); + enb := ALogicalEnabled; + if enb and (it.isKindOfClass(TCocoaMenuItem)) then + begin + enb := not Assigned(TCocoaMenuItem(it).FMenuItemTarget) + or ( TCocoaMenuItem(it).FMenuItemTarget.Enabled ); + end; + {$ifdef BOOLFIX} + it.setEnabled_( Ord(enb)); + {$else} + it.setEnabled(enb); + {$endif} + if (it.hasSubmenu) then + begin + ToggleAppNSMenu(it.submenu, ALogicalEnabled); + end; + end; +end; + +function ToggleAppMenu(ALogicalEnabled: Boolean): Boolean; +begin + Result := isMenuEnabled; + ToggleAppNSMenu( NSApplication(NSApp).mainMenu, ALogicalEnabled ); + isMenuEnabled := ALogicalEnabled; +end; + +procedure Do_SetCheck(const ANSMenuItem: NSMenuItem; const Checked: boolean); +const + menustate : array [Boolean] of NSInteger = (NSOffState, NSOnState); +begin + ANSMenuItem.setState( menustate[Checked] ); +end; + +function LCLMenuItemInit(item: NSMenuItem; const atitle: string; ashortCut: TShortCut): id; +var + key : NSString; + mask : NSUInteger; +begin + ShortcutToKeyEquivalent(ashortCut, key, mask); + Result := item.initWithTitle_action_keyEquivalent( + ControlTitleToNSStr(Atitle), + objcselector('lclItemSelected:'), // Selector is Hard-coded, that's why it's LCLMenuItemInit + key); + NSMenuItem(Result).setKeyEquivalentModifierMask(mask); + NSMenuItem(Result).setTarget(Result); +end; + +function LCLMenuItemInit(item: NSMenuItem; const atitle: string; VKKey: Word; State: TShiftState): id; +var + key : NSString; + mask : NSUInteger; +begin + Result := LCLMenuItemInit(item, atitle, ShortCut(VKKey, State)); +end; + +{ TCocoaMenu } + +procedure TCocoaMenu.dealloc; +begin + if appleMenu <> nil then begin + if indexOfItem(appleMenu) >= 0 then + removeItem(appleMenu); + appleMenu.release; + appleMenu := nil; + end; + inherited dealloc; +end; + +procedure TCocoaMenu.lclItemSelected(sender:id); +begin + +end; + +// For when there is no menu item with title  +procedure TCocoaMenu.createAppleMenu(); +var + nskey, nstitle, nssubmeykey: NSString; + lNSSubmenu: NSMenu; +begin + // create the menu item + nstitle := NSStringUtf8(''); + appleMenu := TCocoaMenuItem.alloc.initWithTitle_action_keyEquivalent(nstitle, + objcselector('lclItemSelected:'), NSString.string_); + nstitle.release; + + // add the submenu + lNSSubmenu := NSMenu.alloc.initWithTitle(NSString.string_); + appleMenu.setSubmenu(lNSSubmenu); + lNSSubmenu.release; + + appleMenu.attachAppleMenuItems(); +end; + +// For when there is a menu item with title  +procedure TCocoaMenu.overrideAppleMenu(AItem: TCocoaMenuItem); +begin + if appleMenu <> nil then + begin + if indexOfItem(appleMenu) >= 0 then + removeItem(appleMenu); + appleMenu.release; + appleMenu := nil; + end; + attachedAppleMenu := False; + AItem.attachAppleMenuItems(); +end; + +procedure TCocoaMenu.attachAppleMenu(); +begin + if attachedAppleMenu then Exit; + if appleMenu = nil then Exit; + attachedAppleMenu := True; + insertItem_atIndex(appleMenu, 0); +end; + +function TCocoaMenu.performKeyEquivalent(theEvent: NSEvent): LCLObjCBoolean; +var + OldKeyEq: boolean; +begin + OldKeyEq:=isKeyEq; + isKeyEq := true; + try + Result := inherited performKeyEquivalent(theEvent); + finally + isKeyEq := OldKeyEq; + end; +end; + +function TCocoaMenu.lclIsKeyEquivalent: LCLObjCBoolean; +begin + Result := isKeyEq; +end; + +{ TCocoaMenuITem } + +procedure TCocoaMenuItem.UncheckSiblings(AIsChangingToChecked: LCLObjCBoolean); +var + i: Integer; + lMenuItem, lSibling, lParentMenu: TMenuItem; + lSiblingHandle: NSMenuItem; +begin + //lMenuItem := GetMenuItemHandle(); + lMenuItem := FMenuItemTarget; + if lMenuItem = nil then Exit; + if not lMenuItem.RadioItem then Exit; + if (not AIsChangingToChecked) and (not lMenuItem.Checked) then Exit; + lParentMenu := lMenuItem.Parent; + if lParentMenu = nil then Exit; + for i := 0 to lParentMenu.Count - 1 do + begin + lSibling := lParentMenu.Items[i]; + if lSibling = nil then Continue; + if lSibling = lMenuItem then Continue; + + if lSibling.RadioItem and (lSibling.GroupIndex = lMenuItem.GroupIndex) and + lSibling.HandleAllocated() then + begin + lSiblingHandle := NSMenuItem(lSibling.Handle); + Do_SetCheck(lSiblingHandle, False); + end; + end; +end; + +function TCocoaMenuItem.GetMenuItemHandle(): TMenuItem; +begin + Result := nil; + if menuItemCallback = nil then Exit; + Result := menuItemCallback.MenuItemTarget; +end; + +procedure TCocoaMenuItem.lclItemSelected(sender:id); +begin + menuItemCallback.ItemSelected; + UncheckSiblings(); +end; + +function TCocoaMenuItem.lclGetCallback: IMenuItemCallback; +begin + result:=menuItemCallback; +end; + +procedure TCocoaMenuItem.lclClearCallback; +begin + menuItemCallback := nil; +end; + +procedure TCocoaMenuItem.attachAppleMenuItems(); +var + item : NSMenuItem; + itemSubMenu: NSMenu; +begin + if attachedAppleMenuItems then Exit; + if not hasSubmenu() then Exit; + + // Separator + submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count); + + // Services + item := LCLMenuItemInit( TCocoaMenuItem.alloc, rsMacOSMenuServices); + item.setTarget(nil); + item.setAction(nil); + submenu.insertItem_atIndex(item, submenu.itemArray.count); + itemSubMenu := NSMenu.alloc.initWithTitle( ControlTitleToNSStr(rsMacOSMenuServices)); + item.setSubmenu(itemSubMenu); + itemSubMenu.release; + NSApplication(NSApp).setServicesMenu(item.submenu); + item.release; + + // Separator + submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count); + + // Hide App Meta-H + item := LCLMenuItemInit( TCocoaMenuItem_HideApp.alloc, Format(rsMacOSMenuHide, [Application.Title]), VK_H, [ssMeta]); + submenu.insertItem_atIndex(item, submenu.itemArray.count); + item.release; + + // Hide Others Meta-Alt-H + item := LCLMenuItemInit( TCocoaMenuItem_HideOthers.alloc, rsMacOSMenuHideOthers, VK_H, [ssMeta, ssAlt]); + submenu.insertItem_atIndex(item, submenu.itemArray.count); + item.release; + + // Show All + item := LCLMenuItemInit( TCocoaMenuItem_ShowAllApp.alloc, rsMacOSMenuShowAll); + submenu.insertItem_atIndex(item, submenu.itemArray.count); + item.release; + + // Separator + submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count); + + // Quit Meta-Q + item := LCLMenuItemInit( TCocoaMenuItem_Quit.alloc, Format(rsMacOSMenuQuit, [Application.Title]), VK_Q, [ssMeta]); + submenu.insertItem_atIndex(item, submenu.itemArray.count); + item.release; + + attachedAppleMenuItems := True; +end; + +function TCocoaMenuItem.isValidAppleMenu(): LCLObjCBoolean; +begin + Result := hasSubmenu() and (submenu() <> nil); + Result := Result and ('' = NSStringToString(title)); +end; + +procedure TCocoaMenuItem.menuNeedsUpdate(AMenu: NSMenu); +begin + if not Assigned(menuItemCallback) then Exit; + if not isMenuEnabled then Exit; + + MenuTrackStarted(AMenu); + + if (menu.isKindOfClass(TCocoaMenu)) then + begin + // Issue #37789 + // Cocoa tries to find, if there's a menu with the key event + // so item is not actually selected yet. Thus should not send ItemSelected + if TCocoaMenu(menu).lclIsKeyEquivalent then + Exit; + end; + + //todo: call "measureItem" + menuItemCallback.ItemSelected; +end; + +procedure TCocoaMenuItem.menuDidClose(AMenu: NSMenu); +begin + MenuTrackEnded(AMenu); +end; + +function TCocoaMenuItem.worksWhenModal: LCLObjCBoolean; +begin + // refer to NSMenuItem.target (Apple) documentation + // the method must be implemented in target and return TRUE + // otherwise it won't work for modal! + // + // The method COULD be used to protect the main menu from being clicked + // if a modal window doesn't have a menu. + // But LCL disables (is it?) the app menu manually on modal + Result := true; +end; + +{ menuDidClose should not change the structure of the menu. + The restructuring is causing issues on Apple's special menus (i.e. HELP menu) + See bug #35625 + +procedure TCocoaMenuItem.menuDidClose(AMenu: NSMenu); +var + par : NSMenu; + idx : NSInteger; + mn : NSMenuItem; +begin + // the only purpose of this code is to "invalidate" the submenu of the item. + // an invalidated menu will call menuNeedsUpdate. + // There's no other way in Cocoa to do the "invalidate" + par := amenu.supermenu; + if Assigned(par) then + begin + idx := par.indexOfItemWithSubmenu(AMenu); + if idx<>NSNotFound then + begin + mn := par.itemAtIndex(idx); + mn.setSubmenu(nil); + mn.setSubmenu(AMenu); + end; + end; +end; +} + +procedure TCocoaMenuItem_HideApp.lclItemSelected(sender: id); +begin + // Applicaiton.Minimize, calls WidgetSet.AppMinimize; + // which calls NSApplication.hide() anyway + Application.Minimize; +end; + +procedure TCocoaMenuItem_HideOthers.lclItemSelected(sender: id); +begin + NSApplication(NSApp).hideOtherApplications(sender); +end; + +procedure TCocoaMenuItem_ShowAllApp.lclItemSelected(sender: id); +begin + NSApplication(NSApp).unhideAllApplications(sender); +end; + +procedure TCocoaMenuItem_Quit.lclItemSelected(sender: id); +begin + {$ifdef COCOALOOPHIJACK} + // see bug #36265. if hot-key (Cmd+Q) is used the menu item + // would be called once. 1) in LCL controlled loop 2) after the loop finished + // The following if statement prevents "double" form close + if LoopHiJackEnded then Exit; + {$endif} + // Should be used instead of Application.Terminate when possible + // to allow events to be sent, see bug 32148 + if Assigned(Application.MainForm) then + Application.MainForm.Close + else + Application.Terminate; +end; + +procedure MenuTrackStarted(mn: NSMenu); +begin + if not Assigned(menuTrack) then menuTrack := NSMutableArray.alloc.init; + menuTrack.addObject(mn); +end; + +procedure MenuTrackEnded(mn: NSMenu); +begin + if Assigned(menuTrack) then + menuTrack.removeObject(mn); +end; + +procedure MenuTrackCancelAll; +var + mn : NSMenu; +begin + if not Assigned(menuTrack) then Exit; + if menuTrack.count = 0 then Exit; + for mn in menuTrack do + begin + if Assigned(mn) then + mn.cancelTracking; + end; + menuTrack.removeAllObjects; +end; + +initialization + +finalization + MenuTrackCancelAll; + if menuTrack <> nil then menuTrack.release; + +end. + diff --git a/lcl/interfaces/cocoa/cocoawsdialogs.pas b/lcl/interfaces/cocoa/cocoawsdialogs.pas index 63c59bcdb3..f32cdafe54 100644 --- a/lcl/interfaces/cocoa/cocoawsdialogs.pas +++ b/lcl/interfaces/cocoa/cocoawsdialogs.pas @@ -34,7 +34,7 @@ uses WSForms, WSLCLClasses, WSDialogs, // LCL Cocoa CocoaUtils, CocoaGDIObjects - ,Cocoa_Extra, CocoaWSMenus; + ,Cocoa_Extra, CocoaMenus; type diff --git a/lcl/interfaces/cocoa/cocoawsextctrls.pas b/lcl/interfaces/cocoa/cocoawsextctrls.pas index a5e98d4a3c..782aaef889 100644 --- a/lcl/interfaces/cocoa/cocoawsextctrls.pas +++ b/lcl/interfaces/cocoa/cocoawsextctrls.pas @@ -30,7 +30,7 @@ uses // widgetset WSExtCtrls, WSLCLClasses, WSControls, WSProc, // LCL Cocoa - CocoaPrivate, CocoaWSMenus, CocoaWSCommon, CocoaGDIObjects, CocoaScrollers + CocoaPrivate, CocoaMenus, CocoaWSCommon, CocoaGDIObjects, CocoaScrollers ,Cocoa_Extra; type diff --git a/lcl/interfaces/cocoa/cocoawsforms.pas b/lcl/interfaces/cocoa/cocoawsforms.pas index 0503ac64ba..4b2cbc4907 100644 --- a/lcl/interfaces/cocoa/cocoawsforms.pas +++ b/lcl/interfaces/cocoa/cocoawsforms.pas @@ -30,7 +30,7 @@ uses // Widgetset WSForms, WSLCLClasses, LCLMessageGlue, // LCL Cocoa - CocoaInt, CocoaConfig, CocoaPrivate, CocoaUtils, CocoaWSCommon, CocoaWSMenus, + CocoaInt, CocoaConfig, CocoaPrivate, CocoaUtils, CocoaWSCommon, CocoaMenus, CocoaGDIObjects, CocoaWindows, CocoaScrollers, cocoa_extra; diff --git a/lcl/interfaces/cocoa/cocoawsmenus.pas b/lcl/interfaces/cocoa/cocoawsmenus.pas index 830716b2e2..cc6c995b18 100644 --- a/lcl/interfaces/cocoa/cocoawsmenus.pas +++ b/lcl/interfaces/cocoa/cocoawsmenus.pas @@ -27,24 +27,17 @@ uses CocoaAll, MacOSAll, // RTL - sysutils, math, // LCL - Controls, Forms, Menus, Graphics, LCLType, LMessages, LCLProc, Classes, + Forms, Menus, Graphics, LCLType, LMessages, LCLProc, Classes, LCLMessageGlue, LCLStrConsts, // Widgetset - WSMenus, WSLCLClasses, + WSMenus, // LCL Cocoa - Cocoa_extra, - CocoaPrivate, CocoaWSCommon, CocoaUtils, CocoaGDIObjects; + CocoaInt, CocoaMenus, CocoaWSCommon, CocoaUtils, CocoaGDIObjects; type - IMenuItemCallback = interface(ICommonCallBack) - procedure ItemSelected; - function MenuItemTarget: TMenuItem; - end; - { TLCLMenuItemCallback } TLCLMenuItemCallback = class(TLCLCommonCallback, IMenuItemCallback) @@ -56,68 +49,6 @@ type function MenuItemTarget: TMenuItem; end; - TCocoaMenuItem = objcclass; - - { TCocoaMenu } - - TCocoaMenu = objcclass(NSMenu) - private - appleMenu: TCocoaMenuItem; - attachedAppleMenu: Boolean; - isKeyEq: Boolean; - public - procedure dealloc; override; - procedure lclItemSelected(sender: id); message 'lclItemSelected:'; - procedure createAppleMenu(); message 'createAppleMenu'; - procedure overrideAppleMenu(AItem: TCocoaMenuItem); message 'overrideAppleMenu:'; - procedure attachAppleMenu(); message 'attachAppleMenu'; - function performKeyEquivalent(theEvent: NSEvent): LCLObjCBoolean; override; - function lclIsKeyEquivalent: LCLObjCBoolean; message 'lclIsKeyEquivalent'; - end; - - { TCocoaMenuItem } - - TCocoaMenuItem = objcclass(NSMenuItem, NSMenuDelegateProtocol) - public - menuItemCallback: IMenuItemCallback; - attachedAppleMenuItems: Boolean; - FMenuItemTarget: TMenuItem; - procedure UncheckSiblings(AIsChangingToChecked: LCLObjCBoolean = False); message 'UncheckSiblings:'; - function GetMenuItemHandle(): TMenuItem; message 'GetMenuItemHandle'; - procedure lclItemSelected(sender: id); message 'lclItemSelected:'; - function lclGetCallback: IMenuItemCallback; override; - procedure lclClearCallback; override; - procedure attachAppleMenuItems(); message 'attachAppleMenuItems'; - function isValidAppleMenu(): LCLObjCBoolean; message 'isValidAppleMenu'; - // menuWillOpen cannot be used. Because it SHOULD NOT change the contents - // of the menu. While LCL allows to modify the menu contents when the submenu - // is about to be activated. - procedure menuNeedsUpdate(AMenu: NSMenu); message 'menuNeedsUpdate:'; - procedure menuDidClose(AMenu: NSMenu); message 'menuDidClose:'; - //procedure menuDidClose(AMenu: NSMenu); message 'menuDidClose:'; - function worksWhenModal: LCLObjCBoolean; message 'worksWhenModal'; - end; - - TCocoaMenuItem_HideApp = objcclass(NSMenuItem) - public - procedure lclItemSelected(sender: id); message 'lclItemSelected:'; - end; - - TCocoaMenuItem_HideOthers = objcclass(NSMenuItem) - public - procedure lclItemSelected(sender: id); message 'lclItemSelected:'; - end; - - TCocoaMenuItem_ShowAllApp = objcclass(NSMenuItem) - public - procedure lclItemSelected(sender: id); message 'lclItemSelected:'; - end; - - TCocoaMenuItem_Quit = objcclass(NSMenuItem) - public - procedure lclItemSelected(sender: id); message 'lclItemSelected:'; - end; - { TCocoaWSMenuItem } TCocoaWSMenuItem = class(TWSMenuItem) @@ -164,111 +95,16 @@ type procedure NSMenuItemSetBitmap(mn: NSMenuItem; bmp: TBitmap); -// the returned "Key" should not be released, as it's not memory owned -procedure ShortcutToKeyEquivalent(const AShortCut: TShortcut; out Key: NSString; out shiftKeyMask: NSUInteger); - -// returns the last state the menu was at -function ToggleAppMenu(ALogicalEnabled: Boolean): Boolean; - function AllocCocoaMenu(const atitle: string = ''): TCocoaMenu; -function LCLMenuItemInit(item: NSMenuItem; const atitle: string; ashortCut: TShortCut): id; -function LCLMenuItemInit(item: NSMenuItem; const atitle: string; VKKey: Word = 0; State: TShiftState = []): id; - -procedure MenuTrackStarted(mn: NSMenu); -procedure MenuTrackEnded(mn: NSMenu); -procedure MenuTrackCancelAll; implementation -uses - CocoaInt; - -var - isMenuEnabled : Boolean = true; - // menuTrack is needed due to the LCL architecture vs Cocoa menu handling. - // - // (below "Modal" refers to LCL "modal" state. Not verified with Cocoa modal) - // - // All Menu hangling in Cocoa is done within a "tracking" loop. (similar to Modal even loop) - // The major issue, is that WS code never knows that a menu was clicked, - // outside of the tracking loop. (The delegate is being notified already within the loop) - // - // If LCL handler is calling for any modal window at the time, - // the menu tracking should stop, and the model window is the only be processed - // - // In order to track "opened" menus, menuTrack list is used. - // If modal window is called MenuTrackCancelAll() will terminate all openned sub-menus - // - // The issue of the conflict between "Menu tracking" and "Modal tracking" - // is only affecting Node-Menus (menu items with submenus) - // because we call LM_ACTIVATE within tracking loop. - // The Leaf-menuitems (menu items w/o submenuis) are calling "itemSElected" (LM_ACTIVATE) - // when the tracking loop is over - // - // See topic: https://forum.lazarus.freepascal.org/index.php/topic,56419.0.html - menuTrack : NSMutableArray; - -procedure MenuTrackStarted(mn: NSMenu); -begin - if not Assigned(menuTrack) then menuTrack := NSMutableArray.alloc.init; - menuTrack.addObject(mn); -end; - -procedure MenuTrackEnded(mn: NSMenu); -begin - if Assigned(menuTrack) then - menuTrack.removeObject(mn); -end; - -procedure MenuTrackCancelAll; -var - mn : NSMenu; -begin - if not Assigned(menuTrack) then Exit; - if menuTrack.count = 0 then Exit; - for mn in menuTrack do - begin - if Assigned(mn) then - mn.cancelTracking; - end; - menuTrack.removeAllObjects; -end; - -function LCLMenuItemInit(item: NSMenuItem; const atitle: string; ashortCut: TShortCut): id; -var - key : NSString; - mask : NSUInteger; -begin - ShortcutToKeyEquivalent(ashortCut, key, mask); - Result := item.initWithTitle_action_keyEquivalent( - ControlTitleToNSStr(Atitle), - objcselector('lclItemSelected:'), // Selector is Hard-coded, that's why it's LCLMenuItemInit - key); - NSMenuItem(Result).setKeyEquivalentModifierMask(mask); - NSMenuItem(Result).setTarget(Result); -end; - -function LCLMenuItemInit(item: NSMenuItem; const atitle: string; VKKey: Word; State: TShiftState): id; -var - key : NSString; - mask : NSUInteger; -begin - Result := LCLMenuItemInit(item, atitle, ShortCut(VKKey, State)); -end; - function AllocCocoaMenu(const atitle: string = ''): TCocoaMenu; begin Result := TCocoaMenu.alloc.initWithTitle(ControlTitleToNSStr(atitle)); Result.setAutoenablesItems(false); end; -{ TCocoaMenuItem_ShowAllApp } - -procedure TCocoaMenuItem_ShowAllApp.lclItemSelected(sender: id); -begin - NSApplication(NSApp).unhideAllApplications(sender); -end; - { TLCLMenuItemCallback } constructor TLCLMenuItemCallback.Create(AOwner: NSObject; AMenuItemTarget: TMenuItem); @@ -292,286 +128,6 @@ begin Result:=FMenuItemTarget; end; -{ TCocoaMenu } - -procedure TCocoaMenu.dealloc; -begin - if appleMenu <> nil then begin - if indexOfItem(appleMenu) >= 0 then - removeItem(appleMenu); - appleMenu.release; - appleMenu := nil; - end; - inherited dealloc; -end; - -procedure TCocoaMenu.lclItemSelected(sender:id); -begin - -end; - -// For when there is no menu item with title  -procedure TCocoaMenu.createAppleMenu(); -var - nskey, nstitle, nssubmeykey: NSString; - lNSSubmenu: NSMenu; -begin - // create the menu item - nstitle := NSStringUtf8(''); - appleMenu := TCocoaMenuItem.alloc.initWithTitle_action_keyEquivalent(nstitle, - objcselector('lclItemSelected:'), NSString.string_); - nstitle.release; - - // add the submenu - lNSSubmenu := NSMenu.alloc.initWithTitle(NSString.string_); - appleMenu.setSubmenu(lNSSubmenu); - lNSSubmenu.release; - - appleMenu.attachAppleMenuItems(); -end; - -// For when there is a menu item with title  -procedure TCocoaMenu.overrideAppleMenu(AItem: TCocoaMenuItem); -begin - if appleMenu <> nil then - begin - if indexOfItem(appleMenu) >= 0 then - removeItem(appleMenu); - appleMenu.release; - appleMenu := nil; - end; - attachedAppleMenu := False; - AItem.attachAppleMenuItems(); -end; - -procedure TCocoaMenu.attachAppleMenu(); -begin - if attachedAppleMenu then Exit; - if appleMenu = nil then Exit; - attachedAppleMenu := True; - insertItem_atIndex(appleMenu, 0); -end; - -function TCocoaMenu.performKeyEquivalent(theEvent: NSEvent): LCLObjCBoolean; -var - OldKeyEq: boolean; -begin - OldKeyEq:=isKeyEq; - isKeyEq := true; - try - Result := inherited performKeyEquivalent(theEvent); - finally - isKeyEq := OldKeyEq; - end; -end; - -function TCocoaMenu.lclIsKeyEquivalent: LCLObjCBoolean; -begin - Result := isKeyEq; -end; - -{ TCocoaMenuITem } - -procedure TCocoaMenuItem.UncheckSiblings(AIsChangingToChecked: LCLObjCBoolean); -var - i: Integer; - lMenuItem, lSibling, lParentMenu: TMenuItem; - lSiblingHandle: NSMenuItem; -begin - //lMenuItem := GetMenuItemHandle(); - lMenuItem := FMenuItemTarget; - if lMenuItem = nil then Exit; - if not lMenuItem.RadioItem then Exit; - if (not AIsChangingToChecked) and (not lMenuItem.Checked) then Exit; - lParentMenu := lMenuItem.Parent; - if lParentMenu = nil then Exit; - for i := 0 to lParentMenu.Count - 1 do - begin - lSibling := lParentMenu.Items[i]; - if lSibling = nil then Continue; - if lSibling = lMenuItem then Continue; - - if lSibling.RadioItem and (lSibling.GroupIndex = lMenuItem.GroupIndex) and - lSibling.HandleAllocated() then - begin - lSiblingHandle := NSMenuItem(lSibling.Handle); - TCocoaWSMenuItem.Do_SetCheck(lSiblingHandle, False); - end; - end; -end; - -function TCocoaMenuItem.GetMenuItemHandle(): TMenuItem; -begin - Result := nil; - if menuItemCallback = nil then Exit; - Result := menuItemCallback.MenuItemTarget; -end; - -procedure TCocoaMenuItem.lclItemSelected(sender:id); -begin - menuItemCallback.ItemSelected; - UncheckSiblings(); -end; - -function TCocoaMenuItem.lclGetCallback: IMenuItemCallback; -begin - result:=menuItemCallback; -end; - -procedure TCocoaMenuItem.lclClearCallback; -begin - menuItemCallback := nil; -end; - -procedure TCocoaMenuItem.attachAppleMenuItems(); -var - item : NSMenuItem; - itemSubMenu: NSMenu; -begin - if attachedAppleMenuItems then Exit; - if not hasSubmenu() then Exit; - - // Separator - submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count); - - // Services - item := LCLMenuItemInit( TCocoaMenuItem.alloc, rsMacOSMenuServices); - item.setTarget(nil); - item.setAction(nil); - submenu.insertItem_atIndex(item, submenu.itemArray.count); - itemSubMenu := NSMenu.alloc.initWithTitle( ControlTitleToNSStr(rsMacOSMenuServices)); - item.setSubmenu(itemSubMenu); - itemSubMenu.release; - NSApplication(NSApp).setServicesMenu(item.submenu); - item.release; - - // Separator - submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count); - - // Hide App Meta-H - item := LCLMenuItemInit( TCocoaMenuItem_HideApp.alloc, Format(rsMacOSMenuHide, [Application.Title]), VK_H, [ssMeta]); - submenu.insertItem_atIndex(item, submenu.itemArray.count); - item.release; - - // Hide Others Meta-Alt-H - item := LCLMenuItemInit( TCocoaMenuItem_HideOthers.alloc, rsMacOSMenuHideOthers, VK_H, [ssMeta, ssAlt]); - submenu.insertItem_atIndex(item, submenu.itemArray.count); - item.release; - - // Show All - item := LCLMenuItemInit( TCocoaMenuItem_ShowAllApp.alloc, rsMacOSMenuShowAll); - submenu.insertItem_atIndex(item, submenu.itemArray.count); - item.release; - - // Separator - submenu.insertItem_atIndex(NSMenuItem.separatorItem, submenu.itemArray.count); - - // Quit Meta-Q - item := LCLMenuItemInit( TCocoaMenuItem_Quit.alloc, Format(rsMacOSMenuQuit, [Application.Title]), VK_Q, [ssMeta]); - submenu.insertItem_atIndex(item, submenu.itemArray.count); - item.release; - - attachedAppleMenuItems := True; -end; - -function TCocoaMenuItem.isValidAppleMenu(): LCLObjCBoolean; -begin - Result := hasSubmenu() and (submenu() <> nil); - Result := Result and ('' = NSStringToString(title)); -end; - -procedure TCocoaMenuItem.menuNeedsUpdate(AMenu: NSMenu); -begin - if not Assigned(menuItemCallback) then Exit; - if not isMenuEnabled then Exit; - - MenuTrackStarted(AMenu); - - if (menu.isKindOfClass(TCocoaMenu)) then - begin - // Issue #37789 - // Cocoa tries to find, if there's a menu with the key event - // so item is not actually selected yet. Thus should not send ItemSelected - if TCocoaMenu(menu).lclIsKeyEquivalent then - Exit; - end; - - //todo: call "measureItem" - menuItemCallback.ItemSelected; -end; - -procedure TCocoaMenuItem.menuDidClose(AMenu: NSMenu); -begin - MenuTrackEnded(AMenu); -end; - -function TCocoaMenuItem.worksWhenModal: LCLObjCBoolean; -begin - // refer to NSMenuItem.target (Apple) documentation - // the method must be implemented in target and return TRUE - // otherwise it won't work for modal! - // - // The method COULD be used to protect the main menu from being clicked - // if a modal window doesn't have a menu. - // But LCL disables (is it?) the app menu manually on modal - Result := true; -end; - -{ menuDidClose should not change the structure of the menu. - The restructuring is causing issues on Apple's special menus (i.e. HELP menu) - See bug #35625 - -procedure TCocoaMenuItem.menuDidClose(AMenu: NSMenu); -var - par : NSMenu; - idx : NSInteger; - mn : NSMenuItem; -begin - // the only purpose of this code is to "invalidate" the submenu of the item. - // an invalidated menu will call menuNeedsUpdate. - // There's no other way in Cocoa to do the "invalidate" - par := amenu.supermenu; - if Assigned(par) then - begin - idx := par.indexOfItemWithSubmenu(AMenu); - if idx<>NSNotFound then - begin - mn := par.itemAtIndex(idx); - mn.setSubmenu(nil); - mn.setSubmenu(AMenu); - end; - end; -end; -} - -procedure TCocoaMenuItem_HideApp.lclItemSelected(sender: id); -begin - // Applicaiton.Minimize, calls WidgetSet.AppMinimize; - // which calls NSApplication.hide() anyway - Application.Minimize; -end; - -procedure TCocoaMenuItem_HideOthers.lclItemSelected(sender: id); -begin - NSApplication(NSApp).hideOtherApplications(sender); -end; - -procedure TCocoaMenuItem_Quit.lclItemSelected(sender: id); -begin - {$ifdef COCOALOOPHIJACK} - // see bug #36265. if hot-key (Cmd+Q) is used the menu item - // would be called once. 1) in LCL controlled loop 2) after the loop finished - // The following if statement prevents "double" form close - if LoopHiJackEnded then Exit; - {$endif} - // Should be used instead of Application.Terminate when possible - // to allow events to be sent, see bug 32148 - if Assigned(Application.MainForm) then - Application.MainForm.Close - else - Application.Terminate; -end; - { TCocoaWSMenu } {------------------------------------------------------------------------------ @@ -975,63 +531,4 @@ begin APopupMenu.Close; // notify LCL popup menu end; -procedure ShortcutToKeyEquivalent(const AShortCut: TShortcut; out Key: NSString; out shiftKeyMask: NSUInteger); -var - w: word; - s: TShiftState; -begin - ShortCutToKey(AShortCut, w, s); - key := VirtualKeyCodeToMacString(w); - shiftKeyMask := 0; - if ssShift in s then - ShiftKeyMask := ShiftKeyMask + NSShiftKeyMask; - if ssAlt in s then - ShiftKeyMask := ShiftKeyMask + NSAlternateKeyMask; - if ssCtrl in s then - ShiftKeyMask := ShiftKeyMask + NSControlKeyMask; - if ssMeta in s then - ShiftKeyMask := ShiftKeyMask + NSCommandKeyMask; -end; - -procedure ToggleAppNSMenu(mn: NSMenu; ALogicalEnabled: Boolean); -var - it : NSMenuItem; - obj : NSObject; - enb : Boolean; -begin - if not Assigned(mn) then Exit; - for obj in mn.itemArray do begin - if not obj.isKindOfClass(NSMenuItem) then continue; - it := NSMenuItem(obj); - enb := ALogicalEnabled; - if enb and (it.isKindOfClass(TCocoaMenuItem)) then - begin - enb := not Assigned(TCocoaMenuItem(it).FMenuItemTarget) - or ( TCocoaMenuItem(it).FMenuItemTarget.Enabled ); - end; - {$ifdef BOOLFIX} - it.setEnabled_( Ord(enb)); - {$else} - it.setEnabled(enb); - {$endif} - if (it.hasSubmenu) then - begin - ToggleAppNSMenu(it.submenu, ALogicalEnabled); - end; - end; -end; - -function ToggleAppMenu(ALogicalEnabled: Boolean): Boolean; -begin - Result := isMenuEnabled; - ToggleAppNSMenu( NSApplication(NSApp).mainMenu, ALogicalEnabled ); - isMenuEnabled := ALogicalEnabled; -end; - -initialization - -finalization - MenuTrackCancelAll; - if menuTrack <> nil then menuTrack.release; - end.