原「智能标点」是小狼毫 digit_separators 的语义:字母或数字后的 , . : 直出英文, 2 秒内再按同一标点转回中文——后半段靠 SendInput 往应用里补一个 Backspace 再打 字符,落点全靠光标当时在哪。这次换掉主路径:输入中文标点后按空格,用 TSF 编辑 会话就地把它改写成对应英文(。,!?;:、 → . , ! ? ; : /),2 秒内再按同一标点 键撤回中文;原来的直出降为子开关,整个家族默认全关、改为显式开启。 - 按键与会话:转换/撤回是本地文档改写,不进 IPC、不进延迟队列;判据落在 _IsKeyEaten,OnTestKeyDown 的预判与 _DispatchKeyDown 的第二遍预判共用同一份, 声称吃掉的键不交还宿主。改写走编辑会话,请求不上时另起一个最小会话把键本身 的字符写回去,避免已吃掉的键静默丢失。 - 状态:原来散落的九个成员收成一个 SmartPunctuationAction(None / ChineseCommitted / AsciiConverted),带焦点令牌、前台窗口和 beforeChar 指纹;清理以分类结果为准, 丢焦点与上下文栈变化一并清掉。 - 转换范围:。,!?;:、 与单独出现的 “”‘’【】《》() 都是映射表里的单字符 条目,路径完全一致;成对补全产生的一对(autoClosedPair)不 arm,按空格只落空格, 不会留下 <》 这类孤儿。 - 开关:总开关 + 中文标点后按空格转换 + 同键转回中文 + 数字后直出 + 字母后直出, 五键全部默认关;直出拆成数字/字母两个独立子开关(IPC 24 收窄为数字、25 追加为 字母),TSF 按前字符类型分别查键。 - 命名:子开关「独立符号」改名「中文标点后按空格转换」,配置键 smart_punctuation_standalone → smart_punctuation_space_convert,IPC 22 只改常量名 (SmartPunctuationSpaceConvertChanged),编号与语义不变、新旧测试包互通;设置页 文案同步重写,五处配置链路与死宿主一并跟改。 - 兼容:旧键未出厂、不迁移(升级合并按模板缺键丢弃、取默认 false);编号 23 随 「按空格整体转换一对」的设计退役并留空不复用。 - 验证:TSF x86/x64 与 Server Release 编译、Server 335 单测、windows_ipc_contract x64/x86、设置页 build + 27 测试、clang-format 18.1.8 全绿。真机未跑。 Signed-off-by: Jia Sui <jsfaint@gmail.com>
1557 lines
49 KiB
C++
1557 lines
49 KiB
C++
#include "Private.h"
|
||
#include "Globals.h"
|
||
#include "MetasequoiaIME.h"
|
||
#include "CompositionProcessorEngine.h"
|
||
#include <cwctype>
|
||
#include <debugapi.h>
|
||
#include <fmt/xchar.h>
|
||
#include <string>
|
||
#include "FanyDefines.h"
|
||
#include "Ipc.h"
|
||
|
||
namespace
|
||
{
|
||
// Keep SEH helpers free of C++ objects with destructors (C2712).
|
||
HRESULT SafeRangeSetText(_In_ ITfRange *range, TfEditCookie ec, DWORD flags, _In_reads_opt_(len) const WCHAR *text,
|
||
LONG len)
|
||
{
|
||
if (range == nullptr)
|
||
{
|
||
return E_INVALIDARG;
|
||
}
|
||
|
||
__try
|
||
{
|
||
return range->SetText(ec, flags, text, len);
|
||
}
|
||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||
{
|
||
return E_FAIL;
|
||
}
|
||
}
|
||
|
||
HRESULT SafeRangeGetText(_In_ ITfRange *range, TfEditCookie ec, DWORD flags, _Out_writes_(len) WCHAR *text, ULONG len,
|
||
_Out_ ULONG *fetched)
|
||
{
|
||
if (range == nullptr || text == nullptr || fetched == nullptr || len == 0)
|
||
{
|
||
return E_INVALIDARG;
|
||
}
|
||
|
||
__try
|
||
{
|
||
return range->GetText(ec, flags, text, len, fetched);
|
||
}
|
||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||
{
|
||
*fetched = 0;
|
||
return E_FAIL;
|
||
}
|
||
}
|
||
|
||
HRESULT SafeRangeShiftStart(_In_ ITfRange *range, TfEditCookie ec, LONG count, _Out_ LONG *shifted)
|
||
{
|
||
if (range == nullptr || shifted == nullptr)
|
||
{
|
||
return E_INVALIDARG;
|
||
}
|
||
|
||
__try
|
||
{
|
||
return range->ShiftStart(ec, count, shifted, nullptr);
|
||
}
|
||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||
{
|
||
*shifted = 0;
|
||
return E_FAIL;
|
||
}
|
||
}
|
||
|
||
HRESULT SafeRangeShiftEnd(_In_ ITfRange *range, TfEditCookie ec, LONG count, _Out_ LONG *shifted)
|
||
{
|
||
if (range == nullptr || shifted == nullptr)
|
||
{
|
||
return E_INVALIDARG;
|
||
}
|
||
|
||
__try
|
||
{
|
||
return range->ShiftEnd(ec, count, shifted, nullptr);
|
||
}
|
||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||
{
|
||
*shifted = 0;
|
||
return E_FAIL;
|
||
}
|
||
}
|
||
|
||
// Places the caret after a smart-punctuation replacement. SetText leaves the
|
||
// range covering the replaced text, so walking one character from its start
|
||
// lands after the single ASCII mark, or between the two halves of a converted
|
||
// pair: both forms want the caret after one character of replacement text.
|
||
void PlaceSmartPunctuationCaret(TfEditCookie ec, _In_ ITfContext *pContext, _In_ ITfRange *range)
|
||
{
|
||
if (range == nullptr || pContext == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
if (FAILED(range->Collapse(ec, TF_ANCHOR_START)))
|
||
{
|
||
return;
|
||
}
|
||
LONG shifted = 0;
|
||
if (FAILED(SafeRangeShiftEnd(range, ec, 1, &shifted)) || shifted != 1)
|
||
{
|
||
return;
|
||
}
|
||
if (FAILED(range->Collapse(ec, TF_ANCHOR_END)))
|
||
{
|
||
return;
|
||
}
|
||
TF_SELECTION selection = {};
|
||
selection.range = range;
|
||
selection.style.ase = TF_AE_NONE;
|
||
selection.style.fInterimChar = FALSE;
|
||
pContext->SetSelection(ec, 1, &selection);
|
||
}
|
||
|
||
bool AreCaretModifiersPhysicallyDown()
|
||
{
|
||
// VK_LWIN/VK_RWIN matter as much as Shift here: Win+Left is the window snap
|
||
// shortcut, so an arrow released into a held Win chord rearranges the
|
||
// desktop instead of moving the caret.
|
||
static const int keys[] = {VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN};
|
||
for (int key : keys)
|
||
{
|
||
if ((GetAsyncKeyState(key) & 0x8000) != 0)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
} // namespace
|
||
|
||
WCHAR CMetasequoiaIME::_GetPrecedingDocumentChar(TfEditCookie ec, _In_ ITfContext *pContext)
|
||
{
|
||
if (pContext == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
ITfRange *pAnchor = nullptr;
|
||
bool releaseAnchor = false;
|
||
|
||
if (_IsComposing() && _pComposition != nullptr)
|
||
{
|
||
if (FAILED(_pComposition->GetRange(&pAnchor)) || pAnchor == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
releaseAnchor = true;
|
||
}
|
||
else
|
||
{
|
||
TF_SELECTION tfSelection = {};
|
||
ULONG fetched = 0;
|
||
const HRESULT hr = pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched);
|
||
if (FAILED(hr) || fetched != 1 || tfSelection.range == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
pAnchor = tfSelection.range;
|
||
releaseAnchor = true;
|
||
}
|
||
|
||
ITfRange *pClone = nullptr;
|
||
WCHAR preceding = 0;
|
||
HRESULT hr = pAnchor->Clone(&pClone);
|
||
if (SUCCEEDED(hr) && pClone != nullptr)
|
||
{
|
||
hr = pClone->Collapse(ec, TF_ANCHOR_START);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
LONG shifted = 0;
|
||
hr = SafeRangeShiftStart(pClone, ec, -1, &shifted);
|
||
if (SUCCEEDED(hr) && shifted == -1)
|
||
{
|
||
// Terminals and other shallow text stores accept the shift but
|
||
// expose no text, leaving preceding at 0.
|
||
WCHAR buffer[2] = {};
|
||
ULONG fetched = 0;
|
||
hr = SafeRangeGetText(pClone, ec, 0, buffer, 1, &fetched);
|
||
if (SUCCEEDED(hr) && fetched == 1)
|
||
{
|
||
preceding = buffer[0];
|
||
}
|
||
}
|
||
}
|
||
pClone->Release();
|
||
}
|
||
|
||
if (releaseAnchor && pAnchor != nullptr)
|
||
{
|
||
pAnchor->Release();
|
||
}
|
||
return preceding;
|
||
}
|
||
|
||
int CMetasequoiaIME::_GetPrecedingDocumentChars(TfEditCookie ec, _In_ ITfContext *pContext,
|
||
_Out_writes_(count) WCHAR *buffer, int count)
|
||
{
|
||
if (pContext == nullptr || buffer == nullptr || count <= 0)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
ITfRange *pAnchor = nullptr;
|
||
bool releaseAnchor = false;
|
||
|
||
if (_IsComposing() && _pComposition != nullptr)
|
||
{
|
||
if (FAILED(_pComposition->GetRange(&pAnchor)) || pAnchor == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
releaseAnchor = true;
|
||
}
|
||
else
|
||
{
|
||
TF_SELECTION tfSelection = {};
|
||
ULONG fetched = 0;
|
||
const HRESULT hr = pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched);
|
||
if (FAILED(hr) || fetched != 1 || tfSelection.range == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
pAnchor = tfSelection.range;
|
||
releaseAnchor = true;
|
||
}
|
||
|
||
ITfRange *pClone = nullptr;
|
||
int readCount = 0;
|
||
HRESULT hr = pAnchor->Clone(&pClone);
|
||
if (SUCCEEDED(hr) && pClone != nullptr)
|
||
{
|
||
hr = pClone->Collapse(ec, TF_ANCHOR_START);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
LONG shifted = 0;
|
||
hr = SafeRangeShiftStart(pClone, ec, -count, &shifted);
|
||
if (SUCCEEDED(hr) && shifted < 0)
|
||
{
|
||
// Terminals and other shallow text stores accept the shift but
|
||
// expose no text, which reads back as 0 characters.
|
||
ULONG fetched = 0;
|
||
if (SUCCEEDED(SafeRangeGetText(pClone, ec, 0, buffer, static_cast<ULONG>(count), &fetched)))
|
||
{
|
||
readCount = static_cast<int>(fetched);
|
||
}
|
||
}
|
||
}
|
||
pClone->Release();
|
||
}
|
||
|
||
if (releaseAnchor && pAnchor != nullptr)
|
||
{
|
||
pAnchor->Release();
|
||
}
|
||
return readCount;
|
||
}
|
||
|
||
WCHAR CMetasequoiaIME::_GetFollowingDocumentChar(TfEditCookie ec, _In_ ITfContext *pContext)
|
||
{
|
||
if (pContext == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
TF_SELECTION tfSelection = {};
|
||
ULONG fetched = 0;
|
||
if (FAILED(pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched)) || fetched != 1 ||
|
||
tfSelection.range == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
ITfRange *pClone = nullptr;
|
||
WCHAR following = 0;
|
||
HRESULT hr = tfSelection.range->Clone(&pClone);
|
||
if (SUCCEEDED(hr) && pClone != nullptr)
|
||
{
|
||
hr = pClone->Collapse(ec, TF_ANCHOR_END);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
LONG shifted = 0;
|
||
hr = SafeRangeShiftEnd(pClone, ec, 1, &shifted);
|
||
if (SUCCEEDED(hr) && shifted == 1)
|
||
{
|
||
// Terminals and other shallow text stores accept the shift but
|
||
// expose no text, leaving following at 0.
|
||
WCHAR buffer[2] = {};
|
||
ULONG got = 0;
|
||
hr = SafeRangeGetText(pClone, ec, 0, buffer, 1, &got);
|
||
if (SUCCEEDED(hr) && got == 1)
|
||
{
|
||
following = buffer[0];
|
||
}
|
||
}
|
||
}
|
||
pClone->Release();
|
||
}
|
||
|
||
tfSelection.range->Release();
|
||
return following;
|
||
}
|
||
|
||
WCHAR CMetasequoiaIME::_GetPrecedingCharForSmartPunctuation(TfEditCookie ec, _In_ ITfContext *pContext)
|
||
{
|
||
if (_smartPunctuationShadowValid)
|
||
{
|
||
return _smartPunctuationShadowChar;
|
||
}
|
||
return _GetPrecedingDocumentChar(ec, pContext);
|
||
}
|
||
|
||
bool CMetasequoiaIME::_SmartPunctuationFingerprintMatches(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR beforeChar)
|
||
{
|
||
if (beforeChar == 0)
|
||
{
|
||
// Nothing was recorded at commit time, so there is no fingerprint to
|
||
// check against.
|
||
return true;
|
||
}
|
||
|
||
// Read the two characters left of the caret: the punctuation itself and,
|
||
// before it, the character this check compares. A shallow text store
|
||
// accepts the shift but reads back nothing, and there the pending state is
|
||
// the only evidence available.
|
||
WCHAR buffer[2] = {};
|
||
const int readCount = _GetPrecedingDocumentChars(ec, pContext, buffer, 2);
|
||
if (readCount == 0)
|
||
{
|
||
return true;
|
||
}
|
||
if (readCount == 1)
|
||
{
|
||
// Only the punctuation itself exists at the document start; anything
|
||
// else means the document moved under the action.
|
||
return false;
|
||
}
|
||
return buffer[0] == beforeChar;
|
||
}
|
||
|
||
WCHAR CMetasequoiaIME::_GetPairedPunctuationClosingFor(WCHAR opening)
|
||
{
|
||
switch (opening)
|
||
{
|
||
case L'“':
|
||
return L'”';
|
||
case L'‘':
|
||
return L'’';
|
||
case L'【':
|
||
return L'】';
|
||
case L'{':
|
||
return L'}';
|
||
case L'《':
|
||
return L'》';
|
||
case L'〈':
|
||
return L'〉';
|
||
case L'(':
|
||
return L')';
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
void CMetasequoiaIME::_PushPairedPunctuation(WCHAR opening, WCHAR closing)
|
||
{
|
||
if (opening == 0 || closing == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Deep nesting is never legitimate here; drop the outermost entry rather
|
||
// than let a host that swallows our bookkeeping keys grow the stack.
|
||
if (_pairedPunctuationStack.size() >= PAIRED_PUNCTUATION_MAX_DEPTH)
|
||
{
|
||
_pairedPunctuationStack.erase(_pairedPunctuationStack.begin());
|
||
}
|
||
|
||
PairedPunctuationEntry entry;
|
||
entry.opening = opening;
|
||
entry.closing = closing;
|
||
entry.focusToken = _CaptureFocusSessionToken();
|
||
_pairedPunctuationStack.push_back(entry);
|
||
}
|
||
|
||
void CMetasequoiaIME::_ClearPairedPunctuationStack()
|
||
{
|
||
_pairedPunctuationStack.clear();
|
||
}
|
||
|
||
bool CMetasequoiaIME::_TryStepOverPairedPunctuation(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR closing)
|
||
{
|
||
if (closing == 0 || _pairedPunctuationStack.empty())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const PairedPunctuationEntry top = _pairedPunctuationStack.back();
|
||
if (top.closing != closing || !_IsFocusSessionCurrent(top.focusToken, pContext))
|
||
{
|
||
_ClearPairedPunctuationStack();
|
||
return false;
|
||
}
|
||
|
||
// A mouse click or a host-side edit moves the caret without producing any
|
||
// key event, so the stack alone cannot prove the closing half is still on
|
||
// the right. Confirm against the document; hosts whose text store exposes
|
||
// nothing read back 0, and there the stack is the only evidence available
|
||
// and is trusted, exactly as the smart-punctuation shadow does.
|
||
const WCHAR following = _GetFollowingDocumentChar(ec, pContext);
|
||
if (following != 0 && following != closing)
|
||
{
|
||
_ClearPairedPunctuationStack();
|
||
return false;
|
||
}
|
||
|
||
_pairedPunctuationStack.pop_back();
|
||
// Nothing is committed on this path, so any pending smart-punctuation
|
||
// action now describes a commit the user has stepped past.
|
||
_ClearSmartPunctuationAction();
|
||
_InvalidateSmartPunctuationShadow();
|
||
_QueuePairedPunctuationCaretMove(1);
|
||
return true;
|
||
}
|
||
|
||
void CMetasequoiaIME::_NoteKeyForPairedPunctuation(UINT code)
|
||
{
|
||
if (_pairedPunctuationStack.empty() && _pendingPairedCaretDelta == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
switch (code)
|
||
{
|
||
case VK_SHIFT:
|
||
case VK_LSHIFT:
|
||
case VK_RSHIFT:
|
||
case VK_CONTROL:
|
||
case VK_LCONTROL:
|
||
case VK_RCONTROL:
|
||
case VK_MENU:
|
||
case VK_LMENU:
|
||
case VK_RMENU:
|
||
case VK_CAPITAL:
|
||
// Modifier presses edit nothing, so the tracked pairs still hold.
|
||
return;
|
||
case VK_BACK:
|
||
case VK_DELETE:
|
||
case VK_INSERT:
|
||
case VK_RETURN:
|
||
case VK_TAB:
|
||
case VK_ESCAPE:
|
||
case VK_LEFT:
|
||
case VK_RIGHT:
|
||
case VK_UP:
|
||
case VK_DOWN:
|
||
case VK_HOME:
|
||
case VK_END:
|
||
case VK_PRIOR:
|
||
case VK_NEXT:
|
||
// Anything that moves the caret or deletes text breaks the invariant
|
||
// that the closing half sits immediately to the right of the caret. A
|
||
// still-deferred caret move was computed against that invariant too, so
|
||
// it must not land on whatever the user has just done instead.
|
||
_ClearPairedPunctuationStack();
|
||
_CancelPairedPunctuationCaretMove();
|
||
return;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// Ordinary text insertion happens at the caret and keeps the closing half
|
||
// on its right, so the stack survives it. A focus change does not.
|
||
if (!_pairedPunctuationStack.empty() && !_IsFocusSessionCurrent(_pairedPunctuationStack.back().focusToken))
|
||
{
|
||
_ClearPairedPunctuationStack();
|
||
}
|
||
}
|
||
|
||
void CMetasequoiaIME::_CancelPairedPunctuationCaretMove()
|
||
{
|
||
if (_pairedCaretRetryTimerActive && _msgWndHandle != nullptr)
|
||
{
|
||
KillTimer(_msgWndHandle, TIMER_PAIRED_PUNCTUATION_CARET);
|
||
}
|
||
_pairedCaretRetryTimerActive = false;
|
||
_pendingPairedCaretDelta = 0;
|
||
_pendingPairedCaretFocusToken = 0;
|
||
_pendingPairedCaretDeadline = 0;
|
||
}
|
||
|
||
void CMetasequoiaIME::_QueuePairedPunctuationCaretMove(int delta)
|
||
{
|
||
if (delta == 0 || _msgWndHandle == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const uint64_t focusToken = _CaptureFocusSessionToken();
|
||
if (focusToken == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// A still-pending move belongs to the same burst only while the focus
|
||
// session matches; otherwise it is stale and its steps must not be added.
|
||
if (_pendingPairedCaretDelta != 0 && _pendingPairedCaretFocusToken == focusToken)
|
||
{
|
||
delta += _pendingPairedCaretDelta;
|
||
}
|
||
|
||
delta = max(-PAIRED_PUNCTUATION_CARET_MAX_STEPS, min(PAIRED_PUNCTUATION_CARET_MAX_STEPS, delta));
|
||
if (delta == 0)
|
||
{
|
||
_CancelPairedPunctuationCaretMove();
|
||
return;
|
||
}
|
||
|
||
_pendingPairedCaretDelta = delta;
|
||
_pendingPairedCaretFocusToken = focusToken;
|
||
_pendingPairedCaretDeadline = GetTickCount64() + PAIRED_PUNCTUATION_CARET_TIMEOUT_MS;
|
||
|
||
if (!PostMessage(_msgWndHandle, WM_PairedPunctuationCaretMove, static_cast<WPARAM>(focusToken & 0xFFFFFFFFULL),
|
||
static_cast<LPARAM>((focusToken >> 32) & 0xFFFFFFFFULL)))
|
||
{
|
||
_CancelPairedPunctuationCaretMove();
|
||
}
|
||
}
|
||
|
||
void CMetasequoiaIME::_RunPairedPunctuationCaretMove()
|
||
{
|
||
if (_pendingPairedCaretDelta == 0)
|
||
{
|
||
_CancelPairedPunctuationCaretMove();
|
||
return;
|
||
}
|
||
|
||
if (!_IsFocusSessionCurrent(_pendingPairedCaretFocusToken) || GetTickCount64() > _pendingPairedCaretDeadline)
|
||
{
|
||
// The document this move was computed against is gone, or the chord was
|
||
// held long enough that the caret is no longer where we left it.
|
||
_ClearPairedPunctuationStack();
|
||
_CancelPairedPunctuationCaretMove();
|
||
return;
|
||
}
|
||
|
||
if (AreCaretModifiersPhysicallyDown())
|
||
{
|
||
if (!_pairedCaretRetryTimerActive && _msgWndHandle != nullptr)
|
||
{
|
||
_pairedCaretRetryTimerActive = SetTimer(_msgWndHandle, TIMER_PAIRED_PUNCTUATION_CARET,
|
||
PAIRED_PUNCTUATION_CARET_RETRY_MS, nullptr) != 0;
|
||
if (!_pairedCaretRetryTimerActive)
|
||
{
|
||
_ClearPairedPunctuationStack();
|
||
_CancelPairedPunctuationCaretMove();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
const int delta = _pendingPairedCaretDelta;
|
||
const WORD vk = delta < 0 ? VK_LEFT : VK_RIGHT;
|
||
const int steps = delta < 0 ? -delta : delta;
|
||
|
||
INPUT inputs[PAIRED_PUNCTUATION_CARET_MAX_STEPS * 2] = {};
|
||
for (int i = 0; i < steps; ++i)
|
||
{
|
||
inputs[i * 2].type = INPUT_KEYBOARD;
|
||
inputs[i * 2].ki.wVk = vk;
|
||
inputs[i * 2].ki.dwExtraInfo = PAIRED_PUNCTUATION_SENDINPUT_EXTRA_INFO;
|
||
inputs[i * 2 + 1] = inputs[i * 2];
|
||
inputs[i * 2 + 1].ki.dwFlags = KEYEVENTF_KEYUP;
|
||
}
|
||
|
||
_CancelPairedPunctuationCaretMove();
|
||
if (SendInput(static_cast<UINT>(steps * 2), inputs, sizeof(INPUT)) != static_cast<UINT>(steps * 2))
|
||
{
|
||
// The caret is no longer provably between the halves.
|
||
_ClearPairedPunctuationStack();
|
||
}
|
||
_InvalidateSmartPunctuationShadow();
|
||
}
|
||
|
||
void CMetasequoiaIME::_ClearSmartPunctuationAction()
|
||
{
|
||
_smartPunctuationAction = {};
|
||
}
|
||
|
||
bool CMetasequoiaIME::_CanInterceptSmartPunctuationConvert()
|
||
{
|
||
const SmartPunctuationAction &state = _smartPunctuationAction;
|
||
if (state.kind != SmartPunctuationAction::Kind::ChineseCommitted ||
|
||
!Global::SmartPunctuationEnabled.load(std::memory_order_relaxed))
|
||
{
|
||
return false;
|
||
}
|
||
if (!Global::SmartPunctuationSpaceConvertEnabled.load(std::memory_order_relaxed))
|
||
{
|
||
return false;
|
||
}
|
||
// No time window: the state only survives until the next key, focus change
|
||
// or foreground change, which is already narrower than an input-stream
|
||
// burst, and a clock would cut off a slow but deliberate space.
|
||
return _IsFocusSessionCurrent(state.focusToken) && GetForegroundWindow() == state.foregroundWindow;
|
||
}
|
||
|
||
bool CMetasequoiaIME::_CanInterceptSmartPunctuationRevert(WCHAR wch)
|
||
{
|
||
const SmartPunctuationAction &state = _smartPunctuationAction;
|
||
if (state.kind != SmartPunctuationAction::Kind::AsciiConverted || wch == 0 || wch != state.triggerKey ||
|
||
!Global::SmartPunctuationEnabled.load(std::memory_order_relaxed) ||
|
||
!Global::SmartPunctuationRepeatToChineseEnabled.load(std::memory_order_relaxed))
|
||
{
|
||
return false;
|
||
}
|
||
if (state.tick == 0 || GetTickCount64() - state.tick > SMART_PUNCTUATION_REPEAT_INTERVAL_MS)
|
||
{
|
||
return false;
|
||
}
|
||
return _IsFocusSessionCurrent(state.focusToken) && GetForegroundWindow() == state.foregroundWindow;
|
||
}
|
||
|
||
void CMetasequoiaIME::_WriteDirectSmartPunctuationState(WCHAR triggerKey, WCHAR chinese, WCHAR ascii, WCHAR beforeChar)
|
||
{
|
||
_smartPunctuationAction = {};
|
||
_smartPunctuationAction.kind = SmartPunctuationAction::Kind::AsciiConverted;
|
||
_smartPunctuationAction.triggerKey = triggerKey;
|
||
_smartPunctuationAction.chineseLeft = chinese;
|
||
_smartPunctuationAction.asciiLeft = ascii;
|
||
_smartPunctuationAction.beforeChar = beforeChar;
|
||
_smartPunctuationAction.tick = GetTickCount64();
|
||
_smartPunctuationAction.focusToken = _CaptureFocusSessionToken();
|
||
_smartPunctuationAction.foregroundWindow = GetForegroundWindow();
|
||
}
|
||
|
||
void CMetasequoiaIME::_NoteCommittedChinesePunctuation(const std::wstring &committedText, bool autoClosedPair,
|
||
WCHAR beforeChar)
|
||
{
|
||
if (committedText.empty() || !Global::SmartPunctuationEnabled.load(std::memory_order_relaxed))
|
||
{
|
||
// The feature being off at commit time must not arm a conversion that
|
||
// a later re-enable would consume.
|
||
return;
|
||
}
|
||
if (autoClosedPair)
|
||
{
|
||
// An auto-completed pair is out of scope on purpose. The two halves sit
|
||
// on either side of the caret, so rewriting just the left one would
|
||
// leave the right one orphaned (〔《>〕). The space inserts normally.
|
||
return;
|
||
}
|
||
const WCHAR tail = committedText.back();
|
||
if (!CCompositionProcessorEngine::IsSmartPunctuationChinese(tail))
|
||
{
|
||
// ASCII commits (direct output, numpad '.') keep whatever
|
||
// _ResolveSmartPunctuation recorded so the same key can revert them.
|
||
return;
|
||
}
|
||
|
||
_smartPunctuationAction = {};
|
||
_smartPunctuationAction.kind = SmartPunctuationAction::Kind::ChineseCommitted;
|
||
_smartPunctuationAction.chineseLeft = tail;
|
||
_smartPunctuationAction.triggerKey = CCompositionProcessorEngine::GetSmartPunctuationAscii(tail);
|
||
_smartPunctuationAction.beforeChar = beforeChar;
|
||
_smartPunctuationAction.tick = GetTickCount64();
|
||
_smartPunctuationAction.focusToken = _CaptureFocusSessionToken();
|
||
_smartPunctuationAction.foregroundWindow = GetForegroundWindow();
|
||
}
|
||
|
||
void CMetasequoiaIME::_InvalidateSmartPunctuationShadow()
|
||
{
|
||
_smartPunctuationShadowChar = 0;
|
||
_smartPunctuationShadowValid = false;
|
||
}
|
||
|
||
void CMetasequoiaIME::_UpdateSmartPunctuationShadow(UINT code, WCHAR wch, bool isEaten)
|
||
{
|
||
switch (code)
|
||
{
|
||
case VK_SHIFT:
|
||
case VK_LSHIFT:
|
||
case VK_RSHIFT:
|
||
case VK_CONTROL:
|
||
case VK_LCONTROL:
|
||
case VK_RCONTROL:
|
||
case VK_MENU:
|
||
case VK_LMENU:
|
||
case VK_RMENU:
|
||
case VK_CAPITAL:
|
||
// Modifier presses edit nothing, so the shadow still describes the caret.
|
||
return;
|
||
case VK_BACK:
|
||
case VK_DELETE:
|
||
case VK_INSERT:
|
||
case VK_RETURN:
|
||
case VK_TAB:
|
||
case VK_ESCAPE:
|
||
case VK_LEFT:
|
||
case VK_RIGHT:
|
||
case VK_UP:
|
||
case VK_DOWN:
|
||
case VK_HOME:
|
||
case VK_END:
|
||
case VK_PRIOR:
|
||
case VK_NEXT:
|
||
_InvalidateSmartPunctuationShadow();
|
||
return;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (CCompositionProcessorEngine::IsSmartAsciiPunctuationKey(wch))
|
||
{
|
||
// _ResolveSmartPunctuation needs the current shadow to decide the form,
|
||
// and records whatever it commits once that decision is made.
|
||
return;
|
||
}
|
||
|
||
if (isEaten || wch == 0 || std::iswprint(static_cast<wint_t>(wch)) == 0)
|
||
{
|
||
// Eaten keys feed the composition and reach the document as committed
|
||
// text, which even a proxy store exposes, so let the document answer.
|
||
_InvalidateSmartPunctuationShadow();
|
||
return;
|
||
}
|
||
|
||
_smartPunctuationShadowChar = wch;
|
||
_smartPunctuationShadowValid = true;
|
||
}
|
||
|
||
void CMetasequoiaIME::_NoteKeyForSmartPunctuation(UINT code, WCHAR wch, bool isEaten, KEYSTROKE_FUNCTION function)
|
||
{
|
||
_UpdateSmartPunctuationShadow(code, wch, isEaten);
|
||
// Self-generated caret moves never reach here: the sinks bail out on the
|
||
// extra-info marker before noting the key, so stepping over a pair does not
|
||
// clear the very stack it is walking.
|
||
_NoteKeyForPairedPunctuation(code);
|
||
|
||
// The two intercept keys must survive until the edit session reads them:
|
||
// the space that converts the last Chinese punctuation, and the punctuation
|
||
// key that reverts a conversion. Only the classification result protects
|
||
// the state; the claim predicates are broader than the modes that allow a
|
||
// claim, so a passthrough key (English punctuation, full-width, composing,
|
||
// candidate) must still clear a pending action rather than leave it armed
|
||
// for the next space.
|
||
if (function == FUNCTION_SMART_PUNCTUATION_CONVERT || function == FUNCTION_SMART_PUNCTUATION_REVERT)
|
||
{
|
||
return;
|
||
}
|
||
_ClearSmartPunctuationAction();
|
||
}
|
||
|
||
std::wstring CMetasequoiaIME::_ResolveSmartPunctuation(WCHAR wch, WCHAR precedingChar)
|
||
{
|
||
if (_pCompositionProcessorEngine == nullptr)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
// A fresh punctuation resolution replaces whatever conversion is pending:
|
||
// the previous commit is no longer the most recent input.
|
||
_ClearSmartPunctuationAction();
|
||
|
||
const bool smartEnabled = Global::SmartPunctuationEnabled.load(std::memory_order_relaxed);
|
||
std::wstring resolved = _pCompositionProcessorEngine->ResolvePunctuation(wch, precedingChar);
|
||
if (!CCompositionProcessorEngine::IsSmartAsciiPunctuationKey(wch) || !smartEnabled)
|
||
{
|
||
if (!resolved.empty())
|
||
{
|
||
_smartPunctuationShadowChar = resolved.back();
|
||
_smartPunctuationShadowValid = true;
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
// ResolvePunctuation returns the ASCII key only when the sub-switch for
|
||
// the preceding character class is on; record it so the same key can
|
||
// revert to Chinese within the window.
|
||
const bool committedAscii = resolved.size() == 1 && resolved[0] == wch &&
|
||
(Global::SmartPunctuationDirectDigitEnabled.load(std::memory_order_relaxed) ||
|
||
Global::SmartPunctuationDirectLetterEnabled.load(std::memory_order_relaxed));
|
||
if (committedAscii)
|
||
{
|
||
const WCHAR *chinese = _pCompositionProcessorEngine->GetPunctuation(wch);
|
||
const WCHAR chineseChar = (chinese != nullptr && chinese[0] != L'\0' && chinese[1] == L'\0') ? chinese[0] : 0;
|
||
if (chineseChar != 0)
|
||
{
|
||
_WriteDirectSmartPunctuationState(wch, chineseChar, wch, precedingChar);
|
||
}
|
||
}
|
||
if (!resolved.empty())
|
||
{
|
||
_smartPunctuationShadowChar = resolved.back();
|
||
_smartPunctuationShadowValid = true;
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
HRESULT CMetasequoiaIME::_ExecuteSmartPunctuationAction(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch,
|
||
KEYSTROKE_FUNCTION function)
|
||
{
|
||
const SmartPunctuationAction state = _smartPunctuationAction;
|
||
// The action consumes the pending state either way: success keeps the
|
||
// converted form armed for the revert, failure falls back to committing
|
||
// the key normally. An early return must not leave the key armed.
|
||
_ClearSmartPunctuationAction();
|
||
|
||
const bool smartEnabled = Global::SmartPunctuationEnabled.load(std::memory_order_relaxed);
|
||
if (function == FUNCTION_SMART_PUNCTUATION_CONVERT)
|
||
{
|
||
CStringRange spaceString;
|
||
spaceString.Set(L" ", 1);
|
||
const auto insertPlainSpace = [&]() -> HRESULT { return _AddCharAndFinalize(ec, pContext, &spaceString); };
|
||
|
||
if (!smartEnabled || state.kind != SmartPunctuationAction::Kind::ChineseCommitted || state.chineseLeft == 0)
|
||
{
|
||
return insertPlainSpace();
|
||
}
|
||
|
||
WCHAR left = _GetPrecedingDocumentChar(ec, pContext);
|
||
if (left == 0)
|
||
{
|
||
// Terminals and proxy stores expose no document text; the
|
||
// commit-time state is the only evidence of what is on screen.
|
||
left = state.chineseLeft;
|
||
}
|
||
if (left != state.chineseLeft)
|
||
{
|
||
// The caret moved (mouse click) or something else edited the
|
||
// document. Rewriting a punctuation that was not just committed
|
||
// would turn historical text into ASCII.
|
||
return insertPlainSpace();
|
||
}
|
||
if (!_SmartPunctuationFingerprintMatches(ec, pContext, state.beforeChar))
|
||
{
|
||
// Same punctuation, different neighbourhood: the caret sits on a
|
||
// historical copy rather than the spot just committed.
|
||
return insertPlainSpace();
|
||
}
|
||
|
||
if (!CCompositionProcessorEngine::IsSmartPunctuationChinese(left) ||
|
||
!Global::SmartPunctuationSpaceConvertEnabled.load(std::memory_order_relaxed))
|
||
{
|
||
return insertPlainSpace();
|
||
}
|
||
const WCHAR asciiOpen = CCompositionProcessorEngine::GetSmartPunctuationAscii(left);
|
||
if (asciiOpen == 0)
|
||
{
|
||
return insertPlainSpace();
|
||
}
|
||
|
||
TF_SELECTION tfSelection = {};
|
||
ULONG fetched = 0;
|
||
HRESULT hr = pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched);
|
||
if (FAILED(hr) || fetched != 1 || tfSelection.range == nullptr)
|
||
{
|
||
return insertPlainSpace();
|
||
}
|
||
|
||
// Cover the Chinese punctuation just committed: one character left of
|
||
// the caret.
|
||
LONG shiftedStart = 0;
|
||
hr = tfSelection.range->Collapse(ec, TF_ANCHOR_START);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
hr = SafeRangeShiftStart(tfSelection.range, ec, -1, &shiftedStart);
|
||
if (SUCCEEDED(hr) && shiftedStart != -1)
|
||
{
|
||
hr = E_FAIL;
|
||
}
|
||
}
|
||
const WCHAR replacement[1] = {asciiOpen};
|
||
bool converted = false;
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
converted = SUCCEEDED(SafeRangeSetText(tfSelection.range, ec, 0, replacement, 1));
|
||
}
|
||
if (!converted)
|
||
{
|
||
tfSelection.range->Release();
|
||
return insertPlainSpace();
|
||
}
|
||
|
||
PlaceSmartPunctuationCaret(ec, pContext, tfSelection.range);
|
||
tfSelection.range->Release();
|
||
|
||
_smartPunctuationAction = {};
|
||
_smartPunctuationAction.kind = SmartPunctuationAction::Kind::AsciiConverted;
|
||
_smartPunctuationAction.triggerKey = state.triggerKey;
|
||
_smartPunctuationAction.chineseLeft = state.chineseLeft;
|
||
_smartPunctuationAction.asciiLeft = asciiOpen;
|
||
// The revert happens at the same spot, so it checks the same
|
||
// fingerprint the commit recorded.
|
||
_smartPunctuationAction.beforeChar = state.beforeChar;
|
||
_smartPunctuationAction.tick = GetTickCount64();
|
||
_smartPunctuationAction.focusToken = _CaptureFocusSessionToken();
|
||
_smartPunctuationAction.foregroundWindow = GetForegroundWindow();
|
||
_smartPunctuationShadowChar = asciiOpen;
|
||
_smartPunctuationShadowValid = true;
|
||
return S_OK;
|
||
}
|
||
|
||
// Revert: the punctuation key that produced the ASCII form is pressed
|
||
// again inside the window. Replace it back with the Chinese form.
|
||
const auto insertChinesePunctuation = [&]() -> HRESULT {
|
||
std::wstring chinese;
|
||
const WCHAR *punctuation =
|
||
_pCompositionProcessorEngine != nullptr ? _pCompositionProcessorEngine->GetPunctuation(wch) : nullptr;
|
||
if (punctuation != nullptr && punctuation[0] != L'\0')
|
||
{
|
||
chinese.assign(punctuation);
|
||
}
|
||
else if (state.chineseLeft != 0)
|
||
{
|
||
chinese.assign(1, state.chineseLeft);
|
||
}
|
||
if (chinese.empty())
|
||
{
|
||
return E_FAIL;
|
||
}
|
||
CStringRange chineseString;
|
||
chineseString.Set(chinese.c_str(), chinese.length());
|
||
const HRESULT insertHr = _AddCharAndFinalize(ec, pContext, &chineseString);
|
||
if (SUCCEEDED(insertHr))
|
||
{
|
||
// The intercepted key never updated the shadow, so point it at the
|
||
// character that now actually reached the document.
|
||
_smartPunctuationShadowChar = chinese.back();
|
||
_smartPunctuationShadowValid = true;
|
||
}
|
||
return insertHr;
|
||
};
|
||
|
||
if (!smartEnabled || !Global::SmartPunctuationRepeatToChineseEnabled.load(std::memory_order_relaxed) ||
|
||
state.kind != SmartPunctuationAction::Kind::AsciiConverted || state.chineseLeft == 0)
|
||
{
|
||
return insertChinesePunctuation();
|
||
}
|
||
|
||
// The document read confirms what the state remembers: while the host
|
||
// exposes text, what sits around the caret must be the converted form.
|
||
// A read of 0 means the host exposes nothing and the state is trusted.
|
||
const WCHAR left = _GetPrecedingDocumentChar(ec, pContext);
|
||
if (left != 0 && left != state.asciiLeft)
|
||
{
|
||
return insertChinesePunctuation();
|
||
}
|
||
if (!_SmartPunctuationFingerprintMatches(ec, pContext, state.beforeChar))
|
||
{
|
||
return insertChinesePunctuation();
|
||
}
|
||
TF_SELECTION tfSelection = {};
|
||
ULONG fetched = 0;
|
||
HRESULT hr = pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched);
|
||
if (FAILED(hr) || fetched != 1 || tfSelection.range == nullptr)
|
||
{
|
||
return insertChinesePunctuation();
|
||
}
|
||
|
||
LONG shiftedStart = 0;
|
||
hr = tfSelection.range->Collapse(ec, TF_ANCHOR_START);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
hr = SafeRangeShiftStart(tfSelection.range, ec, -1, &shiftedStart);
|
||
if (SUCCEEDED(hr) && shiftedStart != -1)
|
||
{
|
||
hr = E_FAIL;
|
||
}
|
||
}
|
||
|
||
const WCHAR replacement[1] = {state.chineseLeft};
|
||
bool reverted = false;
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
reverted = SUCCEEDED(SafeRangeSetText(tfSelection.range, ec, 0, replacement, 1));
|
||
}
|
||
if (!reverted)
|
||
{
|
||
tfSelection.range->Release();
|
||
return insertChinesePunctuation();
|
||
}
|
||
|
||
PlaceSmartPunctuationCaret(ec, pContext, tfSelection.range);
|
||
tfSelection.range->Release();
|
||
_smartPunctuationShadowChar = state.chineseLeft;
|
||
_smartPunctuationShadowValid = true;
|
||
return S_OK;
|
||
}
|
||
|
||
HRESULT CMetasequoiaIME::_ExecuteSmartPunctuationFallback(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch,
|
||
KEYSTROKE_FUNCTION function)
|
||
{
|
||
std::wstring fallback;
|
||
if (function == FUNCTION_SMART_PUNCTUATION_CONVERT)
|
||
{
|
||
fallback = L" ";
|
||
}
|
||
else
|
||
{
|
||
const WCHAR *punctuation =
|
||
_pCompositionProcessorEngine != nullptr ? _pCompositionProcessorEngine->GetPunctuation(wch) : nullptr;
|
||
if (punctuation != nullptr && punctuation[0] != L'\0')
|
||
{
|
||
fallback.assign(punctuation);
|
||
}
|
||
}
|
||
if (fallback.empty())
|
||
{
|
||
return E_FAIL;
|
||
}
|
||
|
||
CStringRange fallbackString;
|
||
fallbackString.Set(fallback.c_str(), fallback.length());
|
||
const HRESULT hr = _AddCharAndFinalize(ec, pContext, &fallbackString);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
_smartPunctuationShadowChar = fallback.back();
|
||
_smartPunctuationShadowValid = true;
|
||
}
|
||
// The key was claimed and can no longer be handed back, so the pending
|
||
// action is consumed either way.
|
||
_ClearSmartPunctuationAction();
|
||
return hr;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// ITfCompositionSink::OnCompositionTerminated
|
||
//
|
||
// Callback for ITfCompositionSink. The system calls this method whenever
|
||
// someone other than this service ends a composition.
|
||
//----------------------------------------------------------------------------
|
||
|
||
STDAPI CMetasequoiaIME::OnCompositionTerminated(TfEditCookie ecWrite, _In_ ITfComposition *pComposition)
|
||
{
|
||
if (pComposition == nullptr || !_IsCompositionCurrent(pComposition))
|
||
{
|
||
DebugTsfIssue47(L"host-terminated-stale-composition", FANY_IME_NO_REQUEST_ID, 0, L'\0', 0, 0, -1,
|
||
_IsComposing(),
|
||
_pCompositionProcessorEngine ? _pCompositionProcessorEngine->GetVirtualKeyLength() : 0, S_FALSE,
|
||
_CaptureCompositionEpoch());
|
||
// A delayed termination callback for an older composition must never
|
||
// delete the current candidate presenter or release newer ownership.
|
||
return S_OK;
|
||
}
|
||
|
||
DebugTsfIssue47(L"host-terminated-current-composition", FANY_IME_NO_REQUEST_ID, 0, L'\0', 0, 0, -1, TRUE,
|
||
_pCompositionProcessorEngine ? _pCompositionProcessorEngine->GetVirtualKeyLength() : 0, S_OK,
|
||
_CaptureCompositionEpoch());
|
||
|
||
// The callback already carries a write cookie and the host has already
|
||
// ended this exact composition. Detach ownership before making COM calls,
|
||
// so a re-entrant/stale callback cannot observe it as current or tear down
|
||
// a composition created later.
|
||
ITfComposition *terminatedComposition = pComposition;
|
||
terminatedComposition->AddRef();
|
||
_pComposition->Release();
|
||
_pComposition = nullptr;
|
||
_voiceCompositionActive = false;
|
||
|
||
ITfContext *ownerContext = _pContext;
|
||
if (ownerContext)
|
||
{
|
||
ownerContext->AddRef();
|
||
_pContext->Release();
|
||
_pContext = nullptr;
|
||
}
|
||
|
||
uint64_t nextEpoch = _compositionEpoch.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||
if (nextEpoch == 0)
|
||
{
|
||
_compositionEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||
}
|
||
|
||
// Detach and end the old candidate/session before the COM cleanup calls
|
||
// below can re-enter and create a presenter for a newer composition.
|
||
_DeleteCandidateList(FALSE, ownerContext);
|
||
if (Global::g_connected)
|
||
{
|
||
// EndCandidateUiSession is intentionally idempotent, but a presenter
|
||
// can exist before its UI session becomes active. Always send one
|
||
// exact routed clear so the Server cannot retain that composition.
|
||
SendHideCandidateWndEventToUIProcess();
|
||
}
|
||
|
||
// Do NOT SetText(empty) here. Cancel paths already wipe via
|
||
// _HandleCancel → _RemoveDummyCompositionForComposing. Wiping again after
|
||
// a normal commit/EndComposition can delete the just-committed text and
|
||
// destabilize fragile hosts (notably QQ).
|
||
if (ownerContext)
|
||
{
|
||
_ClearCompositionDisplayAttributes(ecWrite, ownerContext, terminatedComposition);
|
||
}
|
||
terminatedComposition->Release();
|
||
|
||
if (ownerContext)
|
||
{
|
||
ownerContext->Release();
|
||
}
|
||
|
||
return S_OK;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _IsComposing
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
BOOL CMetasequoiaIME::_IsComposing()
|
||
{
|
||
return _pComposition != nullptr;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _SetComposition
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
void CMetasequoiaIME::_SetComposition(_In_ ITfComposition *pComposition)
|
||
{
|
||
_pComposition = pComposition;
|
||
uint64_t nextEpoch = _compositionEpoch.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||
if (nextEpoch == 0)
|
||
{
|
||
_compositionEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||
}
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _AddComposingAndChar
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
HRESULT CMetasequoiaIME::_AddComposingAndChar(TfEditCookie ec, _In_ ITfContext *pContext,
|
||
_In_ CStringRange *pstrAddString)
|
||
{
|
||
HRESULT hr = S_OK;
|
||
|
||
if (_pComposition != nullptr)
|
||
{
|
||
ITfRange *pRangeComposition = nullptr;
|
||
hr = _pComposition->GetRange(&pRangeComposition);
|
||
if (SUCCEEDED(hr) && pRangeComposition != nullptr)
|
||
{
|
||
hr = SafeRangeSetText(pRangeComposition, ec, 0, pstrAddString->Get(), (LONG)pstrAddString->GetLength());
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
_SetCompositionDisplayAttributesForRange(ec, pContext, pRangeComposition, _gaDisplayAttributeInput);
|
||
|
||
TF_SELECTION sel;
|
||
pRangeComposition->Collapse(ec, TF_ANCHOR_END);
|
||
sel.range = pRangeComposition;
|
||
sel.style.ase = TF_AE_NONE;
|
||
sel.style.fInterimChar = FALSE;
|
||
pContext->SetSelection(ec, 1, &sel);
|
||
|
||
pRangeComposition->Release();
|
||
return hr;
|
||
}
|
||
pRangeComposition->Release();
|
||
}
|
||
}
|
||
|
||
ULONG fetched = 0;
|
||
TF_SELECTION tfSelection;
|
||
|
||
if (pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched) != S_OK || fetched == 0)
|
||
return S_FALSE;
|
||
|
||
//
|
||
// make range start to selection
|
||
//
|
||
ITfRange *pAheadSelection = nullptr;
|
||
hr = pContext->GetStart(ec, &pAheadSelection);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
hr = pAheadSelection->ShiftEndToRange(ec, tfSelection.range, TF_ANCHOR_START);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
ITfRange *pRange = nullptr;
|
||
BOOL exist_composing = _FindComposingRange(ec, pContext, pAheadSelection, &pRange);
|
||
|
||
std::wstring strAddString(pstrAddString->Get(), pstrAddString->GetLength());
|
||
|
||
_SetInputString(ec, pContext, pRange, pstrAddString, exist_composing);
|
||
|
||
if (pRange)
|
||
{
|
||
pRange->Release();
|
||
}
|
||
}
|
||
}
|
||
|
||
tfSelection.range->Release();
|
||
|
||
if (pAheadSelection)
|
||
{
|
||
pAheadSelection->Release();
|
||
}
|
||
|
||
return S_OK;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _AddCharAndFinalize
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
HRESULT CMetasequoiaIME::_AddCharAndFinalize(TfEditCookie ec, _In_ ITfContext *pContext,
|
||
_In_ CStringRange *pstrAddString)
|
||
{
|
||
HRESULT hr = E_FAIL;
|
||
|
||
if (_pComposition != nullptr)
|
||
{
|
||
hr = _SetCompositionTextAndSelection(ec, pContext, pstrAddString);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
return hr;
|
||
}
|
||
}
|
||
|
||
ULONG fetched = 0;
|
||
TF_SELECTION tfSelection;
|
||
|
||
if ((hr = pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched)) != S_OK || fetched != 1)
|
||
return hr;
|
||
|
||
// We use SetText here instead of InsertTextAtSelection because we've already started a composition
|
||
// We don't want to the app to adjust the insertion point inside our composition
|
||
hr = SafeRangeSetText(tfSelection.range, ec, 0, pstrAddString->Get(), (LONG)pstrAddString->GetLength());
|
||
if (hr == S_OK)
|
||
{
|
||
// Update the selection, we'll make it an insertion point just past
|
||
// the inserted text.
|
||
tfSelection.range->Collapse(ec, TF_ANCHOR_END);
|
||
pContext->SetSelection(ec, 1, &tfSelection);
|
||
}
|
||
|
||
tfSelection.range->Release();
|
||
|
||
return hr;
|
||
}
|
||
|
||
HRESULT CMetasequoiaIME::_InsertTextToComposition(TfEditCookie ec, _In_ ITfContext *pContext,
|
||
_In_ CStringRange *pstrAddString)
|
||
{
|
||
if (_pComposition == nullptr)
|
||
{
|
||
return E_FAIL;
|
||
}
|
||
|
||
ITfRange *pRangeComposition = nullptr;
|
||
HRESULT hr = _pComposition->GetRange(&pRangeComposition);
|
||
if (FAILED(hr) || pRangeComposition == nullptr)
|
||
{
|
||
return FAILED(hr) ? hr : E_FAIL;
|
||
}
|
||
|
||
hr = SafeRangeSetText(pRangeComposition, ec, 0, pstrAddString->Get(), (LONG)pstrAddString->GetLength());
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
TF_SELECTION tfSelection;
|
||
pRangeComposition->Collapse(ec, TF_ANCHOR_END);
|
||
tfSelection.range = pRangeComposition;
|
||
tfSelection.style.ase = TF_AE_NONE;
|
||
tfSelection.style.fInterimChar = FALSE;
|
||
pContext->SetSelection(ec, 1, &tfSelection);
|
||
}
|
||
|
||
pRangeComposition->Release();
|
||
return hr;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _SetCompositionTextAndSelection
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
HRESULT CMetasequoiaIME::_SetCompositionTextAndSelection(TfEditCookie ec, _In_ ITfContext *pContext,
|
||
_In_ CStringRange *pstrAddString)
|
||
{
|
||
if (_pComposition == nullptr)
|
||
{
|
||
return E_FAIL;
|
||
}
|
||
|
||
ITfRange *pRangeComposition = nullptr;
|
||
HRESULT hr = _pComposition->GetRange(&pRangeComposition);
|
||
if (FAILED(hr) || pRangeComposition == nullptr)
|
||
{
|
||
return FAILED(hr) ? hr : E_FAIL;
|
||
}
|
||
|
||
hr = SafeRangeSetText(pRangeComposition, ec, 0, pstrAddString->Get(), (LONG)pstrAddString->GetLength());
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
TF_SELECTION tfSelection;
|
||
pRangeComposition->Collapse(ec, TF_ANCHOR_END);
|
||
tfSelection.range = pRangeComposition;
|
||
tfSelection.style.ase = TF_AE_NONE;
|
||
tfSelection.style.fInterimChar = FALSE;
|
||
pContext->SetSelection(ec, 1, &tfSelection);
|
||
}
|
||
|
||
pRangeComposition->Release();
|
||
return hr;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _FindComposingRange
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
BOOL CMetasequoiaIME::_FindComposingRange(TfEditCookie ec, _In_ ITfContext *pContext, _In_ ITfRange *pSelection,
|
||
_Outptr_result_maybenull_ ITfRange **ppRange)
|
||
{
|
||
if (ppRange == nullptr)
|
||
{
|
||
return FALSE;
|
||
}
|
||
|
||
*ppRange = nullptr;
|
||
|
||
// find GUID_PROP_COMPOSING
|
||
ITfProperty *pPropComp = nullptr;
|
||
IEnumTfRanges *enumComp = nullptr;
|
||
|
||
HRESULT hr = pContext->GetProperty(GUID_PROP_COMPOSING, &pPropComp);
|
||
if (FAILED(hr) || pPropComp == nullptr)
|
||
{
|
||
return FALSE;
|
||
}
|
||
|
||
hr = pPropComp->EnumRanges(ec, &enumComp, pSelection);
|
||
if (FAILED(hr) || enumComp == nullptr)
|
||
{
|
||
pPropComp->Release();
|
||
return FALSE;
|
||
}
|
||
|
||
BOOL isCompExist = FALSE;
|
||
VARIANT var;
|
||
ULONG fetched = 0;
|
||
|
||
while (enumComp->Next(1, ppRange, &fetched) == S_OK && fetched == 1)
|
||
{
|
||
hr = pPropComp->GetValue(ec, *ppRange, &var);
|
||
if (hr == S_OK)
|
||
{
|
||
if (var.vt == VT_I4 && var.lVal != 0)
|
||
{
|
||
isCompExist = TRUE;
|
||
break;
|
||
}
|
||
}
|
||
(*ppRange)->Release();
|
||
*ppRange = nullptr;
|
||
}
|
||
|
||
pPropComp->Release();
|
||
enumComp->Release();
|
||
|
||
return isCompExist;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _SetInputString
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
HRESULT CMetasequoiaIME::_SetInputString(TfEditCookie ec, _In_ ITfContext *pContext, _Out_opt_ ITfRange *pRange,
|
||
_In_ CStringRange *pstrAddString, BOOL exist_composing)
|
||
{
|
||
ITfRange *pRangeInsert = nullptr;
|
||
if (!exist_composing)
|
||
{
|
||
_InsertAtSelection(ec, pContext, pstrAddString, &pRangeInsert);
|
||
if (pRangeInsert == nullptr)
|
||
{
|
||
return S_OK;
|
||
}
|
||
else
|
||
{
|
||
// pRange = pRangeInsert;
|
||
|
||
/* To make TsfPad work, we need to get range manually */
|
||
_pComposition->GetRange(&pRange);
|
||
}
|
||
}
|
||
if (pRange != nullptr)
|
||
{
|
||
SafeRangeSetText(pRange, ec, 0, pstrAddString->Get(), (LONG)pstrAddString->GetLength());
|
||
}
|
||
|
||
_SetCompositionLanguage(ec, pContext);
|
||
|
||
_SetCompositionDisplayAttributes(ec, pContext, _gaDisplayAttributeInput);
|
||
|
||
// update the selection, we'll make it an insertion point just past
|
||
// the inserted text.
|
||
ITfRange *pSelection = nullptr;
|
||
TF_SELECTION sel;
|
||
|
||
if ((pRange != nullptr) && (pRange->Clone(&pSelection) == S_OK))
|
||
{
|
||
pSelection->Collapse(ec, TF_ANCHOR_END);
|
||
|
||
sel.range = pSelection;
|
||
sel.style.ase = TF_AE_NONE;
|
||
sel.style.fInterimChar = FALSE;
|
||
pContext->SetSelection(ec, 1, &sel);
|
||
pSelection->Release();
|
||
}
|
||
|
||
if (pRangeInsert)
|
||
{
|
||
pRangeInsert->Release();
|
||
}
|
||
|
||
return S_OK;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _InsertAtSelection
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
HRESULT CMetasequoiaIME::_InsertAtSelection(TfEditCookie ec, _In_ ITfContext *pContext,
|
||
_In_ CStringRange *pstrAddString, _Outptr_ ITfRange **ppCompRange)
|
||
{
|
||
ITfRange *rangeInsert = nullptr;
|
||
ITfInsertAtSelection *pias = nullptr;
|
||
HRESULT hr = S_OK;
|
||
|
||
if (ppCompRange == nullptr)
|
||
{
|
||
hr = E_INVALIDARG;
|
||
goto Exit;
|
||
}
|
||
|
||
*ppCompRange = nullptr;
|
||
|
||
hr = pContext->QueryInterface(IID_ITfInsertAtSelection, (void **)&pias);
|
||
if (FAILED(hr))
|
||
{
|
||
goto Exit;
|
||
}
|
||
|
||
hr = pias->InsertTextAtSelection(ec, TF_IAS_QUERYONLY, pstrAddString->Get(), (LONG)pstrAddString->GetLength(),
|
||
&rangeInsert);
|
||
|
||
if (FAILED(hr) || rangeInsert == nullptr)
|
||
{
|
||
rangeInsert = nullptr;
|
||
pias->Release();
|
||
goto Exit;
|
||
}
|
||
|
||
*ppCompRange = rangeInsert;
|
||
pias->Release();
|
||
hr = S_OK;
|
||
|
||
Exit:
|
||
return hr;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _RemoveDummyCompositionForComposing
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
HRESULT CMetasequoiaIME::_RemoveDummyCompositionForComposing(TfEditCookie ec, _In_ ITfComposition *pComposition)
|
||
{
|
||
HRESULT hr = S_OK;
|
||
|
||
ITfRange *pRange = nullptr;
|
||
|
||
if (pComposition)
|
||
{
|
||
hr = pComposition->GetRange(&pRange);
|
||
if (SUCCEEDED(hr))
|
||
{
|
||
hr = SafeRangeSetText(pRange, ec, 0, nullptr, 0);
|
||
pRange->Release();
|
||
}
|
||
}
|
||
|
||
return hr;
|
||
}
|
||
|
||
//+---------------------------------------------------------------------------
|
||
//
|
||
// _SetCompositionLanguage
|
||
//
|
||
//----------------------------------------------------------------------------
|
||
|
||
BOOL CMetasequoiaIME::_SetCompositionLanguage(TfEditCookie ec, _In_ ITfContext *pContext)
|
||
{
|
||
HRESULT hr = S_OK;
|
||
BOOL ret = TRUE;
|
||
|
||
CCompositionProcessorEngine *pCompositionProcessorEngine = nullptr;
|
||
pCompositionProcessorEngine = _pCompositionProcessorEngine;
|
||
|
||
LANGID langidProfile = 0;
|
||
pCompositionProcessorEngine->GetLanguageProfile(&langidProfile);
|
||
|
||
ITfRange *pRangeComposition = nullptr;
|
||
ITfProperty *pLanguageProperty = nullptr;
|
||
|
||
// we need a range and the context it lives in
|
||
hr = _pComposition->GetRange(&pRangeComposition);
|
||
if (FAILED(hr))
|
||
{
|
||
ret = FALSE;
|
||
goto Exit;
|
||
}
|
||
|
||
// get our the language property
|
||
hr = pContext->GetProperty(GUID_PROP_LANGID, &pLanguageProperty);
|
||
if (FAILED(hr))
|
||
{
|
||
ret = FALSE;
|
||
goto Exit;
|
||
}
|
||
|
||
VARIANT var;
|
||
var.vt = VT_I4; // we're going to set DWORD
|
||
var.lVal = langidProfile;
|
||
|
||
hr = pLanguageProperty->SetValue(ec, pRangeComposition, &var);
|
||
if (FAILED(hr))
|
||
{
|
||
ret = FALSE;
|
||
goto Exit;
|
||
}
|
||
|
||
pLanguageProperty->Release();
|
||
pRangeComposition->Release();
|
||
|
||
Exit:
|
||
return ret;
|
||
}
|