Cocoa: better handling of TCocoaWindow.makeFirstResponder() reentrancy

This commit is contained in:
rich2014 2023-07-14 21:04:33 +08:00
parent 957d62af56
commit 185e72ea31

View File

@ -119,6 +119,9 @@ type
TCocoaWindowContent = objcclass; TCocoaWindowContent = objcclass;
TCocoaWindow = objcclass(NSWindow, NSWindowDelegateProtocol) TCocoaWindow = objcclass(NSWindow, NSWindowDelegateProtocol)
private
// for the reentrancy of makeFirstResponder()
makeFirstResponderCount: Integer;
protected protected
fieldEditor: TCocoaFieldEditor; fieldEditor: TCocoaFieldEditor;
firedMouseEvent: Boolean; firedMouseEvent: Boolean;
@ -1003,28 +1006,36 @@ begin
end; end;
// return proper focused responder by kind of class of NSResponder // return proper focused responder by kind of class of NSResponder
function getProperFocusedResponder( const aResponder : NSResponder): NSResponder; function getProperFocusedResponder( const aResponder : NSResponder ): NSResponder;
var
dl : NSObject;
begin begin
Result := aResponder; 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 if Result.isKindOfClass(NSWindow) then
Result:= TCocoaWindowContent(NSWindow(Result).contentView).documentView;
end;
// return responder callback by kind of class of NSResponder
function getResponderCallback( const aResponder : NSResponder ): ICommonCallback;
var
newResponder: NSResponder;
dl : NSObject;
begin
Result:= nil;
if not Assigned(aResponder) then exit;
newResponder := aResponder;
if newResponder.isKindOfClass(NSText) then
begin begin
// critical step to avoid infinite loops caused by dl := {%H-}NSObject( NSText(newResponder).delegate );
// 'Focus-fight' between LCL and COCOA if Assigned(dl) and (dl.isKindOfClass(NSView)) then
Result := nil; newResponder := NSResponder(dl);
end end
else else
begin begin
Result := Result.lclContentView; newResponder := newResponder.lclContentView;
end; end;
if Assigned(newResponder) then
Result:= newResponder.lclGetCallback;
end; end;
// send KillFocus/SetFocus messages to LCL at the right time // send KillFocus/SetFocus messages to LCL at the right time
@ -1037,31 +1048,44 @@ end;
// 3. makeFirstResponder() already avoids infinite loops caused by 'Focus-fight' // 3. makeFirstResponder() already avoids infinite loops caused by 'Focus-fight'
// between LCL and COCOA, see also: // between LCL and COCOA, see also:
// https://wiki.lazarus.freepascal.org/Cocoa_Internals/Application#Focus_Change // https://wiki.lazarus.freepascal.org/Cocoa_Internals/Application#Focus_Change
// 4. makeFirstResponder() is Reentrant and Thread-safe
//
function TCocoaWindow.makeFirstResponder( aResponder : NSResponder ): ObjCBOOL; function TCocoaWindow.makeFirstResponder( aResponder : NSResponder ): ObjCBOOL;
var var
lastResponder : NSResponder; lastResponder : NSResponder;
newResponder : NSResponder; newResponder : NSResponder;
cb : ICommonCallback;
begin begin
lastResponder := self.firstResponder; inc( makeFirstResponderCount );
newResponder := aResponder; try
lastResponder := self.firstResponder;
newResponder := getProperFocusedResponder( aResponder );
if lastResponder = newResponder then exit;
// do toggle Focused Control // do toggle Focused Control
// Result=false when the focused control has not been changed // Result=false when the focused control has not been changed
Result := inherited makeFirstResponder( newResponder ); // TCocoaWindow.makeFirstResponder() may be triggered reentrant here
if not Result then exit; Result := inherited makeFirstResponder( newResponder );
if not Result then exit;
// send KillFocus/SetFocus messages to LCL // send KillFocus/SetFocus messages to LCL only at level one
// 1st: send KillFocus Message first if makeFirstResponderCount > 1 then
lastResponder := getProperFocusedResponder( lastResponder ); exit;
if Assigned(lastResponder) and Assigned(lastResponder.lclGetCallback) then
lastResponder.lclGetCallback.ResignFirstResponder;
// 2st: send SetFocus Message // 1st: send KillFocus Message first
// focused control may not be aResponder cb:= getResponderCallback( lastResponder );
// get focused control via firstResponder again if Assigned(cb) then
newResponder := getProperFocusedResponder( self.firstResponder ); cb.ResignFirstResponder;
if Assigned(newResponder) and Assigned(newResponder.lclGetCallback) then
newResponder.lclGetCallback.BecomeFirstResponder; // 2st: send SetFocus Message
// TCocoaWindow.makeFirstResponder() may be triggered reentrant here
cb := getResponderCallback( self.firstResponder );
if Assigned(cb) then
cb.BecomeFirstResponder;
finally
dec( makeFirstResponderCount );
end;
end; end;