mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-30 16:03:40 +02:00
502 lines
16 KiB
ObjectPascal
502 lines
16 KiB
ObjectPascal
unit CocoaFullControlEdit;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{$modeswitch objectivec2}
|
|
{$interfaces corba}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
LazUTF8, Graphics, CocoaGDIObjects,
|
|
CocoaAll, CocoaPrivate, CocoaCustomControl, CocoaUtils;
|
|
|
|
const
|
|
IM_MESSAGE_WPARAM_GET_IME_HANDLER = 0;
|
|
IM_MESSAGE_WPARAM_GET_LW_HANDLER = 1;
|
|
|
|
type
|
|
{ ICocoaIMEControl }
|
|
|
|
// IME Parameters for Cocoa Interface internal and LCL Full Control Edit
|
|
// intentionally keep the Record type, emphasizing that it is only a simple type,
|
|
// only used as parameters, don't put into logical functions
|
|
TCocoaIMEParameters = record
|
|
text: ShortString; // Marked Text
|
|
textCharLength: Integer; // length in code point
|
|
textByteLength: Integer; // length in bytes
|
|
textNSLength: Integer; // length in code unit (NSString)
|
|
selectedStart: Integer; // selected range start in code point
|
|
selectedLength: Integer; // selected range length in code point
|
|
eatAmount: Integer; // delete char out of Marked Text
|
|
isFirstCall: Boolean; // if first in the IME session
|
|
end;
|
|
|
|
// the LCL Component that need Cocoa IME support need to
|
|
// implement this simple interface
|
|
// class LazSynCocoaIMM in SynEdit Component for reference
|
|
// class ATSynEdit_Adapter_CocoaIME in ATSynEdit Component for reference
|
|
ICocoaIMEControl = interface ['{AAD5C3AD-C8E0-20A4-E779-6F5D4F8380BD}']
|
|
procedure IMESessionBegin;
|
|
procedure IMESessionEnd;
|
|
procedure IMEUpdateIntermediateText( var params: TCocoaIMEParameters );
|
|
procedure IMEInsertFinalText( var params: TCocoaIMEParameters );
|
|
function IMEGetTextBound( var params: TCocoaIMEParameters ): TRect;
|
|
end;
|
|
|
|
{ ICocoaLookupWord }
|
|
|
|
// Lookup Word Parameters for Cocoa Interface internal and LCL Full Control Edit
|
|
// intentionally keep the Record type, emphasizing that it is only a simple type,
|
|
// only used as parameters, don't put into logical functions
|
|
TCocoaLWParameters = record
|
|
text: String; // return line text from LCL Full Control Edit
|
|
row: Integer; // the row being looked up
|
|
col: Integer; // the column being looked up
|
|
length: Integer; // the length the text we want to obtain
|
|
end;
|
|
|
|
// the LCL Component that need Cocoa Lookup Word support need to
|
|
// implement this simple interface
|
|
ICocoaLookupWord = interface ['{F5B0D020-1F29-9E8C-33DD-AA122597E6A2}']
|
|
procedure LWRowColForScreenPoint( var params: TCocoaLWParameters;
|
|
const screenPoint: TPoint );
|
|
procedure LWLineForRow( var params: TCocoaLWParameters );
|
|
function LWGetTextBound( var params: TCocoaLWParameters ): TRect;
|
|
function LWGetFont( var params: TCocoaLWParameters ): TFont;
|
|
end;
|
|
|
|
{ TCocoaFullControlEdit }
|
|
|
|
// backend of LCL Full Control Edit Component (such as SynEdit/ATSynEdit)
|
|
// Key Class for Cocoa IME support
|
|
// 1. obtain IME capability from Cocoa by implementing NSTextInputClientProtocol
|
|
// 2. synchronize IME data with LCL via ICocoaIMEControl
|
|
TCocoaFullControlEdit = objcclass(TCocoaCustomControl, NSTextInputClientProtocol)
|
|
private
|
|
_currentParams: TCocoaIMEParameters;
|
|
_currentMarkedText: NSString;
|
|
public
|
|
imeHandler: ICocoaIMEControl;
|
|
lwHandler: ICocoaLookupWord;
|
|
public
|
|
function initWithFrame(frameRect: NSRect): id; override;
|
|
|
|
procedure keyDown(theEvent: NSEvent); override;
|
|
procedure mouseDown(event: NSEvent); override;
|
|
procedure mouseUp(event: NSEvent); override;
|
|
function resignFirstResponder: ObjCBOOL; override;
|
|
|
|
procedure insertText_replacementRange (aString: id; replacementRange: NSRange);
|
|
procedure setMarkedText_selectedRange_replacementRange (aString: id; newRange: NSRange; replacementRange: NSRange);
|
|
procedure unmarkText;
|
|
function selectedRange: NSRange;
|
|
function markedRange: NSRange;
|
|
function hasMarkedText: LCLObjCBoolean;
|
|
function firstRectForCharacterRange_actualRange ({%H-}aRange: NSRange; {%H-}actualRange: NSRangePointer): NSRect;
|
|
|
|
function attributedSubstringForProposedRange_actualRange (aRange: NSRange; actualRange: NSRangePointer): NSAttributedString;
|
|
function validAttributesForMarkedText: NSArray;
|
|
function characterIndexForPoint (aPoint: NSPoint): NSUInteger;
|
|
end;
|
|
|
|
implementation
|
|
|
|
{ TCocoaIMEParameters }
|
|
|
|
// set text and length in params
|
|
procedure setIMEParamsText( var params: TCocoaIMEParameters; const nsText: NSString );
|
|
begin
|
|
params.text := NSStringToString( nsText );
|
|
params.textCharLength := UTF8Length( params.text );
|
|
params.textByteLength := Length( params.text );
|
|
params.textNSLength := nsText.length;
|
|
end;
|
|
|
|
// set selected range in code point
|
|
procedure setIMESelectedRange( var params: TCocoaIMEParameters; const nsText: NSString; range: NSRange );
|
|
begin
|
|
if range.location<>NSNotFound then
|
|
begin
|
|
if range.location>nsText.length then
|
|
range.location:= 0;
|
|
if range.location+range.length>nsText.length then
|
|
range.length:= nsText.length-range.location;
|
|
end;
|
|
|
|
if range.location=NSNotFound then
|
|
params.selectedStart:= 0
|
|
else
|
|
params.selectedStart:= UTF8Length( nsText.substringToIndex(range.location).UTF8String );
|
|
|
|
if range.length=0 then
|
|
params.selectedLength:= 0
|
|
else
|
|
params.selectedLength:= UTF8Length( nsText.substringWithRange(range).UTF8String );
|
|
end;
|
|
|
|
{ TCocoaFullControlEdit }
|
|
|
|
function TCocoaFullControlEdit.initWithFrame(frameRect: NSRect): id;
|
|
begin
|
|
Result:=inherited initWithFrame(frameRect);
|
|
self.unmarkText;
|
|
end;
|
|
|
|
{
|
|
for IME Key Down:
|
|
Key step for IME (such as Chinese/Japanese/Korean and DeadKeys)
|
|
1. forward key event to NSInputContext
|
|
2. NSInputContext will call TCocoaFullControlEdit(NSTextControlClient)
|
|
and then call LCL via imeHandler
|
|
}
|
|
procedure TCocoaFullControlEdit.keyDown(theEvent: NSEvent);
|
|
begin
|
|
inputContext.handleEvent(theEvent);
|
|
end;
|
|
|
|
{
|
|
for IME Close:
|
|
1. mouseDown() will not be called when click in the IME Popup Window,
|
|
so it must be clicking outside the IME Popup Windows,
|
|
which should end the IME input
|
|
2. Cocoa had called setMarkedText_selectedRange_replacementRange()
|
|
or insertText_replacementRange() first, then mouseDown() here
|
|
3. NSInputContext.handleEvent() just close IME window here
|
|
4. LCL actually handle mouse event
|
|
}
|
|
procedure TCocoaFullControlEdit.mouseDown(event: NSEvent);
|
|
begin
|
|
inputContext.handleEvent(event);
|
|
Inherited;
|
|
end;
|
|
|
|
procedure TCocoaFullControlEdit.mouseUp(event: NSEvent);
|
|
begin
|
|
inputContext.handleEvent(event);
|
|
Inherited;
|
|
end;
|
|
|
|
// prevent switch to other control when in IME input state
|
|
function TCocoaFullControlEdit.resignFirstResponder: ObjCBOOL;
|
|
begin
|
|
Result := not hasMarkedText();
|
|
end;
|
|
|
|
function isIMEDuplicateCall( const newParams, currentParams: TCocoaIMEParameters ) : Boolean;
|
|
begin
|
|
Result:= false;
|
|
if newParams.isFirstCall then exit;
|
|
if newParams.text <> currentParams.text then exit;
|
|
if newParams.selectedStart<>currentParams.selectedStart then exit;
|
|
if newParams.selectedLength<>currentParams.selectedLength then exit;
|
|
Result:= true;
|
|
end;
|
|
|
|
// send Marked/Intermediate Text to LCL Edit Control which has IME Handler
|
|
// Key step for IME (such as Chinese/Japanese/Korean and DeadKeys)
|
|
procedure TCocoaFullControlEdit.setMarkedText_selectedRange_replacementRange(
|
|
aString: id; newRange: NSRange; replacementRange: NSRange);
|
|
var
|
|
params : TCocoaIMEParameters;
|
|
nsText : NSString;
|
|
begin
|
|
params.isFirstCall:= not hasMarkedText();
|
|
|
|
// no markedText before, the first call
|
|
if params.isFirstCall then imeHandler.IMESessionBegin;
|
|
|
|
// get IME Intermediate Text
|
|
nsText:= getNSStringObject( aString );
|
|
setIMEParamsText( params, nsText );
|
|
|
|
// some IME want to select subRange of Intermediate Text
|
|
// such as Japanese
|
|
setIMESelectedRange( params, nsText, newRange );
|
|
|
|
// some IME incorrectly call setMarkedText() twice with the same parameters
|
|
if isIMEDuplicateCall( params, _currentParams ) then
|
|
exit;
|
|
|
|
// some IME want to eat some chars
|
|
// such as inputting DeadKeys
|
|
if replacementRange.location<>NSNotFound then
|
|
params.eatAmount:= 1 - replacementRange.location
|
|
else
|
|
params.eatAmount:= 0;
|
|
|
|
// Key Step to update(display) Marked/Intermediate Text
|
|
imeHandler.IMEUpdateIntermediateText( params );
|
|
|
|
if params.textNSLength=0 then
|
|
begin
|
|
// cancel Marked/Intermediate Text
|
|
imeHandler.IMESessionEnd;
|
|
unmarkText;
|
|
end
|
|
else
|
|
begin
|
|
// update Marked/Intermediate Text internal status
|
|
_currentParams:= params;
|
|
_currentMarkedText.release;
|
|
_currentMarkedText:= nsText;
|
|
_currentMarkedText.retain;
|
|
end;
|
|
end;
|
|
|
|
{
|
|
send final Text to LCL Edit Control which has IME Handler
|
|
Key step for IME (such as Chinese/Japanese/Korean and DeadKeys)
|
|
1. if in IME input state, handle text via imeHandler.IMEInsertFinalText()
|
|
2. otherwise via lclGetCallback.InputClientInsertText,
|
|
mainly for maximum forward compatibility with TCocoaCustomControl
|
|
}
|
|
procedure TCocoaFullControlEdit.insertText_replacementRange(aString: id;
|
|
replacementRange: NSRange);
|
|
var
|
|
params: TCocoaIMEParameters;
|
|
nsText : NSString;
|
|
begin
|
|
params.isFirstCall:= not hasMarkedText();
|
|
|
|
// IME final text
|
|
nsText:= getNSStringObject( aString );
|
|
setIMEParamsText( params, nsText );
|
|
|
|
// some IME want to eat some chars, such as inputting DeadKeys
|
|
if replacementRange.location<>NSNotFound then
|
|
params.eatAmount:= 1 - replacementRange.location
|
|
else
|
|
params.eatAmount:= 0;
|
|
|
|
if (not params.isFirstCall) or (params.eatAmount<>0) then
|
|
// insert IME final text
|
|
imeHandler.IMEInsertFinalText( params )
|
|
else
|
|
// insert normal text (without IME) by LCLControl.IntfUTF8KeyPress()
|
|
lclGetCallback.InputClientInsertText( params.text );
|
|
|
|
if not params.isFirstCall then
|
|
begin
|
|
imeHandler.IMESessionEnd;
|
|
unmarkText;
|
|
end;
|
|
end;
|
|
|
|
|
|
{
|
|
the Cocoa API uses a continuous positive integer to locate the text position,
|
|
mainly in:
|
|
1. get the text index corresponding to the current mouse cursor through
|
|
the return value of characterIndexForPoint()
|
|
2. the text is obtained through the parameter aRange.location passed in by
|
|
attributedSubstringForProposedRange_actualRange().
|
|
note that Cocoa assumes that the index are continuous, with -1 corresponding
|
|
to the previous character and +1 corresponding to the next character,
|
|
which is where the trouble comes in.
|
|
|
|
however, in the controls such as SynEdit, there is no corresponding Index.
|
|
although the index can be calculated by traversing each line, it increases
|
|
the workload and complexity.
|
|
|
|
in Lookup Word, we only need to ensure that the index is continuous in a line,
|
|
so a simplified method is used:
|
|
1. controls such as SynEdit are indexed by row + column
|
|
2. the index required by Cooca is a 64-bit integer
|
|
3. so the rows and columns are encoded into 64-bit Index, with
|
|
the high 32 bits corresponding to the rows and
|
|
the lower 32 bits corresponding to the columns.
|
|
4. the operation on the Index cannot cross line. if it does,
|
|
a simple correction is required. see rangeToLWParams().
|
|
}
|
|
|
|
const
|
|
LW_LOCATION_BASE = $1000000000000000;
|
|
|
|
function rangeToLWParams( const aRange: NSRange ): TCocoaLWParameters;
|
|
var
|
|
location: NSUInteger;
|
|
begin
|
|
location:= aRange.location;
|
|
if location >= (LW_LOCATION_BASE/2) then
|
|
location:= location - LW_LOCATION_BASE;
|
|
Result.row:= location shr 32;
|
|
Result.col:= location and $FFFFFFFF;
|
|
Result.length:= aRange.length;
|
|
if Result.col < 0 then begin
|
|
Result.col:= 0;
|
|
Result.row:= Result.row + 1;
|
|
end;
|
|
end;
|
|
|
|
function LWParamsToRange( const params: TCocoaLWParameters ): NSRange;
|
|
var
|
|
location: NSUInteger;
|
|
begin
|
|
location:= (QWord(params.row) shl 32) + params.col;
|
|
Result.location:= location + LW_LOCATION_BASE;
|
|
Result.length:= params.length;
|
|
end;
|
|
|
|
// cursor tracking
|
|
function TCocoaFullControlEdit.firstRectForCharacterRange_actualRange(
|
|
aRange: NSRange; actualRange: NSRangePointer): NSRect;
|
|
|
|
function getImeTextBound: TRect;
|
|
var
|
|
params: TCocoaIMEParameters;
|
|
begin
|
|
params:= _currentParams;
|
|
setIMESelectedRange( params, _currentMarkedText, aRange );
|
|
params.isFirstCall:= not hasMarkedText();
|
|
Result:= imeHandler.IMEGetTextBound( params );
|
|
end;
|
|
|
|
function getLookupWordBound: TRect;
|
|
var
|
|
params: TCocoaLWParameters;
|
|
begin
|
|
params:= rangeToLWParams( aRange );
|
|
Result:= lwHandler.LWGetTextBound( params );
|
|
end;
|
|
|
|
var
|
|
rect : TRect;
|
|
begin
|
|
if aRange.location < LW_LOCATION_BASE then begin
|
|
rect:= getImeTextBound;
|
|
end else begin
|
|
rect:= getLookupWordBound;
|
|
end;
|
|
LCLToNSRect( rect, NSGlobalScreenBottom, Result );
|
|
end;
|
|
|
|
procedure TCocoaFullControlEdit.unmarkText;
|
|
begin
|
|
setIMEParamsText( _currentParams, nil );
|
|
_currentParams.selectedStart:= 0;
|
|
_currentParams.selectedLength:= 0;
|
|
_currentParams.eatAmount:= 0;
|
|
_currentParams.isFirstCall:= true;
|
|
_currentMarkedText.release;
|
|
_currentMarkedText:= nil;
|
|
end;
|
|
|
|
function TCocoaFullControlEdit.markedRange: NSRange;
|
|
begin
|
|
if _currentParams.textNSLength=0 then
|
|
Result:= NSMakeRange( NSNotFound, 0 )
|
|
else
|
|
Result:= NSMakeRange( 0, _currentParams.textNSLength );
|
|
end;
|
|
|
|
function TCocoaFullControlEdit.selectedRange: NSRange;
|
|
begin
|
|
if _currentParams.textNSLength=0 then
|
|
Result:= NSMakeRange( 0, 0 )
|
|
else
|
|
Result:= NSMakeRange( _currentParams.selectedStart, _currentParams.selectedLength );
|
|
end;
|
|
|
|
function TCocoaFullControlEdit.hasMarkedText: LCLObjCBoolean;
|
|
begin
|
|
Result:= ( _currentParams.textNSLength > 0 );
|
|
end;
|
|
|
|
{
|
|
1. given the previous description of not crossing lines,
|
|
at most one line of text is returned.
|
|
2. LW_LOCATION_BASE has been added to location to distinguish
|
|
between IME and Lookup Word.
|
|
}
|
|
function TCocoaFullControlEdit.attributedSubstringForProposedRange_actualRange(
|
|
aRange: NSRange; actualRange: NSRangePointer): NSAttributedString;
|
|
var
|
|
params: TCocoaLWParameters;
|
|
textWord: NSString;
|
|
|
|
procedure initParams;
|
|
begin
|
|
params:= rangeToLWParams( aRange );
|
|
self.lwHandler.LWLineForRow( params );
|
|
end;
|
|
|
|
procedure initTextWord;
|
|
var
|
|
lineText: NSString;
|
|
subRange: NSRange;
|
|
begin
|
|
lineText:= StrToNSString( params.text );
|
|
subRange.location:= params.col;
|
|
subRange.length:= aRange.length;
|
|
if subRange.location >= lineText.length then begin
|
|
textWord:= NSSTR( ' ' );
|
|
Exit;
|
|
end;
|
|
if subRange.location + subRange.length > lineText.length then
|
|
subRange.length:= lineText.length - subRange.location;
|
|
textWord:= lineText.substringWithRange( subRange );
|
|
end;
|
|
|
|
function getAttributeWord: NSAttributedString;
|
|
var
|
|
attribs: NSDictionary;
|
|
lclFont: TFont;
|
|
cocoaFont: NSFont;
|
|
begin
|
|
lclFont:= self.lwHandler.LWGetFont( params );
|
|
cocoaFont:= TCocoaFont(lclFont.Reference.Handle).Font;
|
|
attribs:= NSMutableDictionary.alloc.initWithCapacity(1);
|
|
attribs.setValue_forKey( cocoaFont, NSFontAttributeName );
|
|
Result:= NSAttributedString.alloc.initWithString_attributes(
|
|
textWord, attribs );
|
|
Result.autorelease;
|
|
attribs.release;
|
|
end;
|
|
|
|
procedure setActualRange;
|
|
begin
|
|
if aRange.location >= (LW_LOCATION_BASE/2) then begin
|
|
params.length:= textWord.length;
|
|
actualRange^:= LWParamsToRange( params );
|
|
end else begin
|
|
actualRange^.location:= aRange.location;
|
|
actualRange^.length:= textWord.length;
|
|
end
|
|
end;
|
|
|
|
begin
|
|
Result:= nil;
|
|
|
|
if NOT Assigned(self.lwHandler) then
|
|
Exit;
|
|
|
|
initParams;
|
|
initTextWord;
|
|
setActualRange;
|
|
Result:= getAttributeWord;
|
|
end;
|
|
|
|
function TCocoaFullControlEdit.characterIndexForPoint(aPoint: NSPoint
|
|
): NSUInteger;
|
|
var
|
|
params: TCocoaLWParameters;
|
|
lclPoint: TPoint;
|
|
begin
|
|
Result:= NSNotFound;
|
|
if NOT Assigned(self.lwHandler) then
|
|
Exit;
|
|
lclPoint:= ScreenPointFromNSToLCL( aPoint );
|
|
self.lwHandler.LWRowColForScreenPoint( params, lclPoint );
|
|
if params.col >= 0 then
|
|
Result:= LWParamsToRange(params).location;
|
|
end;
|
|
|
|
function TCocoaFullControlEdit.validAttributesForMarkedText: NSArray;
|
|
begin
|
|
Result := nil;
|
|
end;
|
|
|
|
end.
|
|
|