Cocoa/CrossRef: Remove unit cross-references between CocoaInt and CocoaWSMenus

This commit is contained in:
rich2014 2023-11-19 15:25:06 +08:00
parent 92cab2ee2a
commit 365360bd90
6 changed files with 534 additions and 510 deletions

View File

@ -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,

View File

@ -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.

View File

@ -34,7 +34,7 @@ uses
WSForms, WSLCLClasses, WSDialogs,
// LCL Cocoa
CocoaUtils, CocoaGDIObjects
,Cocoa_Extra, CocoaWSMenus;
,Cocoa_Extra, CocoaMenus;
type

View File

@ -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

View File

@ -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;

View File

@ -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.