964 lines
31 KiB
C#
964 lines
31 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using ImeSharp.Native;
|
|
using SharpGen.Runtime;
|
|
using SharpGen.Runtime.Win32;
|
|
using TsfSharp;
|
|
|
|
namespace ImeSharp
|
|
{
|
|
internal class TextStore : CallbackBase,
|
|
ITextStoreACP,
|
|
ITfContextOwnerCompositionSink,
|
|
ITfTextEditSink,
|
|
ITfUIElementSink
|
|
{
|
|
public static readonly Guid IID_ITextStoreACPSink = new Guid(0x22d44c94, 0xa419, 0x4542, 0xa2, 0x72, 0xae, 0x26, 0x09, 0x3e, 0xce, 0xcf);
|
|
public static readonly Guid GUID_PROP_COMPOSING = new Guid("e12ac060-af15-11d2-afc5-00105a2799b5");
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Constructors
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
#region Constructors
|
|
|
|
// Creates a TextStore instance.
|
|
public TextStore(IntPtr windowHandle)
|
|
{
|
|
_windowHandle = windowHandle;
|
|
|
|
|
|
_viewCookie = Environment.TickCount;
|
|
|
|
_editCookie = Tsf.TF_INVALID_COOKIE;
|
|
_uiElementSinkCookie = Tsf.TF_INVALID_COOKIE;
|
|
_textEditSinkCookie = Tsf.TF_INVALID_COOKIE;
|
|
|
|
_IMEStringPool = ArrayPool<IMEString>.Shared;
|
|
}
|
|
|
|
#endregion Constructors
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Methods - ITextStoreACP
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
#region ITextStoreACP
|
|
|
|
public void AdviseSink(Guid riid, IUnknown obj, int flags)
|
|
{
|
|
ITextStoreACPSink sink;
|
|
|
|
if (riid != IID_ITextStoreACPSink)
|
|
throw new COMException("TextStore_CONNECT_E_CANNOTCONNECT");
|
|
|
|
sink = (obj as ComObject).QueryInterface<ITextStoreACPSink>();
|
|
|
|
if (sink == null)
|
|
throw new COMException("TextStore_E_NOINTERFACE");
|
|
|
|
// It's legal to replace existing sink.
|
|
if (_sink != null)
|
|
_sink.Dispose();
|
|
|
|
(obj as ComObject).Dispose();
|
|
|
|
_sink = sink;
|
|
}
|
|
|
|
public void UnadviseSink(IUnknown obj)
|
|
{
|
|
var sink = (obj as ComObject).QueryInterface<ITextStoreACPSink>();
|
|
if (sink.NativePointer != _sink.NativePointer)
|
|
throw new COMException("TextStore_CONNECT_E_NOCONNECTION");
|
|
|
|
_sink.Release();
|
|
_sink = null;
|
|
}
|
|
|
|
private bool _LockDocument(TsfSharp.TsLfFlags dwLockFlags)
|
|
{
|
|
if (_locked)
|
|
return false;
|
|
|
|
_locked = true;
|
|
_lockFlags = dwLockFlags;
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ResetIfRequired()
|
|
{
|
|
if (!_commited)
|
|
return;
|
|
|
|
_commited = false;
|
|
|
|
TsTextchange textChange;
|
|
textChange.AcpStart = 0;
|
|
textChange.AcpOldEnd = _inputBuffer.Count;
|
|
textChange.AcpNewEnd = 0;
|
|
_inputBuffer.Clear();
|
|
|
|
_sink.OnTextChange(0, textChange);
|
|
|
|
_acpStart = _acpEnd = 0;
|
|
_sink.OnSelectionChange();
|
|
_commitStart = _commitLength = 0;
|
|
|
|
//Debug.WriteLine("TextStore reset!!!");
|
|
}
|
|
|
|
private void _UnlockDocument()
|
|
{
|
|
Result hr;
|
|
_locked = false;
|
|
_lockFlags = 0;
|
|
|
|
ResetIfRequired();
|
|
|
|
//if there is a queued lock, grant it
|
|
if (_lockRequestQueue.Count > 0)
|
|
{
|
|
hr = RequestLock(_lockRequestQueue.Dequeue());
|
|
}
|
|
|
|
//if any layout changes occurred during the lock, notify the manager
|
|
if (_layoutChanged)
|
|
{
|
|
_layoutChanged = false;
|
|
_sink.OnLayoutChange(TsLayoutCode.TsLcChange, _viewCookie);
|
|
}
|
|
}
|
|
|
|
private bool _IsLocked(TsfSharp.TsLfFlags dwLockType)
|
|
{
|
|
return _locked && (_lockFlags & dwLockType) != 0;
|
|
}
|
|
|
|
public Result RequestLock(TsfSharp.TsLfFlags dwLockFlags)
|
|
{
|
|
Result hrSession;
|
|
|
|
if (_sink == null)
|
|
throw new COMException("TextStore_NoSink");
|
|
|
|
if (dwLockFlags == 0)
|
|
throw new COMException("TextStore_BadLockFlags");
|
|
|
|
hrSession = Result.Fail;
|
|
|
|
if (_locked)
|
|
{
|
|
//the document is locked
|
|
|
|
if ((dwLockFlags & TsfSharp.TsLfFlags.Sync) == TsfSharp.TsLfFlags.Sync)
|
|
{
|
|
/*
|
|
The caller wants an immediate lock, but this cannot be granted because
|
|
the document is already locked.
|
|
*/
|
|
hrSession = (int)TsErrors.TsESynchronous;
|
|
}
|
|
else
|
|
{
|
|
//the request is asynchronous
|
|
|
|
//Queue the lock request
|
|
_lockRequestQueue.Enqueue(dwLockFlags);
|
|
hrSession = (int)TsErrors.TsSAsync;
|
|
}
|
|
|
|
return hrSession;
|
|
}
|
|
|
|
//lock the document
|
|
_LockDocument(dwLockFlags);
|
|
|
|
//call OnLockGranted
|
|
hrSession = _sink.OnLockGranted(dwLockFlags);
|
|
|
|
//unlock the document
|
|
_UnlockDocument();
|
|
|
|
return hrSession;
|
|
}
|
|
|
|
public TsStatus GetStatus()
|
|
{
|
|
TsStatus status = new TsStatus();
|
|
status.DynamicFlags = 0;
|
|
status.StaticFlags = 0;
|
|
|
|
return status;
|
|
}
|
|
|
|
public void QueryInsert(int acpTestStart, int acpTestEnd, uint cch, out int acpResultStart, out int acpResultEnd)
|
|
{
|
|
acpResultStart = acpResultEnd = 0;
|
|
|
|
// Fix possible crash
|
|
if (_inputBuffer.Count == 0)
|
|
return;
|
|
|
|
//Queryins
|
|
if (acpTestStart > _inputBuffer.Count || acpTestEnd > _inputBuffer.Count)
|
|
throw new COMException("", Result.InvalidArg.Code);
|
|
|
|
//Microsoft Pinyin seems does not init the result value, so we set the test value here, in case crash
|
|
acpResultStart = acpTestStart;
|
|
acpResultEnd = acpTestEnd;
|
|
}
|
|
|
|
public uint GetSelection(uint index, ref TsSelectionAcp selection)
|
|
{
|
|
//does the caller have a lock
|
|
if (!_IsLocked(TsLfFlags.Read))
|
|
{
|
|
//the caller doesn't have a lock
|
|
//return NativeMethods.TS_E_NOLOCK;
|
|
throw new COMException("", (int)TsErrors.TsENolock);
|
|
}
|
|
|
|
//check the requested index
|
|
if (-1 == (int)index)
|
|
{
|
|
index = 0;
|
|
}
|
|
else if (index > 1)
|
|
{
|
|
/*
|
|
The index is too high. This app only supports one selection.
|
|
*/
|
|
throw new COMException("", Result.InvalidArg.Code);
|
|
}
|
|
|
|
selection.AcpStart = _acpStart;
|
|
selection.AcpEnd = _acpEnd;
|
|
selection.Style.InterimCharFlag = _interimChar;
|
|
|
|
if (_interimChar)
|
|
{
|
|
/*
|
|
fInterimChar will be set when an intermediate character has been
|
|
set. One example of when this will happen is when an IME is being
|
|
used to enter characters and a character has been set, but the IME
|
|
is still active.
|
|
*/
|
|
selection.Style.Ase = TsActiveSelEnd.TsAeNone;
|
|
}
|
|
else
|
|
{
|
|
selection.Style.Ase = _activeSelectionEnd;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
public void SetSelection(uint count, ref TsSelectionAcp selections)
|
|
{
|
|
//this implementaiton only supports a single selection
|
|
if (count != 1)
|
|
throw new COMException("", Result.InvalidArg.Code);
|
|
|
|
//does the caller have a lock
|
|
if (!_IsLocked(TsLfFlags.Readwrite))
|
|
{
|
|
//the caller doesn't have a lock
|
|
//return NativeMethods.TS_E_NOLOCK;
|
|
throw new COMException("", (int)TsErrors.TsENolock);
|
|
}
|
|
|
|
_acpStart = selections.AcpStart;
|
|
_acpEnd = selections.AcpEnd;
|
|
_interimChar = selections.Style.InterimCharFlag;
|
|
|
|
if (_interimChar)
|
|
{
|
|
/*
|
|
fInterimChar will be set when an intermediate character has been
|
|
set. One example of when this will happen is when an IME is being
|
|
used to enter characters and a character has been set, but the IME
|
|
is still active.
|
|
*/
|
|
_activeSelectionEnd = TsActiveSelEnd.TsAeNone;
|
|
}
|
|
else
|
|
{
|
|
_activeSelectionEnd = selections.Style.Ase;
|
|
}
|
|
|
|
//if the selection end is at the start of the selection, reverse the parameters
|
|
int lStart = _acpStart;
|
|
int lEnd = _acpEnd;
|
|
|
|
if (TsActiveSelEnd.TsAeStart == _activeSelectionEnd)
|
|
{
|
|
lStart = _acpEnd;
|
|
lEnd = _acpStart;
|
|
}
|
|
}
|
|
|
|
|
|
public void GetText(int acpStart, int acpEnd, System.IntPtr pchPlain, uint cchPlainReq, out uint cchPlainRet,
|
|
ref TsfSharp.TsRuninfo rgRunInfo, uint cRunInfoReq, out uint cRunInfoRet, out int acpNext)
|
|
{
|
|
cchPlainRet = 0;
|
|
cRunInfoRet = 0;
|
|
acpNext = 0;
|
|
|
|
//does the caller have a lock
|
|
if (!_IsLocked(TsLfFlags.Read))
|
|
{
|
|
//the caller doesn't have a lock
|
|
throw new COMException("", (int)TsErrors.TsENolock);
|
|
}
|
|
|
|
bool fDoText = cchPlainReq > 0;
|
|
bool fDoRunInfo = cRunInfoReq > 0;
|
|
int cchTotal;
|
|
|
|
cchPlainRet = 0;
|
|
acpNext = acpStart;
|
|
|
|
cchTotal = _inputBuffer.Count;
|
|
|
|
//validate the start pos
|
|
if ((acpStart < 0) || (acpStart > cchTotal))
|
|
{
|
|
throw new COMException("", Result.InvalidArg.Code);
|
|
}
|
|
else
|
|
{
|
|
//are we at the end of the document
|
|
if (acpStart == cchTotal)
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
int cchReq;
|
|
|
|
/*
|
|
acpEnd will be -1 if all of the text up to the end is being requested.
|
|
*/
|
|
|
|
if (acpEnd >= acpStart)
|
|
{
|
|
cchReq = acpEnd - acpStart;
|
|
}
|
|
else
|
|
{
|
|
cchReq = cchTotal - acpStart;
|
|
}
|
|
|
|
if (fDoText)
|
|
{
|
|
if (cchReq > cchPlainReq)
|
|
{
|
|
cchReq = (int)cchPlainReq;
|
|
}
|
|
|
|
//extract the specified text range
|
|
if (pchPlain != IntPtr.Zero && cchPlainReq > 0)
|
|
{
|
|
//_inputBuffer.CopyTo(acpStart, pchPlain, 0, cchReq);
|
|
|
|
unsafe
|
|
{
|
|
var ptr = (char*)pchPlain;
|
|
|
|
for (int i = acpStart; i < cchReq; i++)
|
|
{
|
|
*ptr = _inputBuffer[i];
|
|
ptr++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//it is possible that only the length of the text is being requested
|
|
cchPlainRet = (uint)cchReq;
|
|
|
|
if (fDoRunInfo)
|
|
{
|
|
/*
|
|
Runs are used to separate text characters from formatting characters.
|
|
|
|
In this example, sequences inside and including the <> are treated as
|
|
control sequences and are not displayed.
|
|
|
|
Plain text = "Text formatting."
|
|
Actual text = "Text <B><I>formatting</I></B>."
|
|
|
|
If all of this text were requested, the run sequence would look like this:
|
|
|
|
prgRunInfo[0].type = TS_RT_PLAIN; //"Text "
|
|
prgRunInfo[0].uCount = 5;
|
|
|
|
prgRunInfo[1].type = TS_RT_HIDDEN; //<B><I>
|
|
prgRunInfo[1].uCount = 6;
|
|
|
|
prgRunInfo[2].type = TS_RT_PLAIN; //"formatting"
|
|
prgRunInfo[2].uCount = 10;
|
|
|
|
prgRunInfo[3].type = TS_RT_HIDDEN; //</B></I>
|
|
prgRunInfo[3].uCount = 8;
|
|
|
|
prgRunInfo[4].type = TS_RT_PLAIN; //"."
|
|
prgRunInfo[4].uCount = 1;
|
|
|
|
TS_RT_OPAQUE is used to indicate characters or character sequences
|
|
that are in the document, but are used privately by the application
|
|
and do not map to text. Runs of text tagged with TS_RT_OPAQUE should
|
|
NOT be included in the pchPlain or cchPlainOut [out] parameters.
|
|
*/
|
|
|
|
/*
|
|
This implementation is plain text, so the text only consists of one run.
|
|
If there were multiple runs, it would be an error to have consecuative runs
|
|
of the same type.
|
|
*/
|
|
rgRunInfo.Type = TsRunType.TsRtPlain;
|
|
rgRunInfo.Count = (uint)cchReq;
|
|
}
|
|
|
|
acpNext = acpStart + cchReq;
|
|
}
|
|
}
|
|
}
|
|
|
|
public TsTextchange SetText(int dwFlags, int acpStart, int acpEnd, string pchText, uint cch)
|
|
{
|
|
/*
|
|
dwFlags can be:
|
|
TS_ST_CORRECTION
|
|
*/
|
|
TsTextchange change = new TsTextchange();
|
|
|
|
//set the selection to the specified range
|
|
TsSelectionAcp tsa = new TsSelectionAcp();
|
|
tsa.AcpStart = acpStart;
|
|
tsa.AcpEnd = acpEnd;
|
|
tsa.Style.Ase = TsActiveSelEnd.TsAeStart;
|
|
tsa.Style.InterimCharFlag = false;
|
|
|
|
SetSelection(1, ref tsa);
|
|
|
|
int start, end;
|
|
InsertTextAtSelection(TsIasFlags.Noquery, pchText, cch, out start, out end, out change);
|
|
|
|
return change;
|
|
}
|
|
|
|
public IDataObject GetFormattedText(int startIndex, int endIndex)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public IUnknown GetEmbedded(int index, Guid guidService, Guid riid)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public RawBool QueryInsertEmbedded(Guid guidService, ref Formatetc formatEtc)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public TsTextchange InsertEmbedded(int flags, int startIndex, int endIndex, TsfSharp.IDataObject dataObjectRef)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public void InsertTextAtSelection(TsfSharp.TsIasFlags dwFlags, string pchText, uint cch, out int pacpStart, out int pacpEnd, out TsfSharp.TsTextchange pChange)
|
|
{
|
|
pacpStart = pacpEnd = 0;
|
|
pChange = new TsTextchange();
|
|
|
|
//does the caller have a lock
|
|
if (!_IsLocked(TsLfFlags.Readwrite))
|
|
{
|
|
//the caller doesn't have a lock
|
|
throw new COMException("", (int)TsErrors.TsENolock);
|
|
}
|
|
|
|
int acpStart;
|
|
int acpOldEnd;
|
|
int acpNewEnd;
|
|
|
|
acpOldEnd = _acpEnd;
|
|
|
|
//set the start point after the insertion
|
|
acpStart = _acpStart;
|
|
|
|
//set the end point after the insertion
|
|
acpNewEnd = _acpStart + (int)cch;
|
|
|
|
if ((dwFlags & TsIasFlags.Queryonly) == TsIasFlags.Queryonly)
|
|
{
|
|
pacpStart = acpStart;
|
|
pacpEnd = acpOldEnd;
|
|
return;
|
|
}
|
|
|
|
//insert the text
|
|
_inputBuffer.RemoveRange(acpStart, acpOldEnd - acpStart);
|
|
_inputBuffer.InsertRange(acpStart, pchText);
|
|
|
|
//set the selection
|
|
_acpStart = acpStart;
|
|
_acpEnd = acpNewEnd;
|
|
|
|
if ((dwFlags & TsIasFlags.Noquery) != TsIasFlags.Noquery)
|
|
{
|
|
pacpStart = acpStart;
|
|
pacpEnd = acpNewEnd;
|
|
}
|
|
|
|
//set the TS_TEXTCHANGE members
|
|
pChange.AcpStart = acpStart;
|
|
pChange.AcpOldEnd = acpOldEnd;
|
|
pChange.AcpNewEnd = acpNewEnd;
|
|
|
|
//defer the layout change notification until the document is unlocked
|
|
_layoutChanged = true;
|
|
}
|
|
|
|
public void InsertEmbeddedAtSelection(int flags, IDataObject obj, out int startIndex, out int endIndex, out TsTextchange change)
|
|
{
|
|
startIndex = endIndex = 0;
|
|
change = new TsTextchange();
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public void RequestSupportedAttrs(int flags, uint cFilterAttrs, ref Guid filterAttributes)
|
|
{
|
|
}
|
|
|
|
public void RequestAttrsAtPosition(int index, uint cFilterAttrs, ref Guid filterAttributes, int flags)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
|
|
public void RequestAttrsTransitioningAtPosition(int position, uint cFilterAttrs, ref Guid filterAttributes, int flags)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public void FindNextAttrTransition(int startIndex, int haltIndex, uint cFilterAttrs, ref Guid filterAttributes, int flags, out int acpNext, out RawBool found, out int foundOffset)
|
|
{
|
|
acpNext = 0;
|
|
found = false;
|
|
foundOffset = 0;
|
|
}
|
|
|
|
public uint RetrieveRequestedAttrs(uint ulCount, ref TsfSharp.TsAttrval aAttrValsRef)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public int GetEndACP()
|
|
{
|
|
int acp = 0;
|
|
//does the caller have a lock
|
|
if (!_IsLocked(TsLfFlags.Read))
|
|
{
|
|
//the caller doesn't have a lock
|
|
throw new COMException("", (int)TsErrors.TsENolock);
|
|
}
|
|
|
|
acp = _inputBuffer.Count;
|
|
|
|
return acp;
|
|
}
|
|
|
|
public int GetActiveView()
|
|
{
|
|
return _viewCookie;
|
|
}
|
|
|
|
public int GetACPFromPoint(int viewCookie, TsfSharp.Point tsfPoint, int dwFlags)
|
|
{
|
|
throw new COMException("", Result.NotImplemented.Code);
|
|
}
|
|
|
|
public void GetTextExt(int viewCookie, int acpStart, int acpEnd, out Rect rect, out RawBool clipped)
|
|
{
|
|
clipped = false;
|
|
rect = InputMethod.TextInputRect;
|
|
|
|
if (_viewCookie != viewCookie)
|
|
throw new COMException("", Result.InvalidArg.Code);
|
|
|
|
//does the caller have a lock
|
|
if (!_IsLocked(TsLfFlags.Read))
|
|
{
|
|
//the caller doesn't have a lock
|
|
throw new COMException("", (int)TsErrors.TsENolock);
|
|
}
|
|
|
|
//According to Microsoft's doc, an ime should not make empty request,
|
|
//but some ime draw comp text themseleves, when empty req will be make
|
|
//Check empty request
|
|
//if (acpStart == acpEnd) {
|
|
// return E_INVALIDARG;
|
|
//}
|
|
|
|
NativeMethods.MapWindowPoints(_windowHandle, IntPtr.Zero, ref rect, 2);
|
|
}
|
|
|
|
public Rect GetScreenExt(int viewCookie)
|
|
{
|
|
Rect rect = new Rect();
|
|
|
|
if (_viewCookie != viewCookie)
|
|
throw new COMException("", Result.InvalidArg.Code);
|
|
|
|
NativeMethods.GetWindowRect(_windowHandle, out rect);
|
|
|
|
return rect;
|
|
}
|
|
|
|
public IntPtr GetWnd(int viewCookie)
|
|
{
|
|
if (viewCookie != _viewCookie)
|
|
{
|
|
throw new COMException("", Result.False.Code);
|
|
}
|
|
|
|
return _windowHandle;
|
|
}
|
|
|
|
#endregion ITextStoreACP2
|
|
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Public Methods - ITfContextOwnerCompositionSink
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
#region ITfContextOwnerCompositionSink
|
|
|
|
public RawBool OnStartComposition(ITfCompositionView view)
|
|
{
|
|
// Return true in ok to start the composition.
|
|
RawBool ok = true;
|
|
_compositionStart = _compositionLength = 0;
|
|
_currentComposition.Clear();
|
|
|
|
InputMethod.OnTextCompositionStarted(this);
|
|
_compViews.Add(view);
|
|
|
|
return ok;
|
|
}
|
|
|
|
public void OnUpdateComposition(ITfCompositionView view, ITfRange rangeNew)
|
|
{
|
|
var range = view.Range;
|
|
var rangeacp = range.QueryInterface<ITfRangeACP>();
|
|
|
|
rangeacp.GetExtent(out _compositionStart, out _compositionLength);
|
|
rangeacp.Dispose();
|
|
range.Dispose();
|
|
_compViews.Add(view);
|
|
}
|
|
|
|
public void OnEndComposition(ITfCompositionView view)
|
|
{
|
|
var range = view.Range;
|
|
var rangeacp = range.QueryInterface<ITfRangeACP>();
|
|
|
|
rangeacp.GetExtent(out _commitStart, out _commitLength);
|
|
rangeacp.Dispose();
|
|
range.Dispose();
|
|
|
|
// Ensure composition string reset
|
|
_compositionStart = _compositionLength = 0;
|
|
_currentComposition.Clear();
|
|
|
|
InputMethod.ClearCandidates();
|
|
InputMethod.OnTextCompositionEnded(this);
|
|
view.Dispose();
|
|
foreach(var item in _compViews)
|
|
item.Dispose();
|
|
_compViews.Clear();
|
|
}
|
|
|
|
#endregion ITfContextOwnerCompositionSink
|
|
|
|
#region ITfTextEditSink
|
|
|
|
public void OnEndEdit(ITfContext context, int ecReadOnly, ITfEditRecord editRecord)
|
|
{
|
|
ITfProperty property = context.GetProperty(GUID_PROP_COMPOSING);
|
|
|
|
ITfRangeACP rangeACP = TextServicesContext.Current.ContextOwnerServices.CreateRange(_compositionStart, _compositionStart + _compositionLength);
|
|
Variant val = property.GetValue(ecReadOnly, rangeACP);
|
|
property.Dispose();
|
|
rangeACP.Dispose();
|
|
if (val.Value == null || (int)val.Value == 0)
|
|
{
|
|
if (_commitLength == 0 || _inputBuffer.Count == 0)
|
|
return;
|
|
|
|
//Debug.WriteLine("Composition result: {0}", new object[] { new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray()) });
|
|
|
|
_commited = true;
|
|
for (int i = 0; i < _commitLength; i++)
|
|
InputMethod.OnTextCompositionResult(this, new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray()));
|
|
}
|
|
|
|
if (_commited)
|
|
return;
|
|
|
|
if (_inputBuffer.Count == 0 && _compositionLength > 0) // Composition just ended
|
|
return;
|
|
|
|
_currentComposition.Clear();
|
|
for (int i = 0; i < _compositionLength; i++)
|
|
_currentComposition.Add(_inputBuffer[_compositionStart + i]);
|
|
|
|
InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd);
|
|
|
|
//var compStr = new string(_currentComposition.ToArray());
|
|
//compStr = compStr.Insert(_acpEnd, "|");
|
|
//Debug.WriteLine("Composition string: {0}, cursor pos: {1}", compStr, _acpEnd);
|
|
}
|
|
|
|
#endregion ITfTextEditSink
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Public Methods - ITfUIElementSink
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
#region ITfUIElementSink
|
|
|
|
public RawBool BeginUIElement(int dwUIElementId)
|
|
{
|
|
// Hide OS rendered Candidate list Window
|
|
RawBool pbShow = InputMethod.ShowOSImeWindow;
|
|
|
|
OnUIElement(dwUIElementId, true);
|
|
|
|
return pbShow;
|
|
}
|
|
|
|
public void UpdateUIElement(int dwUIElementId)
|
|
{
|
|
OnUIElement(dwUIElementId, false);
|
|
}
|
|
|
|
public void EndUIElement(int dwUIElementId)
|
|
{
|
|
}
|
|
|
|
private void OnUIElement(int uiElementId, bool onStart)
|
|
{
|
|
if (InputMethod.ShowOSImeWindow || !_supportUIElement) return;
|
|
|
|
ITfUIElement uiElement = TextServicesContext.Current.UIElementMgr.GetUIElement(uiElementId);
|
|
|
|
ITfCandidateListUIElementBehavior candList;
|
|
|
|
try
|
|
{
|
|
candList = uiElement.QueryInterface<ITfCandidateListUIElementBehavior>();
|
|
}
|
|
catch (SharpGenException)
|
|
{
|
|
_supportUIElement = false;
|
|
return;
|
|
}
|
|
finally
|
|
{
|
|
uiElement.Dispose();
|
|
}
|
|
|
|
uint selection = 0;
|
|
uint currentPage = 0;
|
|
uint count = 0;
|
|
uint pageCount = 0;
|
|
uint pageStart = 0;
|
|
uint pageSize = 0;
|
|
uint i, j;
|
|
|
|
selection = candList.GetSelection();
|
|
currentPage = candList.GetCurrentPage();
|
|
|
|
count = candList.GetCount();
|
|
|
|
pageCount = candList.GetPageIndex(null, 0);
|
|
|
|
if (pageCount > 0)
|
|
{
|
|
uint[] pageStartIndexes = ArrayPool<uint>.Shared.Rent((int)pageCount);
|
|
pageCount = candList.GetPageIndex(pageStartIndexes, pageCount);
|
|
pageStart = pageStartIndexes[currentPage];
|
|
|
|
if (pageStart >= count - 1)
|
|
{
|
|
candList.Abort();
|
|
ArrayPool<uint>.Shared.Return(pageStartIndexes);
|
|
return;
|
|
}
|
|
|
|
if (currentPage < pageCount - 1)
|
|
pageSize = Math.Min(count, pageStartIndexes[currentPage + 1]) - pageStart;
|
|
else
|
|
pageSize = count - pageStart;
|
|
|
|
ArrayPool<uint>.Shared.Return(pageStartIndexes);
|
|
}
|
|
|
|
selection -= pageStart;
|
|
|
|
IMEString[] candidates = _IMEStringPool.Rent((int)pageSize);
|
|
|
|
IntPtr bStrPtr;
|
|
for (i = pageStart, j = 0; i < count && j < pageSize; i++, j++)
|
|
{
|
|
bStrPtr = candList.GetString(i);
|
|
candidates[j] = new IMEString(bStrPtr);
|
|
}
|
|
|
|
//Debug.WriteLine("TSF========TSF");
|
|
//Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, currentPage: {3} candidates:", pageStart, pageSize, selection, currentPage);
|
|
//for (int k = 0; k < candidates.Length; k++)
|
|
// Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : "");
|
|
//Debug.WriteLine("TSF++++++++TSF");
|
|
|
|
InputMethod.CandidatePageStart = (int)pageStart;
|
|
InputMethod.CandidatePageSize = (int)pageSize;
|
|
InputMethod.CandidateSelection = (int)selection;
|
|
InputMethod.CandidateList = candidates;
|
|
|
|
if (_currentComposition != null)
|
|
{
|
|
InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd);
|
|
_IMEStringPool.Return(candidates);
|
|
}
|
|
|
|
candList.Dispose();
|
|
}
|
|
|
|
#endregion ITfUIElementSink
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Public Properties
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
public static TextStore Current
|
|
{
|
|
get
|
|
{
|
|
TextStore defaultTextStore = InputMethod.DefaultTextStore;
|
|
if (defaultTextStore == null)
|
|
{
|
|
defaultTextStore = InputMethod.DefaultTextStore = new TextStore(InputMethod.WindowHandle);
|
|
|
|
defaultTextStore.Register();
|
|
}
|
|
|
|
return defaultTextStore;
|
|
}
|
|
}
|
|
|
|
public ITfDocumentMgr DocumentManager
|
|
{
|
|
get { return _documentMgr; }
|
|
set { _documentMgr = value; }
|
|
}
|
|
|
|
// EditCookie for ITfContext.
|
|
public int EditCookie
|
|
{
|
|
// get { return _editCookie; }
|
|
set { _editCookie = value; }
|
|
}
|
|
|
|
public int UIElementSinkCookie
|
|
{
|
|
get { return _uiElementSinkCookie; }
|
|
set { _uiElementSinkCookie = value; }
|
|
}
|
|
|
|
public int TextEditSinkCookie
|
|
{
|
|
get { return _textEditSinkCookie; }
|
|
set { _textEditSinkCookie = value; }
|
|
}
|
|
|
|
public bool SupportUIElement { get { return _supportUIElement; } }
|
|
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Private Methods
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
// This function calls TextServicesContext to create TSF document and start transitory extension.
|
|
private void Register()
|
|
{
|
|
// Create TSF document and advise the sink to it.
|
|
TextServicesContext.Current.RegisterTextStore(this);
|
|
}
|
|
|
|
//------------------------------------------------------
|
|
//
|
|
// Private Fields
|
|
//
|
|
//------------------------------------------------------
|
|
|
|
// The TSF document object. This is a native resource.
|
|
private ITfDocumentMgr _documentMgr;
|
|
|
|
private int _viewCookie;
|
|
|
|
// The edit cookie TSF returns from CreateContext.
|
|
private int _editCookie;
|
|
private int _uiElementSinkCookie;
|
|
private int _textEditSinkCookie;
|
|
|
|
private ITextStoreACPSink _sink;
|
|
private IntPtr _windowHandle;
|
|
private int _acpStart;
|
|
private int _acpEnd;
|
|
private bool _interimChar;
|
|
private TsActiveSelEnd _activeSelectionEnd;
|
|
private List<char> _inputBuffer = new List<char>();
|
|
|
|
private bool _locked;
|
|
private TsLfFlags _lockFlags;
|
|
private Queue<TsLfFlags> _lockRequestQueue = new Queue<TsLfFlags>();
|
|
private bool _layoutChanged;
|
|
|
|
private List<char> _currentComposition = new List<char>();
|
|
private int _compositionStart;
|
|
private int _compositionLength;
|
|
private int _commitStart;
|
|
private int _commitLength;
|
|
private bool _commited;
|
|
|
|
private bool _supportUIElement = true;
|
|
private List<ITfCompositionView> _compViews = new List<ITfCompositionView>();
|
|
|
|
private ArrayPool<IMEString> _IMEStringPool;
|
|
|
|
}
|
|
}
|