mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-28 22:33:41 +02:00
2114 lines
64 KiB
ObjectPascal
2114 lines
64 KiB
ObjectPascal
unit CocoaWSCommon;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{$modeswitch objectivec1}
|
|
{$include cocoadefines.inc}
|
|
{.$DEFINE COCOA_DEBUG_SETBOUNDS}
|
|
|
|
interface
|
|
|
|
uses
|
|
Types, Classes, Controls, SysUtils,
|
|
WSControls, LCLType, LCLMessageGlue, LMessages, LCLProc, LCLIntf, Graphics, Forms,
|
|
StdCtrls,
|
|
CocoaAll, CocoaInt, CocoaConfig, CocoaPrivate, CocoaCallback, CocoaUtils,
|
|
CocoaCustomControl, CocoaScrollers, CocoaWSScrollers, CocoaFullControlEdit,
|
|
CocoaGDIObjects, CocoaCursor, CocoaCaret, cocoa_extra;
|
|
|
|
type
|
|
{ TLCLCommonCallback }
|
|
|
|
TLCLCommonCallback = class(TObject, ICommonCallBack)
|
|
private
|
|
var
|
|
FPropStorage: TStringList;
|
|
FContext: TCocoaContext;
|
|
FHasCaret: Boolean;
|
|
FBoundsReportedToChildren: boolean;
|
|
FIsOpaque:boolean;
|
|
FIsEventRouting:boolean;
|
|
FLastWheelWasHorz:boolean;
|
|
protected
|
|
function GetHasCaret: Boolean;
|
|
procedure SetHasCaret(AValue: Boolean);
|
|
function GetIsOpaque: Boolean;
|
|
procedure SetIsOpaque(AValue: Boolean);
|
|
function GetShouldBeEnabled: Boolean;
|
|
protected
|
|
FTarget: TWinControl;
|
|
_KeyMsg : TLMKey;
|
|
_CharMsg : TLMKey;
|
|
_SendChar : Boolean;
|
|
_IsSysKey : Boolean;
|
|
_IsKeyDown : Boolean;
|
|
_KeyHandled: Boolean;
|
|
_isCocoaOnlyState: Boolean;
|
|
_UTF8Character : array [0..7] of TUTF8Char;
|
|
_UTF8Charcount : Integer;
|
|
_handleFrame: NSView; // HWND and "frame" (rectangle) of the a control
|
|
private
|
|
procedure send_UTF8KeyPress();
|
|
procedure send_CN_CHAR_Message();
|
|
procedure send_LM_KEYDOWN_Message();
|
|
procedure send_LM_CHAR_Message();
|
|
protected
|
|
procedure OffsetMousePos(LocInWin: NSPoint; out PtInBounds, PtInClient, PtForChildCtrls: TPoint );
|
|
procedure ScreenMousePos(var Point: NSPoint);
|
|
procedure KeyEvBeforeDown;
|
|
procedure KeyEvBeforeUp;
|
|
procedure KeyEvAfterUp;
|
|
procedure KeyEvFlagsChanged(Event: NSEvent);
|
|
procedure KeyEvPrepare(Event: NSEvent); virtual;
|
|
public
|
|
Owner: NSObject;
|
|
BlockCocoaUpDown: Boolean;
|
|
BlockCocoaKeyBeep: Boolean;
|
|
BlockCocoaMouseMove: Boolean;
|
|
SuppressTabDown: Boolean; // all tabs should be suppressed, so Cocoa would not switch focus
|
|
ForceReturnKeyDown: Boolean; // send keyDown/LM_KEYDOWN for Return even if handled by IntfUTF8KeyPress/CN_CHAR
|
|
|
|
lastMouseDownUp: NSTimeInterval; // the last processed mouse Event
|
|
lastMouseWithForce: Boolean;
|
|
|
|
constructor Create(AOwner: NSObject; ATarget: TWinControl; AHandleFrame: NSView = nil); virtual;
|
|
destructor Destroy; override;
|
|
function GetPropStorage: TStringList;
|
|
function GetContext: TCocoaContext;
|
|
function GetTarget: TObject;
|
|
function GetCallbackObject: TObject;
|
|
function GetCaptureControlCallback: ICommonCallBack;
|
|
procedure SendContextMenu(Event: NSEvent; out ContextMenuHandled: Boolean);
|
|
function MouseUpDownEvent(Event: NSEvent; AForceAsMouseUp: Boolean = False; AOverrideBlock: Boolean = False): Boolean; virtual;
|
|
|
|
procedure KeyEvAfterDown(out AllowCocoaHandle: boolean);
|
|
procedure KeyEvBefore(Event: NSEvent; out AllowCocoaHandle: boolean);
|
|
procedure KeyEvAfter;
|
|
procedure KeyEvHandled;
|
|
procedure SetTabSuppress(ASuppress: Boolean);
|
|
function CanFocus: Boolean; virtual;
|
|
|
|
function IsCocoaOnlyState: Boolean;
|
|
procedure SetCocoaOnlyState(state: Boolean);
|
|
|
|
procedure MouseClick; virtual;
|
|
function MouseMove(Event: NSEvent): Boolean; virtual;
|
|
function scrollWheel(Event: NSEvent): Boolean; virtual;
|
|
procedure frameDidChange(sender: id); virtual;
|
|
procedure boundsDidChange(sender: id); virtual;
|
|
procedure BecomeFirstResponder; virtual;
|
|
procedure ResignFirstResponder; virtual;
|
|
procedure DidBecomeKeyNotification; virtual;
|
|
procedure DidResignKeyNotification; virtual;
|
|
function SendOnEditCut: Boolean; virtual;
|
|
function SendOnEditPaste: Boolean; virtual;
|
|
procedure SendOnChange; virtual;
|
|
procedure SendOnTextChanged; virtual; // text controls (like spin) respond to OnChange for this event, but not for SendOnChange
|
|
procedure scroll(isVert: Boolean; Pos: Integer; AScrollPart: NSScrollerPart); virtual;
|
|
function DeliverMessage(var Msg): LRESULT; virtual; overload;
|
|
function DeliverMessage(Msg: Cardinal; WParam: WParam; LParam: LParam): LResult; virtual; overload;
|
|
procedure Draw(ControlContext: NSGraphicsContext; const bounds, dirty: NSRect); virtual;
|
|
procedure DrawBackground(ctx: NSGraphicsContext; const bounds, dirtyRect: NSRect); virtual;
|
|
procedure DrawOverlay(ControlContext: NSGraphicsContext; const bounds, dirty: NSRect); virtual;
|
|
procedure RemoveTarget; virtual;
|
|
|
|
procedure InputClientInsertText(const utf8: string);
|
|
|
|
function HandleFrame: NSView;
|
|
procedure SetHandleFrame( AHandleFrame: NSView );
|
|
|
|
property HasCaret: Boolean read GetHasCaret write SetHasCaret;
|
|
property Target: TWinControl read FTarget;
|
|
property IsOpaque: Boolean read GetIsOpaque write SetIsOpaque;
|
|
end;
|
|
|
|
TLCLCommonCallBackClass = class of TLCLCommonCallBack;
|
|
|
|
{ TLCLFullControlEditCallBack }
|
|
|
|
// CallBack for LCL Full Control Edit (such as SynEdit/ATSynEdit)
|
|
TLCLFullControlEditCallBack = class(TLCLCommonCallBack)
|
|
protected
|
|
procedure KeyEvPrepare(Event: NSEvent); override;
|
|
end;
|
|
|
|
{ TCocoaWSWinControl }
|
|
|
|
{ TCocoaWSControl }
|
|
|
|
TCocoaWSControl = class(TWSControl)
|
|
published
|
|
class function GetCanvasScaleFactor(const AControl: TControl): Double; override;
|
|
end;
|
|
|
|
{ TCocoaWSWinControl }
|
|
|
|
TCocoaWSWinControl = class(TWSWinControl)
|
|
published
|
|
class function CreateHandle(const AWinControl: TWinControl;
|
|
const AParams: TCreateParams): TLCLHandle; override;
|
|
class procedure DestroyHandle(const AWinControl: TWinControl); override;
|
|
class function GetCanvasScaleFactor(const AControl: TControl): Double; override;
|
|
class procedure SetText(const AWinControl: TWinControl; const AText: String); override;
|
|
class function GetText(const AWinControl: TWinControl; var AText: String): Boolean; override;
|
|
class function GetTextLen(const AWinControl: TWinControl; var ALength: Integer): Boolean; override;
|
|
|
|
class function GetClientBounds(const AWincontrol: TWinControl; var ARect: TRect): Boolean; override;
|
|
class function GetClientRect(const AWincontrol: TWinControl; var ARect: TRect): Boolean; override;
|
|
class procedure GetPreferredSize(const AWinControl: TWinControl; var PreferredWidth, PreferredHeight: integer; WithThemeSpace: Boolean); override;
|
|
class procedure SetBounds(const AWinControl: TWinControl; const ALeft, ATop, AWidth, AHeight: Integer); override;
|
|
class procedure SetCursor(const AWinControl: TWinControl; const ACursor: HCursor); override;
|
|
class procedure SetFont(const AWinControl: TWinControl; const AFont: TFont); override;
|
|
class procedure SetColor(const AWinControl: TWinControl); override;
|
|
class procedure SetChildZPosition(const AWinControl, AChild: TWinControl; const AOldPos, ANewPos: Integer; const AChildren: TFPList); override;
|
|
class procedure ShowHide(const AWinControl: TWinControl); override;
|
|
class procedure Invalidate(const AWinControl: TWinControl); override;
|
|
class procedure PaintTo(const AWinControl: TWinControl; ADC: HDC; X, Y: Integer); override;
|
|
end;
|
|
|
|
{ TCocoaWSCustomControl }
|
|
|
|
TCocoaWSCustomControl = class(TWSCustomControl)
|
|
published
|
|
class function CreateHandle(const AWinControl: TWinControl;
|
|
const AParams: TCreateParams): TLCLHandle; override;
|
|
class procedure SetBorderStyle(const AWinControl: TWinControl;
|
|
const ABorderStyle: TBorderStyle); override;
|
|
end;
|
|
|
|
procedure UpdateControlFocusRing( cocoaControl: NSView; lclControl: TWinControl );
|
|
procedure ScrollViewSetScrollStyles(AScroll: TCocoaScrollView; AStyles: TScrollStyle);
|
|
procedure ScrollViewSetBorderStyle(sv: NSScrollView; astyle: TBorderStyle);
|
|
|
|
function ButtonStateToShiftState(BtnState: PtrUInt): TShiftState;
|
|
function CocoaModifiersToKeyState(AModifiers: NSUInteger): PtrInt;
|
|
function CocoaPressedMouseButtonsToKeyState(AMouseButtons: NSUInteger): PtrInt;
|
|
function CocoaModifiersToShiftState(AModifiers: NSUInteger; AMouseButtons: NSUInteger): TShiftState;
|
|
|
|
function NSObjectDebugStr(obj: NSObject): string;
|
|
function CallbackDebugStr(cb: ICommonCallback): string;
|
|
procedure DebugDumpParents(fromView: NSView);
|
|
|
|
const
|
|
VerticalScrollerVisible: array[TScrollStyle] of boolean = (
|
|
{ssNone } false,
|
|
{ssHorizontal } false,
|
|
{ssVertical } true,
|
|
{ssBoth } true,
|
|
{ssAutoHorizontal} false,
|
|
{ssAutoVertical } true,
|
|
{ssAutoBoth } true
|
|
);
|
|
|
|
HorizontalScrollerVisible: array[TScrollStyle] of boolean = (
|
|
{ssNone } false,
|
|
{ssHorizontal } true,
|
|
{ssVertical } false,
|
|
{ssBoth } true,
|
|
{ssAutoHorizontal} true,
|
|
{ssAutoVertical } false,
|
|
{ssAutoBoth } true
|
|
);
|
|
|
|
ScrollerAutoHide: array[TScrollStyle] of boolean = (
|
|
{ssNone } false,
|
|
{ssHorizontal } false,
|
|
{ssVertical } false,
|
|
{ssBoth } false,
|
|
{ssAutoHorizontal} true,
|
|
{ssAutoVertical } true,
|
|
{ssAutoBoth } true
|
|
);
|
|
|
|
implementation
|
|
|
|
uses
|
|
Math;
|
|
|
|
type
|
|
TWinControlAccess = class(TWinControl);
|
|
|
|
var
|
|
LastMouse: TLastMouseInfo;
|
|
LastMouseLeftButtonAsRight: Boolean;
|
|
|
|
function ButtonStateToShiftState(BtnState: PtrUInt): TShiftState;
|
|
begin
|
|
Result := [];
|
|
if BtnState and MK_SHIFT > 0 then Include(Result, ssShift);
|
|
if BtnState and MK_CONTROL > 0 then Include(Result, ssCtrl);
|
|
if BtnState and MK_ALT > 0 then Include(Result, ssAlt);
|
|
if BtnState and MK_LBUTTON > 0 then Include(Result, ssLeft);
|
|
if BtnState and MK_RBUTTON > 0 then Include(Result, ssRight);
|
|
if BtnState and MK_MBUTTON > 0 then Include(Result, ssMiddle);
|
|
if BtnState and MK_XBUTTON1 > 0 then Include(Result, ssExtra1);
|
|
if BtnState and MK_XBUTTON2 > 0 then Include(Result, ssExtra2);
|
|
// what MK_xxx used for Meta?
|
|
end;
|
|
|
|
function CocoaModifiersToShiftState(AModifiers: NSUInteger; AMouseButtons: NSUInteger): TShiftState;
|
|
begin
|
|
Result := [];
|
|
if AModifiers and NSShiftKeyMask <> 0 then Include(Result, ssShift);
|
|
if AModifiers and NSControlKeyMask <> 0 then Include(Result, ssCtrl);
|
|
if AModifiers and NSAlternateKeyMask <> 0 then Include(Result, ssAlt);
|
|
if AModifiers and NSCommandKeyMask <> 0 then Include(Result, ssMeta);
|
|
|
|
if AMouseButtons and (1 shl 0) <> 0 then Include(Result, ssLeft);
|
|
if AMouseButtons and (1 shl 1) <> 0 then Include(Result, ssRight);
|
|
if AMouseButtons and (1 shl 2) <> 0 then Include(Result, ssMiddle);
|
|
if AMouseButtons and (1 shl 3) <> 0 then Include(Result, ssExtra1);
|
|
if AMouseButtons and (1 shl 4) <> 0 then Include(Result, ssExtra2);
|
|
end;
|
|
|
|
procedure UpdateControlFocusRing(cocoaControl: NSView; lclControl: TWinControl);
|
|
const
|
|
NSFocusRing : array [TBorderStyle] of NSBorderType = (
|
|
NSFocusRingTypeNone, // bsNone
|
|
NSFocusRingTypeDefault // bsSingle
|
|
);
|
|
var
|
|
frs: TCocoaConfigFocusRing.Strategy;
|
|
borderStyle: TBorderStyle;
|
|
begin
|
|
frs:= CocoaConfigFocusRing.getStrategy( cocoaControl.className );
|
|
case frs of
|
|
TCocoaConfigFocusRing.Strategy.none:
|
|
cocoaControl.setFocusRingType( NSFocusRingTypeNone );
|
|
TCocoaConfigFocusRing.Strategy.required:
|
|
cocoaControl.setFocusRingType( NSFocusRingTypeExterior );
|
|
TCocoaConfigFocusRing.Strategy.border: begin
|
|
borderStyle:= TWinControlAccess(lclControl).BorderStyle;
|
|
cocoaControl.setFocusRingType( NSFocusRing[borderStyle] );
|
|
end;
|
|
// TCocoaFocusRingStrategy.default: no need to set FocusRing
|
|
end;
|
|
end;
|
|
|
|
procedure ScrollViewSetScrollStyles(AScroll: TCocoaScrollView; AStyles: TScrollStyle);
|
|
begin
|
|
AScroll.setHasVerticalScroller(VerticalScrollerVisible[AStyles]);
|
|
AScroll.setHasHorizontalScroller(HorizontalScrollerVisible[AStyles]);
|
|
AScroll.setAutohidesScrollers(ScrollerAutoHide[AStyles]);
|
|
end;
|
|
|
|
{ TLCLCommonCallback }
|
|
|
|
function TLCLCommonCallback.GetHasCaret: Boolean;
|
|
begin
|
|
Result := FHasCaret;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SetHasCaret(AValue: Boolean);
|
|
begin
|
|
FHasCaret := AValue;
|
|
end;
|
|
|
|
function CocoaModifiersToKeyState(AModifiers: NSUInteger): PtrInt;
|
|
begin
|
|
Result := 0;
|
|
if AModifiers and NSShiftKeyMask <> 0 then
|
|
Result := Result or MK_SHIFT;
|
|
if AModifiers and NSControlKeyMask <> 0 then
|
|
Result := Result or MK_CONTROL;
|
|
if AModifiers and NSAlternateKeyMask <> 0 then
|
|
Result := Result or MK_ALT;
|
|
end;
|
|
|
|
function CocoaPressedMouseButtonsToKeyState(AMouseButtons: NSUInteger): PtrInt;
|
|
begin
|
|
Result := 0;
|
|
if AMouseButtons and (1 shl 0) <> 0 then
|
|
Result := Result or MK_LBUTTON;
|
|
if AMouseButtons and (1 shl 1) <> 0 then
|
|
Result := Result or MK_RBUTTON;
|
|
if AMouseButtons and (1 shl 2) <> 0 then
|
|
Result := Result or MK_MBUTTON;
|
|
if AMouseButtons and (1 shl 3) <> 0 then
|
|
Result := Result or MK_XBUTTON1;
|
|
if AMouseButtons and (1 shl 4) <> 0 then
|
|
Result := Result or MK_XBUTTON2;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.OffsetMousePos(LocInWin: NSPoint; out PtInBounds, PtInClient, PtForChildCtrls: TPoint);
|
|
var
|
|
lView: NSView;
|
|
pt: NSPoint;
|
|
cr: TRect;
|
|
es: NSScrollView;
|
|
r: NSRect;
|
|
begin
|
|
if Owner.isKindOfClass(NSWindow) then
|
|
begin
|
|
PtInBounds.x := Round(LocInWin.x);
|
|
PtInBounds.y := Round(NSWindow(Owner).contentView.bounds.size.height - LocInWin.y);
|
|
PtInClient := PtInBounds; // todo: it's different. But Owner is never NSWindow (it's TConentWindowView instead)
|
|
PtForChildCtrls := PtInClient;
|
|
end
|
|
else if Owner.isKindOfClass(NSView) then
|
|
begin
|
|
pt := LocInWin;
|
|
|
|
NSView(Owner).lclOffsetMousePos(pt);
|
|
PtInBounds.x := Round(pt.x);
|
|
PtInBounds.y := Round(pt.y);
|
|
|
|
//pt := NSView(Owner).frame.origin;
|
|
//if NSView(Owner).frame.
|
|
cr := NSView(Owner).lclClientFrame;
|
|
PtInClient.x := Round({PtInBounds.x - }pt.x - cr.Left);
|
|
PtInClient.y := Round({PtInBounds.y - }pt.y - cr.Top);
|
|
|
|
// child ctrls need not LayoutDelta
|
|
cr := NSView(Owner).lclGetFrameToLayoutDelta;
|
|
PtForChildCtrls := PtInClient;
|
|
PtForChildCtrls.x := PtForChildCtrls.x + cr.Left;
|
|
PtForChildCtrls.y := PtForChildCtrls.y + cr.Top;
|
|
|
|
es := NSView(Owner).enclosingScrollView;
|
|
if Assigned(es) and (es.documentView = NSView(Owner)) then begin
|
|
r := es.documentVisibleRect;
|
|
if NSView(Owner).isFlipped then
|
|
r.origin.y := (es.documentView.frame.size.height - r.size.height - r.origin.y);
|
|
inc(PtForChildCtrls.y, Round(r.origin.y));
|
|
inc(PtForChildCtrls.x, Round(r.origin.x));
|
|
end;
|
|
|
|
end else
|
|
begin
|
|
PtInBounds.x := Round(LocInWin.x);
|
|
PtInBounds.y := Round(LocInWin.y);
|
|
PtInClient := PtInBounds;
|
|
PtForChildCtrls := PtInClient;
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.ScreenMousePos(var Point: NSPoint);
|
|
var
|
|
f: NSRect;
|
|
lWindow: NSWindow;
|
|
begin
|
|
lWindow := NSWindow(GetNSObjectWindow(Owner));
|
|
if lWindow <> nil then
|
|
begin
|
|
f := lWindow.frame;
|
|
Point.x := Point.x + f.origin.x;
|
|
Point.y := NSGlobalScreenBottom - ( Point.y + f.origin.y );
|
|
end;
|
|
end;
|
|
|
|
constructor TLCLCommonCallback.Create(AOwner: NSObject; ATarget: TWinControl; AHandleFrame: NSView);
|
|
begin
|
|
inherited Create;
|
|
Owner := AOwner;
|
|
if Assigned(AHandleFrame) then
|
|
_handleFrame := AHandleFrame
|
|
else if Owner.isKindOfClass(NSView) then
|
|
_handleFrame := NSView(AOwner);
|
|
FTarget := ATarget;
|
|
FContext := nil;
|
|
FHasCaret := False;
|
|
FPropStorage := TStringList.Create;
|
|
FPropStorage.Sorted := True;
|
|
FPropStorage.Duplicates := dupAccept;
|
|
FBoundsReportedToChildren:=false;
|
|
FIsOpaque:=false;
|
|
FIsEventRouting:=false;
|
|
SuppressTabDown := true; // by default all Tabs would not be allowed for Cocoa.
|
|
// it should be enabled, i.e. for TMemo with WantTabs=true
|
|
end;
|
|
|
|
destructor TLCLCommonCallback.Destroy;
|
|
begin
|
|
FContext.Free;
|
|
FPropStorage.Free;
|
|
FTarget := nil;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetPropStorage: TStringList;
|
|
begin
|
|
Result := FPropStorage;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetContext: TCocoaContext;
|
|
begin
|
|
Result := FContext;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetTarget: TObject;
|
|
begin
|
|
Result := Target;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetCallbackObject: TObject;
|
|
begin
|
|
Result := Self;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetCaptureControlCallback: ICommonCallBack;
|
|
var
|
|
obj: NSObject;
|
|
lCaptureView: NSView;
|
|
begin
|
|
Result := nil;
|
|
if CocoaWidgetSet.CaptureControl = 0 then Exit;
|
|
obj := NSObject(CocoaWidgetSet.CaptureControl);
|
|
lCaptureView := obj.lclContentView;
|
|
if (obj <> Owner) and (lCaptureView <> Owner) and not FIsEventRouting then
|
|
begin
|
|
Result := lCaptureView.lclGetCallback;
|
|
end;
|
|
end;
|
|
|
|
{ If a window does not display a shortcut menu it should pass
|
|
this message to the DefWindowProc function. If a window is
|
|
a child window, DefWindowProc sends the message to the parent. }
|
|
procedure TLCLCommonCallback.SendContextMenu(Event: NSEvent; out
|
|
ContextMenuHandled: Boolean);
|
|
var
|
|
MsgContext: TLMContextMenu;
|
|
MousePos : NSPoint;
|
|
Res: PtrInt;
|
|
Rcp : NSObject;
|
|
Trg : TObject;
|
|
cb : ICommonCallback;
|
|
obj : TObject;
|
|
cbobj : TLCLCommonCallback;
|
|
ed : NSText;
|
|
begin
|
|
ContextMenuHandled := false;
|
|
FillChar(MsgContext, SizeOf(MsgContext), #0);
|
|
MsgContext.Msg := LM_CONTEXTMENU;
|
|
MsgContext.hWnd := HWND(_handleFrame);
|
|
MousePos := Event.locationInWindow;
|
|
ScreenMousePos(MousePos);
|
|
MsgContext.XPos := Round(MousePos.X);
|
|
MsgContext.YPos := Round(MousePos.Y);
|
|
Rcp := Owner;
|
|
Res := 1;
|
|
repeat
|
|
cb := Rcp.lclGetCallback;
|
|
if Assigned(cb) then
|
|
begin
|
|
Trg := cb.GetTarget;
|
|
Res := LCLMessageGlue.DeliverMessage(Trg, MsgContext);
|
|
if (Res = 0) and (Rcp.isKindOfClass(NSView)) then
|
|
begin
|
|
if Assigned(NSView(Rcp).menuForEvent(Event)) then
|
|
Break; // Cocoa has it's own menu for the control
|
|
|
|
if Rcp.isKindOfClass(NSControl) then
|
|
begin
|
|
ed := NSControl(Rcp).currentEditor;
|
|
if Assigned(ed) and Assigned(ed.menuForEvent(Event)) then
|
|
Break; // Cocoa has it's own menu for the editor of the control
|
|
end;
|
|
end;
|
|
|
|
// not processed, need to find parent
|
|
if Res = 0 then
|
|
begin
|
|
cbobj := nil;
|
|
if Assigned(cb) then
|
|
begin
|
|
obj := cb.GetCallbackObject;
|
|
if obj is TLCLCommonCallback then cbobj := TLCLCommonCallback(obj);
|
|
end;
|
|
if not Assigned(cbobj) then
|
|
Rcp := nil
|
|
else
|
|
Rcp := cbobj.HandleFrame.superView;
|
|
end;
|
|
end else
|
|
Rcp := nil;
|
|
until (Res <> 0) or not Assigned(Rcp);
|
|
ContextMenuHandled := Res <> 0;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvFlagsChanged(Event: NSEvent);
|
|
const
|
|
cModifiersOfInterest: NSUInteger = (NSControlKeyMask or NSShiftKeyMask or NSAlphaShiftKeyMask or NSAlternateKeyMask or NSCommandKeyMask);
|
|
var
|
|
CurMod, Diff: NSUInteger;
|
|
VKKeyCode: word; // VK_ code
|
|
KeyData: PtrInt; // Modifiers (ctrl, alt, mouse buttons...)
|
|
begin
|
|
_SendChar := False;
|
|
CurMod := Event.modifierFlags;
|
|
//see what changed. we only care of bits 16 through 20
|
|
Diff := (TCocoaApplication(NSAPP).PrevKeyModifiers xor CurMod) and cModifiersOfInterest;
|
|
|
|
case Diff of
|
|
0 : VKKeyCode := VK_UNKNOWN; //nothing (that we cared of) changed
|
|
NSControlKeyMask : VKKeyCode := VK_CONTROL; //command mapped to control
|
|
NSShiftKeyMask : VKKeyCode := VK_SHIFT;
|
|
NSAlphaShiftKeyMask: VKKeyCode := VK_CAPITAL; //caps lock
|
|
NSAlternateKeyMask : VKKeyCode := VK_MENU; //option is alt
|
|
NSCommandKeyMask : VKKeyCode := VK_LWIN; //meta... map to left Windows Key?
|
|
end;
|
|
KeyData := CocoaModifiersToKeyState(CurMod);
|
|
|
|
//diff is now equal to the mask of the bit that changed, so we can determine
|
|
//if this change is a keydown (PrevKeyModifiers didn't have the bit set) or
|
|
//a keyup (PrevKeyModifiers had the bit set)
|
|
_IsKeyDown := ((TCocoaApplication(NSAPP).PrevKeyModifiers and Diff) = 0);
|
|
|
|
FillChar(_KeyMsg, SizeOf(_KeyMsg), 0);
|
|
_KeyMsg.KeyData := KeyData;
|
|
_KeyMsg.CharCode := VKKeyCode;
|
|
_IsSysKey := (VKKeyCode = VK_LWIN);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvPrepare(Event: NSEvent);
|
|
var
|
|
KeyCode: word;
|
|
UTF8Character: TUTF8Char; // char to send via IntfUtf8KeyPress
|
|
KeyChar : char; // Ascii char, when possible (xx_(SYS)CHAR)
|
|
SendChar: boolean; // Should we send char?
|
|
VKKeyCode: word; // VK_ code
|
|
IsSysKey: Boolean; // Is alt (option) key down?
|
|
KeyData: PtrInt; // Modifiers (ctrl, alt, mouse buttons...)
|
|
ignModChr: NSString;
|
|
i,c,j : integer;
|
|
begin
|
|
SendChar := False;
|
|
|
|
UTF8Character := '';
|
|
KeyChar := #0;
|
|
|
|
IsSysKey := (Event.modifierFlags and NSCommandKeyMask) <> 0;
|
|
KeyData := (Ord(Event.isARepeat) + 1) or Event.keyCode shl 16;
|
|
if (Event.modifierFlags and NSAlternateKeyMask) <> 0 then
|
|
KeyData := KeyData or MK_ALT; // So that MsgKeyDataToShiftState recognizes Alt key, see bug 30129
|
|
KeyCode := Event.keyCode;
|
|
|
|
ignModChr := Event.charactersIgnoringModifiers;
|
|
if Assigned(ignModChr)
|
|
and (ignModChr.length=1)
|
|
and ((Event.modifierFlags and NSNumericPadKeyMask) = 0) // num pad should be checked by KeyCode
|
|
then
|
|
begin
|
|
VKKeyCode := MacCharToVK(ignModChr.characterAtIndex(0));
|
|
if VKKeyCode = VK_UNKNOWN then
|
|
VKKeyCode := MacCodeToVK(KeyCode); // fallback
|
|
end
|
|
else
|
|
VKKeyCode := MacCodeToVK(KeyCode);
|
|
|
|
if Assigned(CocoaConfigApplication.events.keyEventToVK) then
|
|
VKKeyCode := CocoaConfigApplication.events.keyEventToVK(Event);
|
|
|
|
case VKKeyCode of
|
|
// for sure, these are "non-printable" keys (see http://wiki.lazarus.freepascal.org/LCL_Key_Handling)
|
|
VK_F1..VK_F24, // Function keys (F1-F12)
|
|
VK_PRINT, VK_SCROLL, VK_PAUSE, // Print Screen, Scroll Lock, Pause
|
|
VK_CAPITAL, VK_TAB, // Caps Lock, Tab
|
|
VK_INSERT, VK_DELETE, // Insert, Delete
|
|
VK_HOME, VK_END, // Home, End
|
|
VK_PRIOR,VK_NEXT, // Page Up,Down
|
|
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, // Arrow Keys
|
|
VK_NUMLOCK, // Num Lock
|
|
VK_SLEEP, VK_APPS // power/sleep, context menu
|
|
:
|
|
SendChar := false;
|
|
|
|
// for sure, these are "printable" keys
|
|
VK_ESCAPE,
|
|
VK_BACK,
|
|
VK_RETURN:
|
|
begin
|
|
SendChar := true;
|
|
KeyChar := char(VKKeyCode);
|
|
UTF8Character := KeyChar;
|
|
end;
|
|
else
|
|
//printable keys
|
|
//for these keys, send char or UTF8KeyPress
|
|
UTF8Character := NSStringToString(Event.characters);
|
|
|
|
if Length(UTF8Character) > 0 then
|
|
begin
|
|
SendChar := True;
|
|
if Length(UTF8Character)=1 then
|
|
// ANSI layout character
|
|
KeyChar := Utf8Character[1]
|
|
else
|
|
// it's non ANSI character. KeyChar must be assinged anything but #0
|
|
// otherise the message could be surpressed.
|
|
// In Windows world this would be an "Ansi" char in current locale
|
|
KeyChar := '?';
|
|
end;
|
|
end;
|
|
|
|
FillChar(_KeyMsg, SizeOf(_KeyMsg), 0);
|
|
_KeyMsg.KeyData := KeyData;
|
|
_KeyMsg.CharCode := VKKeyCode;
|
|
_SendChar := SendChar;
|
|
_IsSysKey := IsSysKey;
|
|
_IsKeyDown := (Event.type_ = NSKeyDown);
|
|
|
|
c:=0;
|
|
i:=1;
|
|
j:=0;
|
|
while (i<=length(UTF8Character)) and (j<length(_UTF8Character)) do
|
|
begin
|
|
c := Utf8CodePointLen(@UTF8Character[i], length(UTF8Character)-i+1, false);
|
|
if (j=0) and (c = length(UTF8Character)) then
|
|
begin
|
|
_UTF8Character[0] := UTF8Character;
|
|
j := 1;
|
|
break;
|
|
end
|
|
else if (c > 0) then
|
|
begin
|
|
_UTF8Character[j] := Copy(UTF8Character, i, c);
|
|
inc(i,c);
|
|
inc(j);
|
|
end else
|
|
break;
|
|
end;
|
|
if (j = 0) then _UTF8Character[0] := '';
|
|
_UTF8Charcount := j;
|
|
|
|
FillChar(_CharMsg, SizeOf(_CharMsg), 0);
|
|
_CharMsg.KeyData := _KeyMsg.KeyData;
|
|
_CharMsg.CharCode := ord(KeyChar);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvBeforeDown;
|
|
begin
|
|
// is the key combination help key (Cmd + ?)
|
|
if _SendChar and _IsSysKey and (_UTF8Character[0] = '?') then
|
|
Application.ShowHelpForObject(Target);
|
|
|
|
//Send message to LCL
|
|
if _KeyMsg.CharCode = VK_UNKNOWN then
|
|
exit;
|
|
|
|
// create the CN_KEYDOWN message
|
|
if _IsSysKey then
|
|
_KeyMsg.Msg := CN_SYSKEYDOWN
|
|
else
|
|
_KeyMsg.Msg := CN_KEYDOWN;
|
|
|
|
NotifyApplicationUserInput(Target, _KeyMsg.Msg);
|
|
if (DeliverMessage(_KeyMsg) <> 0) or (_KeyMsg.CharCode = VK_UNKNOWN) then
|
|
// the LCL handled the key
|
|
KeyEvHandled;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvBeforeUp;
|
|
begin
|
|
if _IsSysKey then
|
|
_KeyMsg.Msg := CN_SYSKEYUP
|
|
else
|
|
_KeyMsg.Msg := CN_KEYUP;
|
|
|
|
//Send message to LCL
|
|
if _KeyMsg.CharCode <> VK_UNKNOWN then
|
|
begin
|
|
NotifyApplicationUserInput(Target, _KeyMsg.Msg);
|
|
if (DeliverMessage(_KeyMsg) <> 0) or (_KeyMsg.CharCode = VK_UNKNOWN) then
|
|
begin
|
|
// the LCL has handled the key
|
|
KeyEvHandled;
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.send_UTF8KeyPress();
|
|
var
|
|
i: integer;
|
|
lclHandled: Boolean;
|
|
begin
|
|
if not _sendChar then exit;
|
|
|
|
// send the UTF8 keypress
|
|
i := 0;
|
|
lclHandled := false;
|
|
for i := 0 to _UTF8Charcount -1 do
|
|
begin
|
|
lclHandled := false;
|
|
if Target.IntfUTF8KeyPress(_UTF8Character[i], 1, _IsSysKey) then
|
|
lclHandled := true;
|
|
end;
|
|
|
|
if lclHandled then
|
|
begin
|
|
// the LCL has handled the key
|
|
if ForceReturnKeyDown and (_KeyMsg.CharCode = VK_RETURN) then
|
|
_SendChar := False
|
|
else
|
|
KeyEvHandled;
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.send_CN_CHAR_Message();
|
|
begin
|
|
if not _SendChar then exit;
|
|
|
|
if _IsSysKey then
|
|
_CharMsg.Msg := CN_SYSCHAR
|
|
else
|
|
_CharMsg.Msg := CN_CHAR;
|
|
|
|
if (DeliverMessage(_CharMsg) <> 0) or (_CharMsg.CharCode=VK_UNKNOWN) then
|
|
KeyEvHandled;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.send_LM_KEYDOWN_Message();
|
|
begin
|
|
if _KeyMsg.CharCode = VK_UNKNOWN then exit;
|
|
|
|
if _IsSysKey then
|
|
_KeyMsg.Msg := LM_SYSKEYDOWN
|
|
else
|
|
_KeyMsg.Msg := LM_KEYDOWN;
|
|
|
|
if (DeliverMessage(_KeyMsg) <> 0) or (_KeyMsg.CharCode = VK_UNKNOWN) then
|
|
KeyEvHandled;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.send_LM_CHAR_Message();
|
|
begin
|
|
if not _SendChar then exit;
|
|
|
|
if _IsSysKey then
|
|
_CharMsg.Msg := LM_SYSCHAR
|
|
else
|
|
_CharMsg.Msg := LM_CHAR;
|
|
|
|
if DeliverMessage(_CharMsg) <> 0 then
|
|
KeyEvHandled;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvAfterDown(out AllowCocoaHandle: boolean);
|
|
begin
|
|
AllowCocoaHandle:= false;
|
|
|
|
if _KeyHandled then exit;
|
|
send_UTF8KeyPress;
|
|
|
|
if _KeyHandled then exit;
|
|
send_CN_CHAR_Message;
|
|
|
|
if _KeyHandled then exit;
|
|
send_LM_KEYDOWN_Message;
|
|
|
|
if _KeyHandled then exit;
|
|
send_LM_CHAR_Message;
|
|
|
|
if _KeyHandled then exit;
|
|
AllowCocoaHandle:= not BlockCocoaKeyBeep;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvAfterUp;
|
|
begin
|
|
if _KeyHandled then Exit;
|
|
KeyEvHandled;
|
|
|
|
//Send a LM_(SYS)KEYUP
|
|
if _IsSysKey then
|
|
_KeyMsg.Msg := LM_SYSKEYUP
|
|
else
|
|
_KeyMsg.Msg := LM_KEYUP;
|
|
|
|
if DeliverMessage(_KeyMsg) <> 0 then
|
|
begin
|
|
// the LCL handled the key
|
|
NotifyApplicationUserInput(Target, _KeyMsg.Msg);
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvBefore(Event: NSEvent;
|
|
out AllowCocoaHandle: boolean);
|
|
begin
|
|
_keyHandled := False;
|
|
AllowCocoaHandle := true;
|
|
|
|
if Event.type_ = NSFlagsChanged then
|
|
KeyEvFlagsChanged(Event)
|
|
else
|
|
KeyEvPrepare(Event);
|
|
|
|
if _IsKeyDown then begin
|
|
KeyEvBeforeDown;
|
|
if SuppressTabDown and (_KeyMsg.CharCode = VK_TAB) then
|
|
AllowCocoaHandle := false;
|
|
end else
|
|
KeyEvBeforeUp;
|
|
|
|
if _keyHandled then
|
|
AllowCocoaHandle := false;
|
|
|
|
// flagsChanged always needs to be passed on to Cocoa
|
|
if Event.type_ = NSFlagsChanged then
|
|
AllowCocoaHandle := true;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvAfter;
|
|
var
|
|
AllowCocoaHandle: Boolean;
|
|
begin
|
|
if _IsKeyDown then KeyEvAfterDown(AllowCocoaHandle)
|
|
else KeyEvAfterUp;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.KeyEvHandled;
|
|
begin
|
|
_KeyHandled := True;
|
|
end;
|
|
|
|
function TLCLCommonCallback.IsCocoaOnlyState: Boolean;
|
|
begin
|
|
Result := _isCocoaOnlyState;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SetCocoaOnlyState( state:Boolean );
|
|
begin
|
|
_isCocoaOnlyState := state;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SetTabSuppress(ASuppress: Boolean);
|
|
begin
|
|
SuppressTabDown := ASuppress;
|
|
end;
|
|
|
|
function TLCLCommonCallback.CanFocus: Boolean;
|
|
begin
|
|
Result := not Assigned(Target) or not (csDesigning in Target.ComponentState);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.MouseClick;
|
|
begin
|
|
LCLSendClickedMsg(Target);
|
|
end;
|
|
|
|
function isContextMenuEvent(event: NSEvent): Boolean;
|
|
begin
|
|
Result := Assigned(event)
|
|
and (
|
|
(Event.type_ = NSRightMouseUp)
|
|
or(
|
|
(Event.type_ = NSLeftMouseUp)
|
|
and (event.modifierFlags and NSControlKeyMask <> 0)
|
|
and (event.clickCount = 1)
|
|
)
|
|
);
|
|
end;
|
|
|
|
function TLCLCommonCallback.MouseUpDownEvent(Event: NSEvent; AForceAsMouseUp: Boolean = False; AOverrideBlock: Boolean = False): Boolean;
|
|
const
|
|
MSGKINDUP: array[0..3] of Integer = (LM_LBUTTONUP, LM_RBUTTONUP, LM_MBUTTONUP, LM_XBUTTONUP);
|
|
var
|
|
Msg: TLMMouse;
|
|
MousePos: NSPoint;
|
|
MButton: NSInteger;
|
|
lCaptureControlCallback: ICommonCallback;
|
|
//Str: string;
|
|
lEventType: NSEventType;
|
|
|
|
bndPt, clPt, srchPt: TPoint; // clPt - is the one to send to LCL
|
|
// srchPt - is the one to use for each chidlren (clPt<>srchPt for TScrollBox)
|
|
menuHandled : Boolean;
|
|
mc: Integer; // modal counter
|
|
begin
|
|
if Assigned(Owner) and not NSObjectIsLCLEnabled(Owner) then
|
|
begin
|
|
Result := True; // Cocoa should not handle the message.
|
|
Exit; // LCL should not get the notification either, as the control is disabled.
|
|
end;
|
|
|
|
// If LCL control is provided and it's in designing state.
|
|
// The default resolution: Notify LCL about event, but don't let Cocoa
|
|
// do anything with it. (Result=true)
|
|
Result := Assigned(Target) and (csDesigning in Target.ComponentState);
|
|
|
|
lCaptureControlCallback := GetCaptureControlCallback();
|
|
//Str := (Format('MouseUpDownEvent Target=%s Self=%x CaptureControlCallback=%x', [Target.name, PtrUInt(Self), PtrUInt(lCaptureControlCallback)]));
|
|
if lCaptureControlCallback <> nil then
|
|
begin
|
|
FIsEventRouting:=true;
|
|
Result := lCaptureControlCallback.MouseUpDownEvent(Event, AForceAsMouseUp);
|
|
FIsEventRouting:=false;
|
|
exit;
|
|
end;
|
|
|
|
// The following check prevents the same event to be handled twice
|
|
// Because of the compositive nature of cocoa.
|
|
// For example NSTextField (TEdit) may contains NSTextView and BOTH
|
|
// will signal mouseDown when the field is selected by mouse the first time.
|
|
// In this case only 1 mouseDown should be passed to LCL
|
|
if (lastMouseDownUp = Event.timestamp) then begin
|
|
if not AForceAsMouseUp then Exit; // the same mouse event from a composite child
|
|
if lastMouseWithForce then Exit; // the same forced mouseUp event from a composite child
|
|
end;
|
|
lastMouseDownUp := Event.timestamp;
|
|
lastMouseWithForce := AForceAsMouseUp;
|
|
|
|
|
|
FillChar(Msg, SizeOf(Msg), #0);
|
|
|
|
MousePos := Event.locationInWindow;
|
|
OffsetMousePos(MousePos, bndPt, clPt, srchPt);
|
|
|
|
Msg.Keys := CocoaModifiersToKeyState(Event.modifierFlags) or CocoaPressedMouseButtonsToKeyState(NSEvent.pressedMouseButtons);
|
|
|
|
Msg.XPos := clPt.X;
|
|
Msg.YPos := clPt.Y;
|
|
|
|
MButton := event.buttonNumber;
|
|
if MButton >= 3 then
|
|
begin
|
|
// high word of XButton messages indicate the X button which is pressed
|
|
Msg.Keys := Msg.Keys or (MButton - 2) shl 16;
|
|
MButton := 3;
|
|
end;
|
|
|
|
lEventType := Event.type_;
|
|
if AForceAsMouseUp then
|
|
lEventType := NSLeftMouseUp;
|
|
|
|
if CocoaConfigMouse.controlLeftToRightClick then begin
|
|
// treat ctrl+left button as right button
|
|
if (lEventType = NSLeftMouseDown) and
|
|
(Event.modifierFlags and NSControlKeyMask <> 0) then
|
|
LastMouseLeftButtonAsRight := True;
|
|
if LastMouseLeftButtonAsRight then
|
|
begin
|
|
if MButton = 0 then
|
|
MButton := 1;
|
|
if Msg.Keys and MK_LBUTTON <> 0 then
|
|
Msg.Keys := (Msg.Keys or MK_RBUTTON) and not MK_LBUTTON;
|
|
if lEventType = NSLeftMouseUp then
|
|
LastMouseLeftButtonAsRight := False;
|
|
end;
|
|
end;
|
|
|
|
Result := Result or (BlockCocoaUpDown and not AOverrideBlock);
|
|
mc := CocoaWidgetSet.ModalCounter;
|
|
case lEventType of
|
|
NSLeftMouseDown,
|
|
NSRightMouseDown,
|
|
NSOtherMouseDown:
|
|
begin
|
|
Msg.Msg := CheckMouseButtonDownUp(TLCLHandle(Owner),FTarget,LastMouse,
|
|
FTarget.ClientToScreen(Point(Msg.XPos, Msg.YPos)),MButton+1,True);
|
|
|
|
case LastMouse.ClickCount of
|
|
2: Msg.Keys := msg.Keys or MK_DOUBLECLICK;
|
|
3: Msg.Keys := msg.Keys or MK_TRIPLECLICK;
|
|
4: Msg.Keys := msg.Keys or MK_QUADCLICK;
|
|
end;
|
|
|
|
NotifyApplicationUserInput(Target, Msg.Msg);
|
|
DeliverMessage(Msg);
|
|
|
|
end;
|
|
NSLeftMouseUp,
|
|
NSRightMouseUp,
|
|
NSOtherMouseUp:
|
|
begin
|
|
Msg.Msg := CheckMouseButtonDownUp(TLCLHandle(Owner),FTarget,LastMouse,
|
|
FTarget.ClientToScreen(Point(Msg.XPos, Msg.YPos)),MButton+1,False);
|
|
case LastMouse.ClickCount of
|
|
2: Msg.Keys := msg.Keys or MK_DOUBLECLICK;
|
|
3: Msg.Keys := msg.Keys or MK_TRIPLECLICK;
|
|
4: Msg.Keys := msg.Keys or MK_QUADCLICK;
|
|
end;
|
|
|
|
NotifyApplicationUserInput(Target, Msg.Msg);
|
|
DeliverMessage(Msg);
|
|
|
|
// TODO: Check if Cocoa has special context menu check event
|
|
// it does (menuForEvent:), but it doesn't work all the time
|
|
// http://sound-of-silence.com/?article=20150923
|
|
if (GetTarget is TControl) and isContextMenuEvent(Event) then
|
|
begin
|
|
SendContextMenu(Event, menuHandled);
|
|
if menuHandled then Result := true;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if mc <> CocoaWidgetSet.ModalCounter then
|
|
begin
|
|
// showing of a modal window is causing "mouse" event to be lost.
|
|
// so, preventing Cocoa from handling it
|
|
Result := true;
|
|
Exit;
|
|
end;
|
|
|
|
//debugln('MouseUpDownEvent:'+DbgS(Msg.Msg)+' Target='+Target.name+);
|
|
if not Result then
|
|
//Result := Result or (BlockCocoaUpDown and not AOverrideBlock);
|
|
case lEventType of
|
|
NSLeftMouseDown,
|
|
NSRightMouseDown,
|
|
NSOtherMouseDown:
|
|
TrackedControl := Owner;
|
|
NSLeftMouseUp,
|
|
NSRightMouseUp,
|
|
NSOtherMouseUp:
|
|
begin
|
|
if TrackedControl = Owner then TrackedControl := nil;
|
|
if lEventType = NSLeftMouseUp then
|
|
BlockCocoaMouseMove := false;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function isValidMouseControl( control:TWinControl ): Boolean;
|
|
begin
|
|
if (control is TCustomForm) and (csDesigning in control.ComponentState) then
|
|
exit( True );
|
|
Result:= control.Visible and control.Enabled;
|
|
end;
|
|
|
|
function TLCLCommonCallback.MouseMove(Event: NSEvent): Boolean;
|
|
var
|
|
Msg: TLMMouseMove;
|
|
MousePos: NSPoint;
|
|
i: integer;
|
|
rect: TRect;
|
|
//mp: TPoint;
|
|
obj: NSObject;
|
|
callback: ICommonCallback;
|
|
targetControl: TWinControl;
|
|
childControl:TWinControl;
|
|
bndPt, clPt: TPoint;
|
|
MouseTargetLookup: Boolean;
|
|
srchPt: TPoint;
|
|
begin
|
|
Result:= true;
|
|
|
|
if not NSApp.isActive then
|
|
exit;
|
|
|
|
if Assigned(Owner) and not NSObjectIsLCLEnabled(Owner) then
|
|
exit; // LCL should get the notification either.
|
|
|
|
// If LCL control is provided and it's in designing state.
|
|
// The default resolution: Notify LCL about event, but don't let Cocoa
|
|
// do anything with it. (Result=true)
|
|
Result := Assigned(Target) and (csDesigning in Target.ComponentState);
|
|
|
|
MousePos := Event.locationInWindow;
|
|
OffsetMousePos(MousePos, bndPt, clPt, srchPt);
|
|
|
|
// For "dragged" events, the same "Target" should be used
|
|
MouseTargetLookup := Event.type_ = NSMouseMoved;
|
|
|
|
if MouseTargetLookup then
|
|
begin
|
|
rect:=Owner.lclClientFrame;
|
|
targetControl:=nil;
|
|
|
|
callback := GetCaptureControlCallback();
|
|
if callback <> nil then
|
|
begin
|
|
FIsEventRouting:=true;
|
|
Result := callback.MouseMove(Event);
|
|
FIsEventRouting:=false;
|
|
exit;
|
|
end
|
|
else
|
|
begin
|
|
if Event.window<>GetCocoaWindowAtPos(GetScreenPointFromEvent(Event)) then
|
|
exit;
|
|
|
|
rect:=Target.BoundsRect;
|
|
OffsetRect(rect, -rect.Left, -rect.Top);
|
|
if (event.type_ = NSMouseMoved) and (not Types.PtInRect(rect, bndPt)) then
|
|
begin
|
|
// do not send negative coordinates (unless dragging mouse)
|
|
Exit;
|
|
end;
|
|
|
|
if assigned(Target.Parent) and not Types.PtInRect(rect, bndPt) then
|
|
targetControl:=Target.Parent // outside myself then route to parent
|
|
else
|
|
for i:=Target.ControlCount-1 downto 0 do // otherwise check, if over child and route to child
|
|
if Target.Controls[i] is TWinControl then
|
|
begin
|
|
childControl:=TWinControl(Target.Controls[i]);
|
|
rect:=childControl.BoundsRect;
|
|
if Types.PtInRect(rect, srchPt) and isValidMouseControl(childControl) then
|
|
begin
|
|
targetControl:=childControl;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if assigned(targetControl) and not FIsEventRouting then
|
|
begin
|
|
if not targetControl.HandleAllocated then Exit; // Fixes crash due to events being sent after ReleaseHandle
|
|
FIsEventRouting:=true;
|
|
//debugln(Target.name+' -> '+targetControl.Name+'- is parent:'+dbgs(targetControl=Target.Parent)+' Point: '+dbgs(br)+' Rect'+dbgs(rect));
|
|
obj := NSObject(targetControl.Handle).lclContentView;
|
|
if obj = nil then Exit;
|
|
callback := obj.lclGetCallback;
|
|
if callback = nil then Exit; // Avoids crashes
|
|
result := callback.MouseMove(Event);
|
|
FIsEventRouting := false;
|
|
exit;
|
|
end;
|
|
|
|
if (Event.type_ = NSMouseMoved) and Owner.lclIsMouseInAuxArea(Event) then
|
|
begin
|
|
// mouse is over auxillary area that's "blind" to mouse moves
|
|
// even though the mouse cursos is within the control bounds.
|
|
// (i.e. scrollbars)
|
|
CursorHelper.ForceSetDefaultCursor;
|
|
Result := false;
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
// debugln('Send to: '+Target.name+' Point: '+dbgs(mp));
|
|
|
|
FillChar(Msg, SizeOf(Msg), #0);
|
|
Msg.Msg := LM_MOUSEMOVE;
|
|
Msg.Keys := CocoaModifiersToKeyState(Event.modifierFlags) or CocoaPressedMouseButtonsToKeyState(NSEvent.pressedMouseButtons);
|
|
Msg.XPos := clPt.X;
|
|
Msg.YPos := clPt.Y;
|
|
|
|
//debugln('MouseMove x='+dbgs(MousePos.X)+' y='+dbgs(MousePos.Y)+' Target='+Target.Name);
|
|
|
|
NotifyApplicationUserInput(Target, Msg.Msg);
|
|
// LCL/LM_MOUSEMOVE always return false, so we should discard return value
|
|
DeliverMessage(Msg);
|
|
// 1. for MouseMove Event, it has been processed by LCL,
|
|
// and does not need Cocoa to continue processing.
|
|
// 2. for MouseDragged Event, it needs Cocoa to continue processing
|
|
// (when not Dragging yet)
|
|
if Event.type_ = NSMouseMoved then
|
|
Result:= True
|
|
else
|
|
Result:= Target.Dragging;
|
|
|
|
// if Screen.Cursor set, LCL won't call TCocoaWSWinControl.SetCursor().
|
|
// we need to set the cursor ourselves
|
|
CursorHelper.SetScreenCursorWhenNotDefault;
|
|
end;
|
|
|
|
function TLCLCommonCallback.scrollWheel(Event: NSEvent): Boolean;
|
|
var
|
|
Msg: TLMMouseEvent;
|
|
MousePos: NSPoint;
|
|
MButton: NSInteger;
|
|
bndPt, clPt, srchPt: TPoint;
|
|
scrollDelta: Single;
|
|
wheelDelta: Integer;
|
|
begin
|
|
Result := False; // allow cocoa to handle message
|
|
|
|
if Assigned(Target)
|
|
and not (csDesigning in Target.ComponentState)
|
|
and not NSObjectIsLCLEnabled(Owner) then
|
|
Exit;
|
|
|
|
MousePos := Event.locationInWindow;
|
|
OffsetMousePos(MousePos, bndPt, clPt, srchPt);
|
|
|
|
MButton := event.buttonNumber;
|
|
if MButton >= 3 then
|
|
MButton := 3;
|
|
|
|
FillChar(Msg, SizeOf(Msg), #0);
|
|
|
|
Msg.Button := MButton;
|
|
Msg.X := round(clPt.X);
|
|
Msg.Y := round(clPt.Y);
|
|
Msg.State := CocoaModifiersToShiftState(Event.modifierFlags, NSEvent.pressedMouseButtons);
|
|
|
|
// Some info on event.deltaY can be found here:
|
|
// https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/
|
|
// It says that deltaY=1 means 1 line, and in the LCL 1 line is 120
|
|
if (event.scrollingDeltaY <> 0) and
|
|
((event.scrollingDeltaX = 0) or not FLastWheelWasHorz) then
|
|
begin
|
|
Msg.Msg := LM_MOUSEWHEEL;
|
|
if event.hasPreciseScrollingDeltas then
|
|
scrollDelta := event.scrollingDeltaY / 15 // Average line height
|
|
else
|
|
scrollDelta := event.scrollingDeltaY;
|
|
wheelDelta := round(scrollDelta * 120);
|
|
end
|
|
else
|
|
if event.scrollingDeltaX <> 0 then
|
|
begin
|
|
Msg.Msg := LM_MOUSEHWHEEL;
|
|
// see "deltaX" documentation.
|
|
// on macOS: -1 = right, +1 = left
|
|
// on LCL: -1 = left, +1 = right
|
|
wheelDelta := round(-event.scrollingDeltaX * 120);
|
|
end
|
|
else
|
|
begin
|
|
// Filter out empty events - See bug 28491
|
|
Result := true;
|
|
Exit;
|
|
end;
|
|
|
|
// Filter scrolls that affect both X and Y towards whatever the last scroll was
|
|
FLastWheelWasHorz := (Msg.Msg = LM_MOUSEHWHEEL);
|
|
|
|
// Avoid overflow/underflow in message
|
|
if wheelDelta > High(SmallInt) then
|
|
wheelDelta := High(SmallInt)
|
|
else if wheelDelta < Low(SmallInt) then
|
|
wheelDelta := Low(SmallInt);
|
|
Msg.WheelDelta := wheelDelta;
|
|
|
|
NotifyApplicationUserInput(Target, Msg.Msg);
|
|
Result := DeliverMessage(Msg) <> 0;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.frameDidChange(sender: id);
|
|
begin
|
|
boundsDidChange(sender);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.boundsDidChange(sender: id);
|
|
var
|
|
NewBounds, OldBounds: TRect;
|
|
PosMsg: TLMWindowPosChanged;
|
|
Resized, Moved, ClientResized: Boolean;
|
|
SizeType: Integer;
|
|
begin
|
|
NewBounds := HandleFrame.lclFrame;
|
|
|
|
//debugln('Newbounds='+ dbgs(newbounds));
|
|
// send window pos changed
|
|
PosMsg.Msg := LM_WINDOWPOSCHANGED;
|
|
PosMsg.Result := 0;
|
|
New(PosMsg.WindowPos);
|
|
try
|
|
with PosMsg.WindowPos^ do
|
|
begin
|
|
hWndInsertAfter := 0;
|
|
x := NewBounds.Left;
|
|
y := NewBounds.Right;
|
|
cx := NewBounds.Right - NewBounds.Left;
|
|
cy := NewBounds.Bottom - NewBounds.Top;
|
|
flags := 0;
|
|
end;
|
|
LCLMessageGlue.DeliverMessage(Target, PosMsg);
|
|
finally
|
|
Dispose(PosMsg.WindowPos);
|
|
end;
|
|
|
|
OldBounds := Target.BoundsRect;
|
|
//debugln('OldBounds Target='+Target.Name+':'+ dbgs(OldBounds));
|
|
|
|
Resized :=
|
|
(OldBounds.Right - OldBounds.Left <> NewBounds.Right - NewBounds.Left) or
|
|
(OldBounds.Bottom - OldBounds.Top <> NewBounds.Bottom - NewBounds.Top);
|
|
|
|
Moved :=
|
|
(OldBounds.Left <> NewBounds.Left) or
|
|
(OldBounds.Top <> NewBounds.Top);
|
|
|
|
ClientResized := (sender <> HandleFrame)
|
|
and not EqualRect(Target.ClientRect, HandleFrame.lclClientFrame);
|
|
|
|
// update client rect
|
|
if ClientResized or Resized or Target.ClientRectNeedsInterfaceUpdate then
|
|
begin
|
|
Target.InvalidateClientRectCache(false);
|
|
ClientResized := True;
|
|
end;
|
|
|
|
// then send a LM_SIZE message
|
|
if Resized or ClientResized then
|
|
begin
|
|
LCLSendSizeMsg(Target, Max(NewBounds.Right - NewBounds.Left,0),
|
|
Max(NewBounds.Bottom - NewBounds.Top,0), Owner.lclWindowState, True);
|
|
end;
|
|
|
|
// then send a LM_MOVE message
|
|
if Moved then
|
|
begin
|
|
LCLSendMoveMsg(Target, NewBounds.Left,
|
|
NewBounds.Top, Move_SourceIsInterface);
|
|
end;
|
|
|
|
if not FBoundsReportedToChildren then // first time we need this to update non cocoa based client rects
|
|
begin
|
|
Target.InvalidateClientRectCache(true);
|
|
FBoundsReportedToChildren:=true;
|
|
end;
|
|
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.BecomeFirstResponder;
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
// LCL is unable to determine the "already focused" message
|
|
// thus Cocoa related code is doing that.
|
|
//if not Target.Focused then
|
|
LCLSendSetFocusMsg(Target);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.ResignFirstResponder;
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
CocoaWidgetSet.KillingFocus:= true;
|
|
try
|
|
LCLSendKillFocusMsg(Target);
|
|
finally
|
|
CocoaWidgetSet.KillingFocus:= false;
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.DidBecomeKeyNotification;
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
LCLSendActivateMsg(Target, WA_ACTIVE, false);
|
|
LCLSendSetFocusMsg(Target);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.DidResignKeyNotification;
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
LCLSendActivateMsg(Target, WA_INACTIVE, false);
|
|
LCLSendKillFocusMsg(Target);
|
|
end;
|
|
|
|
function TLCLCommonCallback.SendOnEditCut: Boolean;
|
|
begin
|
|
Result:= false;
|
|
if Assigned(Target) then
|
|
Result:= SendSimpleMessage(Target, LM_CUT)=0;
|
|
end;
|
|
|
|
function TLCLCommonCallback.SendOnEditPaste: Boolean;
|
|
begin
|
|
Result:= false;
|
|
if Assigned(Target) then
|
|
Result:= SendSimpleMessage(Target, LM_PASTE)=0;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SendOnChange;
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
SendSimpleMessage(Target, LM_CHANGED);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SendOnTextChanged;
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
SendSimpleMessage(Target, CM_TEXTCHANGED);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.scroll(isVert: Boolean; Pos: Integer;
|
|
AScrollPart: NSScrollerPart);
|
|
var
|
|
LMScroll: TLMScroll;
|
|
b: Boolean;
|
|
lclCode: Integer;
|
|
begin
|
|
FillChar(LMScroll{%H-}, SizeOf(LMScroll), #0);
|
|
//todo: this should be a part of a parameter
|
|
//LMScroll.ScrollBar := Target.Handle;
|
|
|
|
if IsVert then
|
|
LMScroll.Msg := LM_VSCROLL
|
|
else
|
|
LMScroll.Msg := LM_HSCROLL;
|
|
|
|
LMScroll.Pos := Pos;
|
|
case AScrollPart of
|
|
NSScrollerDecrementPage: lclCode := SB_PAGELEFT;
|
|
NSScrollerIncrementPage: lclCode := SB_PAGERIGHT;
|
|
NSScrollerDecrementLine: lclCode := SB_LINELEFT;
|
|
NSScrollerIncrementLine: lclCode := SB_LINERIGHT;
|
|
else
|
|
lclCode := SB_THUMBPOSITION;
|
|
end;
|
|
LMScroll.ScrollCode := lclCode; //SIF_POS;
|
|
|
|
LCLMessageGlue.DeliverMessage(Target, LMScroll);
|
|
end;
|
|
|
|
function TLCLCommonCallback.DeliverMessage(var Msg): LRESULT;
|
|
begin
|
|
if Assigned(Target) then
|
|
Result := LCLMessageGlue.DeliverMessage(Target, Msg)
|
|
else
|
|
Result := 0;
|
|
end;
|
|
|
|
function TLCLCommonCallback.DeliverMessage(Msg: Cardinal; WParam: WParam; LParam: LParam): LResult;
|
|
var
|
|
Message: TLMessage;
|
|
begin
|
|
Message.Msg := Msg;
|
|
Message.WParam := WParam;
|
|
Message.LParam := LParam;
|
|
Message.Result := 0;
|
|
Result := DeliverMessage(Message);
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.Draw(ControlContext: NSGraphicsContext;
|
|
const bounds, dirty: NSRect);
|
|
var
|
|
PS: TPaintStruct;
|
|
nsr:NSRect;
|
|
begin
|
|
// todo: think more about draw call while previous draw still active
|
|
if Assigned(FContext) then
|
|
Exit;
|
|
FContext := TCocoaContext.Create(ControlContext);
|
|
FContext.Control := FTarget;
|
|
FContext.isControlDC := True;
|
|
try
|
|
// debugln('Draw '+Target.name+' bounds='+Dbgs(NSRectToRect(bounds))+' dirty='+Dbgs(NSRectToRect(dirty)));
|
|
if FContext.InitDraw(Round(bounds.size.width), Round(bounds.size.height)) then
|
|
begin
|
|
nsr:=dirty;
|
|
if NOT Owner.isKindOfClass(NSView) or NOT NSView(Owner).isFlipped then
|
|
nsr.origin.y:=bounds.size.height-dirty.origin.y-dirty.size.height;
|
|
|
|
if FIsOpaque and (Target.Color<>clDefault) then
|
|
begin
|
|
FContext.BkMode:=OPAQUE;
|
|
FContext.BkColor:=Target.Color;
|
|
FContext.BackgroundFill(nsr);
|
|
//debugln('Background '+Target.name+Dbgs(NSRectToRect(dirty)));
|
|
end;
|
|
|
|
FillChar(PS, SizeOf(TPaintStruct), 0);
|
|
PS.hdc := HDC(FContext);
|
|
PS.rcPaint := NSRectToRect(nsr);
|
|
LCLSendPaintMsg(Target, HDC(FContext), @PS);
|
|
if FHasCaret then
|
|
DrawCaret;
|
|
end;
|
|
finally
|
|
FreeAndNil(FContext);
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.DrawBackground(ctx: NSGraphicsContext; const bounds, dirtyRect: NSRect);
|
|
var
|
|
lTarget: TWinControl;
|
|
begin
|
|
// Implement Color property
|
|
lTarget := TWinControl(GetTarget());
|
|
if (lTarget.Color <> clDefault) and (lTarget.Color <> clBtnFace) and (lTarget.Color <> clNone) then
|
|
begin
|
|
ColorToNSColor(ColorToRGB(lTarget.Color)).set_();
|
|
// NSRectFill() always requires the coordinate system of the lower left corner
|
|
// of the origin, contrary to the Rect provided to LCL in
|
|
// TLCLCommonCallback.Draw() and TLCLCommonCallback.DrawOverlay()
|
|
NSRectFill(dirtyRect);
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.DrawOverlay(ControlContext: NSGraphicsContext;
|
|
const bounds, dirty: NSRect);
|
|
var
|
|
PS : TPaintStruct;
|
|
nsr : NSRect;
|
|
begin
|
|
// todo: think more about draw call while previous draw still active
|
|
if Assigned(FContext) then
|
|
Exit;
|
|
FContext := TCocoaContext.Create(ControlContext);
|
|
FContext.isControlDC := True;
|
|
FContext.isDesignDC := True;
|
|
try
|
|
// debugln('Draw '+Target.name+' bounds='+Dbgs(NSRectToRect(bounds))+' dirty='+Dbgs(NSRectToRect(dirty)));
|
|
if FContext.InitDraw(Round(bounds.size.width), Round(bounds.size.height)) then
|
|
begin
|
|
nsr:=dirty;
|
|
if NOT Owner.isKindOfClass(NSView) or NOT NSView(Owner).isFlipped then
|
|
nsr.origin.y:=bounds.size.height-dirty.origin.y-dirty.size.height;
|
|
|
|
FillChar(PS, SizeOf(TPaintStruct), 0);
|
|
PS.hdc := HDC(FContext);
|
|
PS.rcPaint := NSRectToRect(nsr);
|
|
LCLSendPaintMsg(Target, HDC(FContext), @PS);
|
|
end;
|
|
finally
|
|
FreeAndNil(FContext);
|
|
end;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.RemoveTarget;
|
|
begin
|
|
FTarget := nil;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.InputClientInsertText(const utf8: string);
|
|
var
|
|
i : integer;
|
|
c : integer;
|
|
ch : TUTF8Char;
|
|
begin
|
|
if (utf8 = '') then Exit;
|
|
i:=1;
|
|
while (i<=length(utf8)) do
|
|
begin
|
|
c := Utf8CodePointLen(@utf8[i], length(utf8)-i+1, false);
|
|
ch := Copy(utf8, i, c);
|
|
FTarget.IntfUTF8KeyPress(ch, 1, false);
|
|
inc(i, c);
|
|
end;
|
|
|
|
end;
|
|
|
|
function TLCLCommonCallback.HandleFrame: NSView;
|
|
begin
|
|
Result:= _handleFrame;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SetHandleFrame(AHandleFrame: NSView);
|
|
begin
|
|
_handleFrame:= AHandleFrame;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetIsOpaque: Boolean;
|
|
begin
|
|
Result:= FIsOpaque;
|
|
end;
|
|
|
|
procedure TLCLCommonCallback.SetIsOpaque(AValue: Boolean);
|
|
begin
|
|
FIsOpaque:=AValue;
|
|
end;
|
|
|
|
function TLCLCommonCallback.GetShouldBeEnabled: Boolean;
|
|
begin
|
|
Result := Assigned(FTarget) and FTarget.Enabled;
|
|
end;
|
|
|
|
{ TLCLFullControlEditCallBack }
|
|
|
|
{
|
|
Key Step for IME (such as Chinese/Japanese/Korean and DeadKeys)
|
|
1. set _sendChar:=false to avoid KeyDown Event being eaten
|
|
in IntfUTF8KeyPress() or CN_CHAR message.
|
|
2. KeyDown Event will be handled in TCocoaFullControlEdit.keyDown(),
|
|
and NSInputContext.sendEvent() will be called in it,
|
|
and function in NSTextInputClient will be called.
|
|
}
|
|
procedure TLCLFullControlEditCallback.KeyEvPrepare(Event: NSEvent);
|
|
begin
|
|
inherited;
|
|
_sendChar := false;
|
|
end;
|
|
|
|
{ TCocoaWSControl }
|
|
|
|
class function TCocoaWSControl.GetCanvasScaleFactor(const AControl: TControl
|
|
): Double;
|
|
begin
|
|
if Assigned(AControl.Parent) then
|
|
Result := AControl.Parent.GetCanvasScaleFactor
|
|
else
|
|
Result := 1;
|
|
end;
|
|
|
|
{ TCocoaWSWinControl }
|
|
|
|
class function TCocoaWSWinControl.CreateHandle(const AWinControl: TWinControl;
|
|
const AParams: TCreateParams): TLCLHandle;
|
|
begin
|
|
Result := TCocoaWSCustomControl.CreateHandle(AWinControl, AParams);
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.DestroyHandle(const AWinControl: TWinControl);
|
|
var
|
|
obj: NSObject;
|
|
Callback: ICommonCallback;
|
|
CallbackObject: TObject;
|
|
begin
|
|
if not AWinControl.HandleAllocated then
|
|
Exit;
|
|
|
|
if Assigned(CocoaWidgetSet) and (AWinControl.Handle = CocoaWidgetSet.GetCapture) then
|
|
CocoaWidgetSet.ReleaseCapture;
|
|
|
|
obj := NSObject(AWinControl.Handle);
|
|
Callback := obj.lclGetCallback;
|
|
|
|
if AWinControl.Focused and Assigned(Callback) then
|
|
Callback.ResignFirstResponder; // dont' call LCLSendKillFocusMsg
|
|
LCLSendDestroyMsg( AWinControl );
|
|
|
|
if obj.isKindOfClass_(NSView) then
|
|
begin
|
|
// no need to "retain" prior to "removeFromSuperview"
|
|
// the original referecnce count with "alloc" is not being released
|
|
// after "addToSuperview"
|
|
NSView(obj).removeFromSuperview;
|
|
end
|
|
else
|
|
if obj.isKindOfClass_(NSWindow) then
|
|
NSWindow(obj).close;
|
|
|
|
// destroy the callback
|
|
if Assigned(Callback) then
|
|
begin
|
|
if Callback.HasCaret then DestroyCaret(nil);
|
|
CallbackObject := Callback.GetCallbackObject;
|
|
Callback.RemoveTarget;
|
|
Callback := nil;
|
|
obj.lclClearCallback;
|
|
// Do not free the callback object here. It might be processing an event
|
|
// and is performing a self destruction. Thus there might be a code performing
|
|
// even after DestroyHandle() was called. The destruction needs to be delayed
|
|
// until after the event processing is done
|
|
CocoaWidgetSet.AddToCollect(CallbackObject);
|
|
end;
|
|
obj.release;
|
|
end;
|
|
|
|
class function TCocoaWSWinControl.GetCanvasScaleFactor(const AControl: TControl
|
|
): Double;
|
|
var
|
|
obj: NSObject;
|
|
win: NSWindow;
|
|
begin
|
|
win := nil;
|
|
Result := 1;
|
|
|
|
if TWinControl(AControl).HandleAllocated then
|
|
begin
|
|
obj := NSObject(TWinControl(AControl).Handle);
|
|
if obj.isKindOfClass_(NSView) then
|
|
win := NSView(obj).window
|
|
else if obj.isKindOfClass_(NSWindow) then
|
|
win := NSWindow(obj);
|
|
end;
|
|
|
|
if Assigned(win) then
|
|
begin
|
|
if win.respondsToSelector( ObjCSelector('backingScaleFactor')) then
|
|
Result := win.backingScaleFactor
|
|
else if win.respondsToSelector( ObjCSelector('userSpaceScaleFactor')) then // for older OSX
|
|
Result := win.userSpaceScaleFactor;
|
|
end;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.SetText(const AWinControl: TWinControl; const AText: String);
|
|
var
|
|
obj: NSObject;
|
|
begin
|
|
if not AWinControl.HandleAllocated then
|
|
Exit;
|
|
obj := NSObject(AWinControl.Handle);
|
|
if obj.isKindOfClass_(NSControl) then
|
|
SetNSControlValue(NSControl(obj), AText);
|
|
end;
|
|
|
|
class function TCocoaWSWinControl.GetText(const AWinControl: TWinControl; var AText: String): Boolean;
|
|
var
|
|
obj: NSObject;
|
|
begin
|
|
Result := AWinControl.HandleAllocated;
|
|
if not Result then
|
|
Exit;
|
|
obj := NSObject(AWinControl.Handle);
|
|
Result := obj.isKindOfClass_(NSControl);
|
|
if Result then
|
|
AText := GetNSControlValue(NSControl(obj));
|
|
end;
|
|
|
|
class function TCocoaWSWinControl.GetTextLen(const AWinControl: TWinControl; var ALength: Integer): Boolean;
|
|
var
|
|
obj: NSObject;
|
|
s: NSString;
|
|
begin
|
|
Result := AWinControl.HandleAllocated;
|
|
if not Result then
|
|
Exit;
|
|
|
|
obj := NSObject(AWinControl.Handle);
|
|
Result := obj.isKindOfClass_(NSControl);
|
|
if not Result then Exit;
|
|
|
|
s := NSControl(obj).stringValue;
|
|
if Assigned(s) then
|
|
ALength := s.length
|
|
else
|
|
ALength := 0
|
|
end;
|
|
|
|
class function TCocoaWSWinControl.GetClientBounds(const AWincontrol: TWinControl; var ARect: TRect): Boolean;
|
|
begin
|
|
Result := AWinControl.HandleAllocated;
|
|
if Result then
|
|
ARect := NSObject(AWinControl.Handle).lclClientFrame;
|
|
end;
|
|
|
|
class function TCocoaWSWinControl.GetClientRect(const AWincontrol: TWinControl; var ARect: TRect): Boolean;
|
|
begin
|
|
Result:=(AWinControl.Handle<>0);
|
|
if not Result then Exit;
|
|
ARect:=NSObject(AWinControl.Handle).lclClientFrame;
|
|
if (ARect.Left<>0) or (ARect.Top<>0) then
|
|
OffsetRect(ARect, -ARect.Left, -ARect.Top);
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.GetPreferredSize(
|
|
const AWinControl: TWinControl; var PreferredWidth, PreferredHeight: integer;
|
|
WithThemeSpace: Boolean);
|
|
var
|
|
lView: NSView;
|
|
Size: NSSize;
|
|
r: TRect;
|
|
begin
|
|
if not AWinControl.HandleAllocated then Exit;
|
|
|
|
lView := NSObject(AWinControl.Handle).lclContentView;
|
|
if lView = nil then Exit;
|
|
|
|
//todo: using fittingSize is wrong - it's based on constraints of the control solely.
|
|
//CocoaWidgetset is not using these constrains. As a result, CocoaComboBox
|
|
//produces wrong size: width 3 and height 26 (or OSX 10.9)
|
|
//as well as SpinEdit itself. The better approach is to use intrinsicContentSize method.
|
|
// Felipe: intrinsicContentSize doesn't give any better results in my tests, it results in even smaller controls
|
|
if lView.respondsToSelector(objcselector('fittingSize')) then // fittingSize is 10.7+
|
|
begin
|
|
Size := lView.fittingSize();
|
|
r := lview.lclGetFrameToLayoutDelta;
|
|
PreferredWidth := Round(Size.width) - r.Left + r.Right;
|
|
PreferredHeight := Round(Size.height) - r.Top + r.Bottom;
|
|
end;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.SetBounds(const AWinControl: TWinControl;
|
|
const ALeft, ATop, AWidth, AHeight: Integer);
|
|
var
|
|
cb : ICommonCallBack;
|
|
r : TRect;
|
|
begin
|
|
if AWinControl.HandleAllocated then
|
|
begin
|
|
{$IFDEF COCOA_DEBUG_SETBOUNDS}
|
|
writeln(Format('TCocoaWSWinControl.SetBounds: %s Bounds=%s',
|
|
[AWinControl.Name, dbgs(Bounds(ALeft, ATop, AWidth, AHeight))]));
|
|
{$ENDIF}
|
|
NSObject(AWinControl.Handle).lclSetFrame(Bounds(ALeft, ATop, AWidth, AHeight));
|
|
end;
|
|
end;
|
|
|
|
function getWinControlAtMouse(): TControl;
|
|
begin
|
|
Result:= Application.GetControlAtMouse;
|
|
if not Assigned(Result) then
|
|
exit;
|
|
|
|
// find TWinControl (not TGraphicControl)
|
|
// see also TControl.SetTempCursor()
|
|
while not (Result is TWinControl) do
|
|
begin
|
|
Result:= Result.Parent;
|
|
if not Assigned(Result) then
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.SetCursor(const AWinControl: TWinControl;
|
|
const ACursor: HCursor);
|
|
var
|
|
control: TControl;
|
|
topParent: TControl;
|
|
begin
|
|
//debugln('SetCursor '+AWinControl.name+' '+dbgs(ACursor));
|
|
|
|
// screen cursor has higher priority than control cursor.
|
|
if Screen.Cursor<>crDefault
|
|
then exit;
|
|
|
|
// control cursor only should be set when mouse in the keyWindow or hintWindow.
|
|
// for the MacOS system automatic restore cursor feature(also an issue),
|
|
// it is more appropriate to set the default cursor when the mouse is out
|
|
// of the keyWindow, which has been set in TLCLCommonCallback.MouseMove().
|
|
// the keyWindow here refers to the Client Frame, excluding TitleBar.
|
|
// see also: https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/40515
|
|
topParent:= AWinControl.GetTopParent;
|
|
if (topParent is TCustomForm) and not (topParent is THintWindow) then
|
|
begin
|
|
if NSView(TCustomForm(topParent).handle).window <> NSApp.keyWindow then
|
|
exit;
|
|
end;
|
|
|
|
// control cursor only need be set when mouse in AWinControl.
|
|
// suppose there is a Button, which is to set a Cursor of a ListBox.
|
|
// without the code here, it will be set to the Cursor of the ListBox
|
|
// after clicking the Button.
|
|
if not ((AWinControl is TCustomForm) and (csDesigning in AWinControl.ComponentState)) then
|
|
begin
|
|
control:= getWinControlAtMouse;
|
|
if control<>AWinControl then
|
|
exit;
|
|
end;
|
|
|
|
CursorHelper.SetNewCursor( TCocoaCursor(ACursor) );
|
|
end;
|
|
|
|
type
|
|
NSFontSetter = objccategory external(NSObject)
|
|
procedure setFont(afont: NSFont); message 'setFont:';
|
|
procedure setTextColor(clr: NSColor); message 'setTextColor:';
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.SetFont(const AWinControl: TWinControl; const AFont: TFont);
|
|
var
|
|
Obj: NSObject;
|
|
Cell: NSCell;
|
|
Str: NSAttributedString;
|
|
NewStr: NSMutableAttributedString;
|
|
Range: NSRange;
|
|
begin
|
|
if not (AWinControl.HandleAllocated) then Exit;
|
|
|
|
Obj := NSObject(AWinControl.Handle).lclContentView;
|
|
|
|
if Obj.respondsToSelector(ObjCSelector('setFont:')) then
|
|
Obj.setFont(TCocoaFont(AFont.Reference.Handle).Font);
|
|
|
|
if Obj.respondsToSelector(ObjCSelector('setTextColor:')) then
|
|
begin
|
|
if AFont.Color = clDefault then
|
|
Obj.setTextColor(NSColor.controlTextColor)
|
|
else
|
|
Obj.setTextColor(ColorToNSColor(ColorToRGB(AFont.Color)));
|
|
end;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.SetColor(const AWinControl: TWinControl);
|
|
begin
|
|
invalidate(AWinControl);
|
|
end;
|
|
|
|
function indexInList(ctrl: id; l: TFPList): integer;
|
|
var
|
|
i : integer;
|
|
begin
|
|
for i:=0 to l.Count-1 do
|
|
if PtrUInt(TWinControl(l[i]).Handle)=PtrUInt(ctrl) then
|
|
begin
|
|
Result:=i;
|
|
exit;
|
|
end;
|
|
Result:=-1;
|
|
end;
|
|
|
|
function SortHandles(param1: id; param2: id; param3: Pointer): NSComparisonResult; cdecl;
|
|
var
|
|
i1,i2: integer;
|
|
begin
|
|
i1:=indexInList(param1, TFPList(param3));
|
|
i2:=indexInList(param2, TFPList(param3));
|
|
if i1<i2 then Result:=NSOrderedDescending
|
|
else if i1>i2 then Result:=NSOrderedAscending
|
|
else Result:=NSOrderedSame;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.SetChildZPosition(const AWinControl,
|
|
AChild: TWinControl; const AOldPos, ANewPos: Integer; const AChildren: TFPList
|
|
);
|
|
var
|
|
pr : NSView;
|
|
begin
|
|
if (not AWinControl.HandleAllocated) or (not AChild.HandleAllocated) then Exit;
|
|
|
|
pr := NSView(AWinControl.Handle).lclContentView;
|
|
|
|
if pr.subviews.count <= 1 then exit;
|
|
|
|
// 1. sorting is a better option than removing / adding a view.
|
|
// whenever a focused (firstrepsonder view) is removed and added to front,
|
|
// focus is lost. the issue was exposed by
|
|
// https://gitlab.com/freepascal.org/lazarus/lazarus/-/commit/853461fed65fed2ce1614bcc06eca5b880810f0e
|
|
// <LCL: fixed TWinControl.SetChildZPosition, WS must be informed about change of order in any case. issue #40450>
|
|
// 2. regarding performance, though on every comparison an index must be
|
|
// searched withing a list, but performance still does not need to be
|
|
// paid too much attention to.
|
|
// 3. it's because SetChildZPosition() rarely needs to be called at runtime,
|
|
// and usually there are only a few sibling controls.
|
|
pr.sortSubviewsUsingFunction_context(@SortHandles, AChildren);
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.ShowHide(const AWinControl: TWinControl);
|
|
var
|
|
lShow: Boolean;
|
|
begin
|
|
//WriteLn(Format('[TCocoaWSWinControl.ShowHide] AWinControl=%s %s', [AWinControl.Name, AWinControl.ClassName]));
|
|
if AWinControl.HandleAllocated then
|
|
begin
|
|
lShow := AWinControl.HandleObjectShouldBeVisible;
|
|
|
|
NSObject(AWinControl.Handle).lclSetVisible(lShow);
|
|
end;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.Invalidate(const AWinControl: TWinControl);
|
|
begin
|
|
if AWinControl.HandleAllocated then
|
|
NSObject(AWinControl.Handle).lclInvalidate;
|
|
end;
|
|
|
|
class procedure TCocoaWSWinControl.PaintTo(const AWinControl: TWinControl;
|
|
ADC: HDC; X, Y: Integer);
|
|
var
|
|
bc : TCocoaBitmapContext;
|
|
v : NSView;
|
|
b : NSBitmapImageRep;
|
|
obj : NSObject;
|
|
f : NSRect;
|
|
begin
|
|
if not (TObject(ADC) is TCocoaBitmapContext) then Exit;
|
|
if not NSObject(AWinControl.Handle).isKindOfClass(NSView) then Exit;
|
|
bc := TCocoaBitmapContext(ADC);
|
|
v := NSView(AWinControl.Handle);
|
|
f := v.frame;
|
|
f.origin.x := 0;
|
|
f.origin.y := 0;
|
|
|
|
b := v.bitmapImageRepForCachingDisplayInRect(f);
|
|
|
|
v.cacheDisplayInRect_toBitmapImageRep(f, b);
|
|
bc.DrawImageRep(
|
|
NSMakeRect(0,0, f.size.width, f.size.height),
|
|
f, b);
|
|
end;
|
|
|
|
{ TCocoaWSCustomControl }
|
|
|
|
function SendIMCompostionMessage(
|
|
const control: TWinControl; const WParam: LclType.WPARAM ): PtrInt;
|
|
var
|
|
Mess : TLMessage;
|
|
begin
|
|
FillChar(Mess,SizeOf(Mess),0);
|
|
Mess.Msg:= LM_IM_COMPOSITION;
|
|
Mess.WParam:= WParam;
|
|
Result:= DeliverMessage( control, Mess );
|
|
end;
|
|
|
|
// get IMEHandler by LM_IM_COMPOSITION message
|
|
function getControlIMEHandler(const control: TWinControl): ICocoaIMEControl;
|
|
var
|
|
handle : PtrInt;
|
|
begin
|
|
handle := SendIMCompostionMessage( control, IM_MESSAGE_WPARAM_GET_IME_HANDLER );
|
|
Result := TObject(handle) as ICocoaIMEControl;
|
|
end;
|
|
|
|
// get Lookup Word Handler by LM_IM_COMPOSITION message
|
|
function getControlLWHandler(const control: TWinControl): ICocoaLookupWord;
|
|
var
|
|
handle: PtrInt;
|
|
begin
|
|
Result:= nil;
|
|
handle := SendIMCompostionMessage( control, IM_MESSAGE_WPARAM_GET_LW_HANDLER );
|
|
if TObject(handle) is ICocoaLookupWord then
|
|
Result:= TObject(handle) as ICocoaLookupWord;
|
|
end;
|
|
|
|
class function TCocoaWSCustomControl.CreateHandle(const AWinControl: TWinControl;
|
|
const AParams: TCreateParams): TLCLHandle;
|
|
var
|
|
ctrl : TCocoaCustomControl;
|
|
sl : TCocoaManualScrollView;
|
|
hs : TCocoaManualScrollHost;
|
|
lcl : TLCLCommonCallback;
|
|
imeHandler : ICocoaIMEControl;
|
|
begin
|
|
imeHandler := getControlIMEHandler(AWinControl);
|
|
if Assigned(imeHandler) then
|
|
begin
|
|
// AWinControl implements ICocoaIMEControl
|
|
// AWinControl is a Full Control Edit (such as SynEdit/ATSynEdit)
|
|
ctrl := TCocoaFullControlEdit.alloc.lclInitWithCreateParams(AParams);
|
|
lcl := TLCLFullControlEditCallback.Create(ctrl, AWinControl);
|
|
TCocoaFullControlEdit(ctrl).imeHandler:= imeHandler;
|
|
TCocoaFullControlEdit(ctrl).lwHandler:= getControlLWHandler(AWinControl);
|
|
end
|
|
else
|
|
begin
|
|
// AWinControl not implements ICocoaIMEControl
|
|
// AWinControl is a normal Custom Control
|
|
ctrl := TCocoaCustomControlWithBaseInputClient.alloc.lclInitWithCreateParams(AParams);
|
|
lcl := TLCLCommonCallback.Create(ctrl, AWinControl);
|
|
end;
|
|
lcl.BlockCocoaUpDown := true;
|
|
lcl.BlockCocoaKeyBeep := true; // prevent "dings" on keyDown for custom controls (i.e. SynEdit)
|
|
ctrl.callback := lcl;
|
|
|
|
sl := EmbedInManualScrollView(ctrl);
|
|
sl.callback := ctrl.callback;
|
|
|
|
hs := EmbedInManualScrollHost(sl);
|
|
hs.callback := ctrl.callback;
|
|
lcl.SetHandleFrame(hs);
|
|
|
|
ScrollViewSetBorderStyle(hs, TCustomControl(AWinControl).BorderStyle );
|
|
|
|
Result := TLCLHandle(hs);
|
|
end;
|
|
|
|
class procedure TCocoaWSCustomControl.SetBorderStyle(
|
|
const AWinControl: TWinControl; const ABorderStyle: TBorderStyle);
|
|
begin
|
|
if not Assigned(AWinControl) or not (AWinControl.HandleAllocated) then Exit;
|
|
ScrollViewSetBorderStyle( TCocoaManualScrollHost(AWinControl.Handle), ABorderStyle );
|
|
end;
|
|
|
|
function NSObjectDebugStr(obj: NSObject): string;
|
|
begin
|
|
Result := IntToStr(PtrUInt(obj));
|
|
if Assigned(obj) then
|
|
Result := Result +' '+obj.lclClassName+' lcl: '+CallbackDebugStr(obj.lclGetCallback);
|
|
end;
|
|
|
|
function CallbackDebugStr(cb: ICommonCallback): string;
|
|
var
|
|
trg : TObject;
|
|
begin
|
|
Result := IntToStr(PtrUInt(cb));
|
|
if Assigned(cb) then
|
|
begin
|
|
trg := cb.GetTarget;
|
|
Result := Result + ' trg: '+IntToStr(PtrUInt(trg));
|
|
if Assigned(trg) then
|
|
begin
|
|
Result := Result + ' '+trg.ClassName;
|
|
if trg is TWinControl then
|
|
Result := Result +' '+TWinControl(trg).Name;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure DebugDumpParents(fromView: NSView);
|
|
begin
|
|
while Assigned(fromView) do begin
|
|
writeln(fromView.lclClassName);
|
|
fromView := fromView.superView;
|
|
end;
|
|
end;
|
|
|
|
procedure ScrollViewSetBorderStyle(sv: NSScrollView; astyle: TBorderStyle);
|
|
const
|
|
NSBorderStyle : array [TBorderStyle] of NSBorderType = (
|
|
NSNoBorder, // bsNone
|
|
NSBezelBorder // bsSingle (NSLineBorder is too thick)
|
|
);
|
|
begin
|
|
if not Assigned(sv) then Exit;
|
|
sv.setBorderType( NSBorderStyle[astyle] );
|
|
end;
|
|
|
|
end.
|
|
|