FIX: COCOA: send KillFocus/SetFocus messages to LCL at the right time, adapt to LCL, just like Win32 (by TCocoaWindow.makeFirstResponder() issues with infinite loops fixed)

This commit is contained in:
rich2014 2022-10-23 00:21:58 +08:00 committed by Maxim Ganetsky
parent 467026508f
commit f4acf204fb
3 changed files with 67 additions and 58 deletions

View File

@ -519,7 +519,6 @@ var
allowcocoa : Boolean;
idx: integer;
win : NSWindow;
cbnew : ICommonCallback;
responder : NSResponder;
begin
{$ifdef COCOALOOPNATIVE}
@ -601,25 +600,8 @@ begin
finally
// Focus change notification used to be in makeFirstResponder method
// However, it caused many issues with infinite loops.
// Sometimes Cocoa like to switch focus to window (temporary) (i.e. when switching tabs)
// That's causing a conflict with LCL. LCL tries to switch focus back
// to the original control. And Cocoa keep switching it back to the Window.
// (Note, that for Cocoa, window should ALWAYS be focusable)
// Thus, Focus switching notification was moved to post event handling.
//
// can't have this code in TCocoaWindow, because some key events are not forwarded
// to the window
cbnew := win.firstResponder.lclGetCallback;
if not isCallbackForSameObject(cb, cbnew) then
begin
if Assigned(cb) then cb.ResignFirstResponder;
cbnew := win.firstResponder.lclGetCallback;
if Assigned(cbnew) then cbnew.BecomeFirstResponder;
end;
CocoaWidgetSet.ReleaseToCollect(idx);
end;
{$ifdef COCOALOOPNATIVE}
if CocoaWidgetSet.FTerminating then stop(nil);

View File

@ -2535,12 +2535,6 @@ begin
Result := True;
end;
function NeedsFocusNotifcation(event: NSEvent; win: NSWindow): Boolean;
begin
Result := (Assigned(win))
and (not Assigned(event) or (event.window <> win));
end;
function TCocoaWidgetSet.SetFocus(Handle: HWND): HWND;
var
Obj: NSObject;
@ -2566,15 +2560,7 @@ begin
if lView.window <> nil then
begin
lView.window.makeKeyWindow;
if lView.window.makeFirstResponder(lView.lclContentView) then
begin
// initial focus set (right before the event loop starts)
if NeedsFocusNotifcation(NSApp.currentEvent, lView.window) then
begin
cb := lView.lclGetCallback;
if Assigned(cb) then cb.BecomeFirstResponder;
end;
end;
lView.window.makeFirstResponder(lView.lclContentView);
end else
Result := 0; // the view is on window, cannot set focus. Fail
end else

View File

@ -145,10 +145,8 @@ type
keepWinLevel : NSInteger;
//LCLForm: TCustomForm;
procedure dealloc; override;
function acceptsFirstResponder: LCLObjCBoolean; override;
function makeFirstResponder(aResponder: NSResponder): ObjCBOOL; override;
function canBecomeKeyWindow: LCLObjCBoolean; override;
function becomeFirstResponder: LCLObjCBoolean; override;
function resignFirstResponder: LCLObjCBoolean; override;
function lclGetCallback: ICommonCallback; override;
procedure lclClearCallback; override;
// mouse
@ -737,6 +735,7 @@ begin
//DebugLn('[TCocoaWindow.windowWillReturnFieldEditor_toObject]');
Result := nil;
// NSTextView itself is NSTextFieldEditor, then windowWillReturnFieldEditor never called for NSTextView
if (NSObject(client).isKindOfClass(NSTextField)) and Assigned(NSObject(client).lclGetCallBack) then
begin
if (fieldEditor = nil) then
@ -842,32 +841,11 @@ begin
inherited dealloc;
end;
function TCocoaWindow.acceptsFirstResponder: LCLObjCBoolean;
begin
Result := True;
end;
function TCocoaWindow.canBecomeKeyWindow: LCLObjCBoolean;
begin
Result := Assigned(callback) and callback.CanActivate;
end;
function TCocoaWindow.becomeFirstResponder: LCLObjCBoolean;
begin
Result := inherited becomeFirstResponder;
// uncommenting the following lines starts an endless focus loop
// if Assigned(callback) then
// callback.BecomeFirstResponder;
end;
function TCocoaWindow.resignFirstResponder: LCLObjCBoolean;
begin
Result := inherited resignFirstResponder;
// if Assigned(callback) then
// callback.ResignFirstResponder;
end;
function TCocoaWindow.lclGetCallback: ICommonCallback;
begin
Result := callback;
@ -1022,6 +1000,69 @@ begin
inherited keyDown(event);
end;
// return proper focused responder by kind of class of NSResponder
function getProperFocusedResponder( const aResponder : NSResponder): NSResponder;
var
dl : NSObject;
begin
Result := aResponder;
if Result.isKindOfClass(TCocoaFieldEditor) then
begin
dl := {%H-}NSObject( TCocoaFieldEditor(Result).delegate );
if Assigned(dl) and (dl.isKindOfClass(NSView)) then
Result := NSResponder(dl);
end
else
if Result.isKindOfClass(NSWindow) then
begin
// critical step to avoid infinite loops caused by
// 'Focus-fight' between LCL and COCOA
Result := nil;
end
else
begin
Result := Result.lclContentView;
end;
end;
// send KillFocus/SetFocus messages to LCL at the right time
// 1. KillFocus/SetFocus messages should be sent after LCLIntf.SetFocus() in LCL,
// and before generating CM_UIACTIVATE message in LCL,
// this adapts to LCL, just like Win32.
// 2. if KillFocus/SetFocus messages are delayed,
// such as at the end of TCocoaApplication.sendevent(), it will cause many problems.
// for example, there are two buttons showing the selected state.
// 3. makeFirstResponder() already avoids infinite loops caused by 'Focus-fight'
// between LCL and COCOA, see also:
// https://wiki.lazarus.freepascal.org/Cocoa_Internals/Application#Focus_Change
function TCocoaWindow.makeFirstResponder( aResponder : NSResponder ): ObjCBOOL;
var
lastResponder : NSResponder;
newResponder : NSResponder;
begin
lastResponder := self.firstResponder;
newResponder := aResponder;
// do toggle Focused Control
// Result=false when the focused control has not been changed
Result := inherited makeFirstResponder( newResponder );
if not Result then exit;
// send KillFocus/SetFocus messages to LCL
// 1st: send KillFocus Message first
lastResponder := getProperFocusedResponder( lastResponder );
if Assigned(lastResponder) and Assigned(lastResponder.lclGetCallback) then
lastResponder.lclGetCallback.ResignFirstResponder;
// 2st: send SetFocus Message
// focused control may not be aResponder
// get focused control via firstResponder again
newResponder := getProperFocusedResponder( self.firstResponder );
if Assigned(newResponder) and Assigned(newResponder.lclGetCallback) then
newResponder.lclGetCallback.BecomeFirstResponder;
end;
function TCocoaWindowContentDocument.draggingEntered(sender: NSDraggingInfoProtocol): NSDragOperation;
begin
Result := NSDragOperationNone;