From f4acf204fb15de85ca611ce08e08f783faa7fb14 Mon Sep 17 00:00:00 2001 From: rich2014 Date: Sun, 23 Oct 2022 00:21:58 +0800 Subject: [PATCH] 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) --- lcl/interfaces/cocoa/cocoaint.pas | 20 +----- lcl/interfaces/cocoa/cocoawinapi.inc | 16 +---- lcl/interfaces/cocoa/cocoawindows.pas | 89 +++++++++++++++++++-------- 3 files changed, 67 insertions(+), 58 deletions(-) diff --git a/lcl/interfaces/cocoa/cocoaint.pas b/lcl/interfaces/cocoa/cocoaint.pas index 62c52ef5f7..aa656280e3 100644 --- a/lcl/interfaces/cocoa/cocoaint.pas +++ b/lcl/interfaces/cocoa/cocoaint.pas @@ -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); diff --git a/lcl/interfaces/cocoa/cocoawinapi.inc b/lcl/interfaces/cocoa/cocoawinapi.inc index 4768ed857c..beb2634c14 100644 --- a/lcl/interfaces/cocoa/cocoawinapi.inc +++ b/lcl/interfaces/cocoa/cocoawinapi.inc @@ -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 diff --git a/lcl/interfaces/cocoa/cocoawindows.pas b/lcl/interfaces/cocoa/cocoawindows.pas index cfb85985a6..ee7497e881 100644 --- a/lcl/interfaces/cocoa/cocoawindows.pas +++ b/lcl/interfaces/cocoa/cocoawindows.pas @@ -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;