diff --git a/components/synedit/lazsyncocoaimm.pas b/components/synedit/lazsyncocoaimm.pas new file mode 100644 index 0000000000..3a63309bad --- /dev/null +++ b/components/synedit/lazsyncocoaimm.pas @@ -0,0 +1,238 @@ +{ + SynEdit MacOS IME Handler: + 1. various IME are fully supported, such as Chinese/Japanese/Korean and DeadKeys + 2. MultiCarets supported + 3. GroupUndo or not are both fully supported + + in order to be compatible with MultiCarets: + 1. TSynEditUndoList.BeginBlock() cannot be used directly, + only TSynEditUndoList.GroupUndo and TSynEditUndoList.ForceGroupEnd() + can be combined to Undo + 2. InsertTextAtCaret() cannot be used directly, only ecChar Command can be used + 3. mabye the support for MultiSelections in MultiCaretsPlugin is not perfect. + for example, Shift+Arrow can only expand the Selection of the Main Caret, + but not other Carets +} + +unit LazSynCocoaIMM; + +{$mode objfpc}{$H+} + +interface + +uses + CocoaPrivate, + Classes, SysUtils, + SynEditMiscClasses, LazSynIMMBase, SynEditKeyCmds, SynEditTextBase; + +type + { LazSynImeCocoa } + + LazSynImeCocoa = class( LazSynIme, ICocoaIMEControl ) + private + _undoList: TSynEditUndoList; + _IntermediateTextBeginPos: TPoint; + public + procedure IMESessionBegin; + procedure IMESessionEnd; + procedure IMEUpdateIntermediateText( var params: TCocoaIMEParameters ); + procedure IMEInsertFinalText( var params: TCocoaIMEParameters ); + function IMEGetTextBound( var params: TCocoaIMEParameters ) : TRect; + private + procedure InsertTextAtCaret_CompatibleWithMultiCarets( var params: TCocoaIMEParameters ) ; + procedure SelectText_CompatibleWithMultiCarets( var params: TCocoaIMEParameters ); + function calcBound( var params: TCocoaIMEParameters ) : TRect; + function PosToPixels( const pos: TPoint ) : TPoint; + public + constructor Create(AOwner: TSynEditBase); + destructor Destroy; override; + end; + +implementation + +uses + LCLType, LazUTF8, SynEdit, LazSynEditText; + +procedure LazSynImeCocoa.IMESessionBegin; +begin + if FriendEdit.ReadOnly then exit; + DoIMEStarted; +end; + +procedure LazSynImeCocoa.IMESessionEnd; +begin + if FriendEdit.ReadOnly then exit; + ViewedTextBuffer.RedoList.Clear; // clear Intermediate Text redo items + DoIMEEnded; +end; + +{ + update IME Intermediate Text, Key function for IME: + 1. some IME do not have a popup window and rely on the Editor + to display the Intermediate Text + 2. use selection to simulate Intermediate Text display in this implementation, + may be better to use TSynEditMarkup (slightly complicated) + 3. it means completely cancel the IME session if Intermediate Text is empty + 4. it's First call of IMEUpdateIntermediateText or IMEInsertFinalText + if isFirstCall=True + 5. eat some chars if eatAmount>0 (such as DeadKeys) +} +procedure LazSynImeCocoa.IMEUpdateIntermediateText( var params: TCocoaIMEParameters ); +var + groupUndoBefore: boolean; +begin + if FriendEdit.ReadOnly + then exit; + + // clear last Intermediate Text + if not params.isFirstCall then + FriendEdit.Undo; + + // length=0 means to completely cancel the IME session + if params.textCharLength=0 then + exit; + + // save caret pos + _IntermediateTextBeginPos := FriendEdit.LogicalCaretXY; + + // in order to be compatible with MultiCarets, + // TSynEditUndoList.BeginBlock() cannot be used directly, + // only TSynEditUndoList.GroupUndo and TSynEditUndoList.ForceGroupEnd() + // can be combined to Undo + groupUndoBefore:= _undoList.GroupUndo; + _undoList.GroupUndo:= true; + _undoList.ForceGroupEnd; + + // need to eat some chars, such as DeadKeys + if params.eatAmount<>0 then + TSynEdit(FriendEdit).CommandProcessor(ecDeleteLastChar, #0, nil); + + // in order to be compatible with MultiCarets, + // InsertTextAtCaret() cannot be used directly, + // only ecChar Command can be used indirectly + InsertTextAtCaret_CompatibleWithMultiCarets( params ); + SelectText_CompatibleWithMultiCarets( params ); + + _undoList.GroupUndo:= groupUndoBefore; +end; + +{ + insert IME Final Text, Key function for IME: + 1. called only when inputting via IME, otherwise handled by UTF8KeyPress() + 2. when the IME input is finished, either IMEUpdateIntermediateText(with empty text) + is called, or IMEInsertFinalText(with final text) is called, + NOT the both + 3. it's First call of IMEUpdateIntermediateText or IMEInsertFinalText + if isFirstCall=True + 4. eat some chars if eatAmount>0 (such as DeadKeys) +} +procedure LazSynImeCocoa.IMEInsertFinalText( var params: TCocoaIMEParameters ); +begin + if FriendEdit.ReadOnly then exit; + + // clear Intermediate Text + if not params.isFirstCall then + FriendEdit.Undo; + + // need to eat some chars, such as DeadKeys + if params.eatAmount<>0 then + TSynEdit(FriendEdit).CommandProcessor( ecDeleteLastChar, #0, nil ); + + InsertTextAtCaret_CompatibleWithMultiCarets( params ); +end; + +{ + calc Intermediate Text bound: + 1. return Intermediate Text bound when in IME inut state. it's possible + to only get the bound of the Intermediate Text in a subrange + (selectedStart and selectedLength) + 2. return caret pos when not in IME input state + 3. in Screen Pixels +} +function LazSynImeCocoa.IMEGetTextBound( var params: TCocoaIMEParameters ) : TRect; +begin + if not params.isFirstCall then + Result:= calcBound( params ) + else + Result:= TRect.Create( Point(FriendEdit.CaretXPix,FriendEdit.CaretYPix), 0, FriendEdit.LineHeight ); + + Result:= FriendEdit.ClientToScreen( Result ); +end; + + +procedure LazSynImeCocoa.InsertTextAtCaret_CompatibleWithMultiCarets( var params: TCocoaIMEParameters ); +var + i: integer; + c: integer; + ch: TUTF8Char; +begin + if params.textByteLength=0 then Exit; + i:=1; + while( i<=params.textByteLength ) do + begin + c:= Utf8CodePointLen( @params.text[i], params.textByteLength-i+1, false ); + ch:= Copy( params.text, i, c ); + TSynEdit(FriendEdit).CommandProcessor( ecChar, ch, nil ); + inc( i, c ); + end; +end; + +procedure LazSynImeCocoa.SelectText_CompatibleWithMultiCarets( var params: TCocoaIMEParameters ); +var + i: Integer; + start: Integer; + length: Integer; +begin + if params.selectedLength=0 then begin + start:= 0; + length:= params.textCharLength; + end else begin + start:= params.selectedStart; + length:= params.selectedLength; + end; + + for i:=params.textCharLength-start downto 1 do TSynEdit(FriendEdit).CommandProcessor( ecLeft, #0, nil ); + for i:=length downto 1 do TSynEdit(FriendEdit).CommandProcessor( ecSelRight, #0, nil ); +end; + +function LazSynImeCocoa.calcBound( var params: TCocoaIMEParameters ) : TRect; +var + p1: TPoint; + p2: TPoint; +begin + // two vertexs in bytes + p1:= _IntermediateTextBeginPos; + p2:= p1; + p1.X:= p1.X + UTF8CodepointToByteIndex( pchar(@params.text[1]), params.textByteLength, params.selectedStart ); + p2.X:= p2.X + UTF8CodepointToByteIndex( pchar(@params.text[1]), params.textByteLength, params.selectedStart+params.selectedLength ); + + // two vertexs in pixels + p1:= PosToPixels( p1 ); + p2:= PosToPixels( p2 ); + p2.Y:= p2.Y + FriendEdit.LineHeight; + + // client rect in pixels + Result:= TRect.Create( p1 , p2 ); +end; + +function LazSynImeCocoa.PosToPixels( const pos: TPoint ) : TPoint; +begin + Result:= FriendEdit.LogicalToPhysicalPos( pos ); + Result:= FriendEdit.TextXYToScreenXY( Result ); + Result:= TSynEdit(FriendEdit).ScreenXYToPixels( Result ); +end; + +constructor LazSynImeCocoa.Create( AOwner: TSynEditBase ); +begin + Inherited; + _undoList:= ViewedTextBuffer.UndoList; +end; + +destructor LazSynImeCocoa.Destroy; +begin + _undoList:= nil; + Inherited; +end; + +end. + diff --git a/components/synedit/synedit.lpk b/components/synedit/synedit.lpk index 9750c91a65..41318a1334 100644 --- a/components/synedit/synedit.lpk +++ b/components/synedit/synedit.lpk @@ -394,6 +394,11 @@ If you wish to allow use of your version of these files only under the terms of + + + + + diff --git a/components/synedit/synedit.pp b/components/synedit/synedit.pp index 5b548cb5dc..170ba6ea3c 100644 --- a/components/synedit/synedit.pp +++ b/components/synedit/synedit.pp @@ -50,6 +50,10 @@ unit SynEdit; {$ENDIF} {$ENDIF} +{$IFDEF LCLCOCOA} + {$DEFINE CocoaIME} +{$ENDIF} + {$I synedit.inc} @@ -106,6 +110,9 @@ uses {$IFDEF WinIME} LazSynIMM, {$ENDIF} + {$IFDEF CocoaIME} + LazSynCocoaIMM, + {$ENDIF} {$IFDEF Gtk2IME} LazSynGtk2IMM, {$ENDIF} @@ -438,6 +445,10 @@ type protected procedure GTK_IMComposition(var Message: TMessage); message LM_IM_COMPOSITION; {$ENDIF} + {$IFDEF CocoaIME} + private + procedure COCOA_IMComposition(var Message: TMessage); message LM_IM_COMPOSITION; + {$ENDIF} {$IFDEF WinIME} private procedure WMImeRequest(var Msg: TMessage); message WM_IME_REQUEST; @@ -2339,6 +2350,10 @@ begin {$ENDIF} FImeHandler.InvalidateLinesMethod := @InvalidateLines; {$ENDIF} + {$IFDEF CocoaIME} + FImeHandler := LazSynImeCocoa.Create(Self); + FImeHandler.InvalidateLinesMethod := @InvalidateLines; + {$ENDIF} {$IFDEF Gtk2IME} FImeHandler := LazSynImeGtk2 .Create(Self); FImeHandler.InvalidateLinesMethod := @InvalidateLines; @@ -2926,6 +2941,13 @@ begin end; {$endif} +{$ifdef CocoaIME} +procedure TCustomSynEdit.Cocoa_IMComposition(var Message: TMessage); +begin + Message.Result := PtrInt(FImeHandler); +end; +{$endif} + {$IFDEF WinIME} procedure TCustomSynEdit.WMImeRequest(var Msg: TMessage); begin