1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
|
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.
|