IME 处理指南

本文档解释了 Gecko 如何处理 IME。

简介

IME 是输入法编辑器的缩写。这是一个来自 Windows 的技术术语,但如今在其他平台上也使用它。

IME 是用户文本输入的辅助应用程序。它在聚焦应用程序之前或之后(取决于平台)处理原生键盘事件,并创建组合字符串(也称为预编辑字符串),建议用户尝试输入的内容列表,将组合字符串作为列表中的选定项提交,并在没有任何转换的情况下提交组合字符串。IME 被中国、日本、韩国和台湾用户用来输入汉字,因为汉字的数量超过数千个,无法直接从键盘输入。然而,尤其是在当今的移动设备上,IME 也被用于输入拉丁语,例如自动完成。此外,IME 可以在某些平台上用于手写系统或语音输入系统。

如果聚焦元素上启用了 IME,我们称该状态为“已启用”。如果 IME 无法完全使用(即用户无法启用 IME),我们称该状态为“已禁用”。

如果 IME 已启用,但用户使用直接输入模式(例如,输入拉丁字符),我们称之为“IME 已关闭”。否则,我们称之为“IME 已打开”。(仅供参考:“打开”也称为“激活”或“已打开”。“关闭”也称为“未激活”或“已关闭”)

因此,当您尝试修复 Gecko 中文本输入的错误时,本文档很有用。

组合字符串和分句

典型的日语 IME 可以将两个或多个单词输入到组合字符串中。当用户将平假名字符转换为汉字时,日语 IME 会将组合字符串分成多个分句。例如,如果用户输入“watasinonamaehanakanodesu”,它会自动转换为平假名字符“わたしのなまえはなかのです”(在下面的屏幕截图中,组合字符串带有波浪下划线,只有一个分句称为“原始输入分句”)。

Screenshot of raw composition string which is inputting Roman character mode of MS-IME (Japanese) Screenshot of raw composition string whose all characters are Hiragana character (MS-IME, Japanese)

当用户按下Convert键时,日语 IME 将组合字符串分成“わたしの”(我的),“なまえは”(名字是)和“なかのです”(中野)。然后,使用汉字转换每个分句:“私の”,“名前は”和“中野です”(在下面的屏幕截图中,每个分句都带下划线,并且不连续连接。这些分句称为“已转换分句”)。

Screenshot of converted composition string (MS-IME, Japanese)

如果一个或多个分句未按预期转换,用户可以使用方向键选择其中一个分句,并在下拉菜单中查找预期的结果(在下面的屏幕截图中,带粗下划线的句称为“已选择分句”)。

Screenshot of candidate window of MS-IME (Japanese) which converts the selected clause

基本上,组合字符串和每个分句的样式由 Gecko 呈现。下拉菜单由 IME 创建。

每个分句都用编辑器中的选择表示。从 chrome 脚本中,您可以使用nsISelectionController检查它。在原生代码中,您可以使用nsISelectionControllermozilla::SelectionType访问它(后者推荐使用,因为它类型更安全)。编辑器从mozilla::TextRangeType设置这些 IME 选择,这些选择由mozilla::WidgetCompositionEvent作为mozilla::TextRangeArray发送。下表解释了它们之间的映射关系。

组合字符串或插入符号每个分句的选择类型

nsISelectionController

mozilla::SelectionType

mozilla::TextRangeType

插入符号

SELECTION_NORMAL

eNormal

eCaret

用户输入的原始文本

SELECTION_IME_RAW_INPUT

eIMERawClause

eRawClause

用户输入的原始文本的选定分句

SELECTION_IME_SELECTEDRAWTEXT

eIMESelectedRawClause

eSelectedRawClause

IME 转换的分句

SELECTION_IME_CONVERTEDTEXT

eIMEConvertedClause

eConvertedClause

用户或 IME 选择的分句,并且也由 IME 转换

SELECTION_IME_SELECTEDCONVERTEDTEXT

eIMESelectedClause

eSelectedClause

请注意,通常不使用“用户输入的原始文本的选定分句”,因为当组合字符串已被分成多个分句时,这意味着组合字符串至少已由 IME 转换过一次。

处理 IME 组合的模块

小部件

每个小部件都处理原生 IME 事件,并使用mozilla::widget::TextEventDispatcher分派WidgetCompositionEvent来表示聚焦编辑器中 IME 的行为。

这是唯一依赖于用户平台的模块。有关每个平台实现的详细信息,另请参见原生 IME 处理程序部分。

注意

Android 小部件仍不使用TextEventDispatcher来分派WidgetCompositionEvents,请参见错误 1137567

mozilla::widget::TextEventDispatcher

此类由每个平台上的原生 IME 处理程序使用。它封装了分派WidgetCompositionEventWidgetKeyboardEvent的逻辑,以使每个平台上的行为完全相同。例如,如果在 XP 层级由此类管理组合时应分派WidgetKeyboardEvent。首先,原生 IME 处理程序通过调用BeginNativeInputTransaction()获取使用TextEventDispatcher的权限。然后,如果BeginNativeInputTransaction()返回 true,则StartComposition()SetPendingComposition()FlushPendingComposition()CommitComposition()等可用。这些方法会自动管理组合状态并正确分派WidgetCompositionEvent

这也由mozilla::TextInputProcessor使用,后者可以使用 chrome 脚本模拟(或实现)IME。因此,原生 IME 处理程序使用此类意味着分派部分也由自动化测试进行测试。

mozilla::WidgetCompositionEvent

在内部,WidgetCompositionEvent表示原生 IME 行为。其消息是以下值之一

eCompositionStart

在开始组合时分派。这表示 DOM compositionstart事件。mData 值是在分派 DOM 事件时选择的字符串,它由TextComposition自动设置。

eCompositionUpdate

eCompositionChange将更改组合字符串时,TextComposition会分派此事件。这表示 DOM compositionupdate事件。

eCompositionEnd

当分派eCompositionCommitAsIseCompositionCommit事件时,TextComposition会分派此事件。这表示 DOM compositionend事件。

eCompositionChange

仅在内部使用。在修改组合字符串、提交组合、更改插入符号位置和/或更改分句范围时分派。这表示 DOM 文本事件,该事件不在任何标准中。mRanges在此消息中不应为空。

eCompositionCommitAsIs

仅在内部使用。当组合使用字符串提交时分派。mData值应始终为空字符串。这会导致不带分句信息的 DOM 文本事件和 DOM compositionend事件。

eCompositionCommit

仅在内部使用。当组合使用特定字符串提交时分派。mData值是提交字符串。这会导致不带分句信息的 DOM 文本事件和 DOM compositionend事件。

事件消息表

mData 的含义

谁设置mData

mRanges

表示 DOM 事件

eCompositionStart

开始组合前的选定字符串

TextComposition

nullptr

compositionstart

eCompositionUpdate

新的组合字符串

TextComposition

nullptr

compositionupdate

eCompositionEnd

提交字符串

TextComposition

nullptr

compositionend

eCompositionChange

新的组合字符串

小部件(或TextComposition

不能是nullptr

文本

eCompositionCommitAsIs

N/A(必须为空)

没有人

nullptr

None

eCompositionCommit

提交字符串

小部件(或TextComposition

nullptr

None

PresShell

PresShell接收小部件事件,并从聚焦文档和元素中确定事件目标。然后,它将事件和事件目标发送到IMEStateManager

mozilla::IMEStateManager

IMEStateManager 会查找一个其原生 IME 上下文与分发小部件事件的小部件相同的 TextComposition 实例。如果没有合适的 TextComposition 实例,它会创建该实例。然后,它将事件发送到 TextComposition 实例。

请注意,所有 TextComposition 实例都由 IMEStateManager 管理。创建实例时,会将其注册到列表中。当合成完全结束时,它将从列表中注销(并自动释放)。

mozilla::TextComposition

TextComposition 管理合成并分发 DOM compositionupdate 事件。

当它接收到 eCompositionChangeeCompositionCommiteCompositionCommitAsIs 事件时,它会将事件分发到存储的节点,该节点是 eCompositionStart 事件的事件目标。因此,此类保证同一合成的所有合成事件都将在相同的元素上触发。

当它接收到 eCompositionChangeeCompositionCommit 时,它会检查新的合成字符串(或提交字符串)是否与 TextComposition 存储的最后数据不同。如果合成事件正在更改合成字符串,则 TextComposition 实例会直接向 DOM 树分发带有 eCompositionUpdateWidgetCompositionEvent 并修改最后的数据。 eCompositionUpdate 事件将导致 DOM compositionupdate 事件。

当它接收到 eCompositionCommitAsIseCompositionCommit 时,它会分发一个 eCompositionEnd 事件,该事件将在分发 eCompositionUpdate 事件和/或 eCompositionChange 事件(如果必要)后导致 DOM compositionend 事件。

它的另一个重要工作是,当聚焦的编辑器处理分发的 eCompositionChange 事件时,它会修改存储的合成字符串及其子句信息。编辑器引用存储的信息来创建或修改表示合成字符串的文本节点。

并且在分发 eComposition* 事件之前,此类会从默认设置中分发合成事件数据的 ASCII 控制字符中删除。尽管可以通过 "dom.compositionevent.allow_control_characters" 首选项禁用此功能。

最后,此类保证同步执行请求向 IME 提交或取消当前合成的操作。有关详细信息,请参阅 强制提交合成 部分。

editor/libeditor

mozilla::EditorEventListener 侦听受信任的 DOM compositionstarttextcompositionend 事件,并将事件通知 mozilla::EditorBasemozilla::TextEditor

EditorBase 接收到 eCompositionStart(DOM "compositionstart")事件时,它会查找合适的 TextComposition 实例并将其存储。

TextEditor 接收到 eCompositionChange(DOM "text")事件时,它会创建或修改包含合成字符串的文本节点,并且 mozilla::CompositionTransaction(以前称为 IMETextTxn)设置 IME 选择以表示合成字符串的子句。

EditorBase 接收到 eCompositionEnd(DOM "compositionend")事件时,它会释放存储的 TextComposition 实例。

nsTextFrame

nsTextFrame 绘制 IME 选择。

mozilla::IMEContentObserver

IMEContentObserver 观察聚焦编辑器的各种更改。当 TextEditorHTMLEditor 实例的对应元素获得焦点时, IMEStateManager 会创建一个实例,然后开始观察并通知 widget IME 获得焦点。当编辑器失去焦点时,它会通知 widget IME 失去焦点并停止观察所有内容。最后,它被 IMEStateManager 销毁。

此类观察聚焦编辑器的选择更改(插入点位置更改)、文本更改以及文档中所有内容的布局更改(通过重排或滚动)。它取决于 nsIWidget::GetIMEUpdatePreference() 的结果观察什么。

当它向 widget 通知某些内容时,它需要能够安全地运行脚本,因为通知某些内容可能会导致分发一个或多个 DOM 事件和/或新的重排。因此,IMEContentObserver 仅存储应发送到 widget 的通知。然后,mozilla::IMEContentObserver::IMENotificationSender 会尝试在可能安全地执行此操作时发送挂起的通知。目前,它尝试在以下时间发送:

  • PresShell::HandleEventInternal() 分发本地事件后

  • 新获得焦点的编辑器接收 DOM focus 事件时

  • 下一个刷新驱动程序刻度时

注意

第三个时间实际上可能不安全,但它会导致大量自动化测试错误。

有关发送通知的详细信息,请参阅 发送到 IME 的通知 部分。

目前,WidgetQueryContentEvent 通过 IMEContentObserver 处理,因为如果它有选择的缓存,它可以仅使用缓存设置 eQuerySelectedText 事件的回复。这比使用 ContentEventHandler 快得多。

e10s 支持

即使远程进程具有焦点,chrome 进程中的原生 IME 处理程序也会执行其工作。因此,原生 IME 处理程序和聚焦编辑器之间存在进程边界。不幸的是,不允许从 chrome 进程到远程进程使用同步通信。这意味着 chrome 进程(以及原生 IME 和我们的原生 IME 处理程序)无法直接查询聚焦编辑器的内容。为了解决此问题,我们在进程边界周围设置了 ContentCache 类。

mozilla::ContentCache

这是 ContentCacheInChildContentCacheInParent 的基类,并且支持 IPC。它包含它们的公共成员,包括所有缓存数据:

mText

聚焦编辑器中的全部文本。这可能太大,但 IME 可能会请求编辑器中的所有文本。

如果我们能够按段落分隔编辑器内容,则段落之间的选择移动会生成伪焦点移动,我们可以减少 ContentEventHandler 的大小和运行时成本。但是,我们还没有计划这样做。请注意,Microsoft Word 使用此技巧。

mCompositionStart

mText 中合成字符串的偏移量。当没有合成时,它是 UINT32_MAX

mSelection::mAnchormSelection::mFocus

mText 中选择锚点和焦点的偏移量。

mSelection::mWritingMode

选择开始处的书写模式。

mSelection::mAnchorCharRectmSelection::mFocusCharRect

mSelection::mAnchormSelection::mFocus 的下一个字符矩形。如果对应的偏移量是编辑器内容的末尾,则其矩形应为插入点矩形。

这些矩形不应为空矩形。

mSelection::mRect

选择范围内的统一字符矩形。当选择折叠时,这应该是插入点矩形。

mFirstRect

mText 的第一个字符矩形。当 mText 为空字符串时,这应该是插入点矩形。

mCaret::mOffset

即使选择未折叠,也始终与选择开始偏移量相同。

mCaret::mRect

mCaret::mOffset 处的插入点矩形。如果插入点实际上不存在,则使用该偏移量处的字符矩形计算它。

mTextRectArray::mStart

如果有合成,则 mStart 与 mCompositionStart 相同。否则,它是 UINT32_MAX。

mTextRectArray::mRects

合成字符串的每个字符矩形。

mEditorRect

编辑器元素的矩形。

mozilla::ContentCacheInChild

此类仅存在于远程进程中。它作为 PuppetWidget 的成员创建。当 PuppetWidget 从远程进程中的 IMEContentObserver 接收到发送到 IME 的通知时,它会使此类修改其缓存的内容。然后,此类使用 WidgetQueryContentEvents 执行此操作。最后,PuppetWidget 将通知和 ContentCacheInParent 实例作为 ContentCache 发送到其父进程。

mozilla::ContentCacheInParent

此类作为 TabParent 的成员存在。当 TabParent 从相应的远程进程接收到通知时,它会为 ContentCacheInParent 分配新的 ContentCache 并将通知发布到 ContentCacheInParent。如果所有发送的 WidgetCompositionEventsWidgetSelectionEvents 已经在远程进程中处理,则 ContentCacheInParent 会将通知发送到小部件。

此外,它还使用其缓存处理 WidgetQueryContentEvents。它们支持的事件消息为:

  • eQuerySelectedText(仅适用于 SelectionType::eNormal

  • eQueryTextContent

  • eQueryTextRect

  • eQueryCaretRect

  • eQueryEditorRect

此外,它不支持带有 XP 换行符的内容查询事件,但这不应有任何问题,因为原生 IME 处理程序使用原生换行符查询内容。

ContentCacheInParent 还管理发送的 WidgetCompositionEventsWidgetSelectionEvents。这些事件在远程进程中处理后, TabParent 会通过调用 RecvOnEventNeedingAckHandled() 接收它。然后,它调用 ContentCacheInParent::OnEventNeedingAckHandled()。最后,ContentCacheInParent 会刷新挂起的通知。

在 e10s 模式下,mozilla::TextComposition 和 mozilla::IMEStateManager 如何工作?

在远程进程中,它们的工作方式与非 e10s 模式相同。另一方面,它们在父进程中以特殊方式工作。

当父进程中的 IMEStateManager 接收 eCompositionStart 事件时,它会像平常一样创建 TextComposition 实例。但是,如果事件目标包含远程内容,TextComposition::DispatchCompositionEvent() 会直接将事件发送到远程进程,而不是在当前进程中将其分派到目标 DOM 树。

这意味着即使在父进程中,任何人都可以检索 TextComposition 实例,但它在父进程中什么也不做。

IMEStateManager 的工作方式更加复杂,因为每个进程中的 IMEStateManager 需要协商输入上下文管理的所有权。

当远程进程获得焦点时,父进程中的 IMEStateManager 会暂时禁用窗口小部件中的 IME。之后,远程进程中的 IMEStateManager 将为获得焦点的编辑器设置正确的输入上下文。此时,父进程中的 IMEStateManager 不执行任何操作。因此,当远程进程拥有焦点时,IMEContentObserver 永远不会被创建。

当远程进程失去焦点时,父进程中的 IMEStateManager 会通知远程进程中的 IMEStateManager “停止 IME 状态管理”。当远程进程通过此操作调用 IMEStateManager::StopIMEStateManagement() 时,IMEStateManager 会忘记所有焦点信息(即,表示没有任何对象拥有焦点)。

当父进程中的 IMEStateManager 收到伪焦点从菜单栏移动到菜单栏或从菜单栏移动出的通知,并且远程进程拥有焦点时,它会通知远程进程“菜单键盘监听器已安装”。然后,TabChild 会在远程进程中调用 IMEStateManager::OnInstalledMenuKeyboardListener()

每个分句的样式

每个 IME 分句的样式由 LookAndFeel 类根据平台管理。因此,它可以通过偏好设置覆盖。

可以使用以下偏好设置指定背景颜色、前景色(文本颜色)和下划线颜色。值必须是“#rrggbb”格式的字符串。

  • ui.IMERawInputBackground

  • ui.IMERawInputForeground

  • ui.IMERawInputUnderline

  • ui.IMESelectedRawTextBackground

  • ui.IMESelectedRawTextForeground

  • ui.IMESelectedRawTextUnderline

  • ui.IMEConvertedTextBackground

  • ui.IMEConvertedTextForeground

  • ui.IMEConvertedTextUnderline

  • ui.IMESelectedConvertedTextBackground

  • ui.IMESelectedConvertedTextForeground

  • ui.IMESelectedConvertedTextUnderline

可以使用以下偏好设置指定下划线样式。值是整数,0:无,1:点状,2:虚线,3:实线,4:双线,5:波浪线(值与 nsStyleConsts.h 中定义的 mozilla::StyleTextDecorationStyle 相同)。

  • ui.IMERawInputUnderlineStyle

  • ui.IMESelectedRawTextUnderlineStyle

  • ui.IMEConvertedTextUnderlineStyle

  • ui.IMESelectedConvertedTextUnderlineStyle

可以使用 "ui.IMEUnderlineRelativeSize" 偏好设置指定下划线宽度。这会影响所有类型的分句。该值应为 100 或 200。100 表示正常宽度,200 表示双倍宽度。

在某些平台上,IME 可能支持其自身每个分句的样式。目前,此功能在 Windows 的 TSF 模式和 Linux 上受支持。样式信息存储在 TextRange.h 中定义的 TextRangeStyle 中。它是 TextRange 的成员。只有当消息为 eCompositionChange 时,TextRange 才会存储在 WidgetCompositionEventmRanges 中。

合成字符串的生命周期

当本地 IME 通知 Gecko 开始合成时,窗口小部件会分派带有 eCompositionStartWidgetCompositionEvent,这将导致 DOM compositionstart 事件。

当本地 IME 通知 Gecko 合成字符串更改、插入符号位置更改和/或分句长度更改时,窗口小部件会分派带有 eCompositionChange 事件的 WidgetCompositionEvent。当合成字符串正在更改时,它将导致 DOM compositionupdate 事件。这是由 TextComposition 自动分派。之后,当获得焦点的编辑器的窗口小部件和 PresShell 尚未被销毁时,eCompositionChange 将导致一个 DOM 文本事件,该事件不在任何 Web 标准中。

当本地 IME 通知 Gecko 合成结束时,窗口小部件会分派带有 eCompositionCommitAsIseCompositionCommitWidgetCompositionEvent。如果提交的字符串与最后一组数据不同(即,如果事件消息为 eCompositionCommit),TextComposition 会分派 DOM compositionupdate 事件。之后,当获得焦点的编辑器的窗口小部件和 PresShell 尚未被销毁时,TextComposition 分派 eCompositionChange 事件,从而导致 DOM 文本事件。最后,如果获得焦点的编辑器的窗口小部件和 PresShell 也尚未被销毁,TextComposition 会分派 eCompositionEnd 事件,这将导致 DOM compositionend 事件。

处理合成的限制

目前,EditorBase 在接收每个 WidgetCompositionEvent 时都会接触撤消堆栈。因此,在发生以下情况时,EditorBase 会请求提交合成

  • 编辑器失去焦点

  • 插入符号通过鼠标或 Javascript 移动

  • 编辑器的值通过 Javascript 更改

  • 编辑器的节点从 DOM 树中移除

  • HTML 编辑器中的一些对象被修改,例如调整图像大小

  • 合成字符串移动到由本地 IME 指定的不同位置(例如,仅提交合成的一部分)

将来,我们应该修复此限制。如果我们使 EditorBase 在合成提交之前不接触撤消堆栈,则必须修复某些情况。

向 IME 发送通知

Gecko 的 XP 部分使用 nsIWidget::NotifyIME() 来通知 widget 对处理 IME 有用的某些内容。请注意,只有当 nsIWidget::GetIMEUpdatePreference() 返回请求通知的标志时,才会发送其中一些通知。

NOTIFY_IME_OF_TEXT_CHANGENOTIFY_IME_OF_SELECTION_CHANGENOTIFY_IME_OF_POSITION_CHANGENOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED 始终按以下顺序发送

  1. NOTIFY_IME_OF_TEXT_CHANGE

  2. NOTIFY_IME_OF_SELECTION_CHANGE

  3. NOTIFY_IME_OF_POSITION_CHANGE

  4. NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED

如果发送上述通知之一会导致更高优先级的通知,则发送方应中止发送剩余的通知并从高优先级通知重新开始。

此外,除 NOTIFY_IME_OF_BLUR 之外的所有通知都应仅在安全运行脚本时发送,因为该通知可能会导致查询内容和/或分派合成事件。

NOTIFY_IME_OF_FOCUS

当可编辑编辑器获得焦点并且 IMEContentObserver 开始观察它时,会将其发送到窗口小部件。这必须在先前的 IMEContentObserver 通知窗口小部件 NOTIFY_IME_OF_BLUR 之后调用。

请注意,即使有挂起的通知,当发送 NOTIFY_IME_OF_FOCUS 时,它们也会被取消,因为在获得焦点后立即使用以下通知查询内容没有意义。结果始终与接收此通知时查询内容的结果相同。

NOTIFY_IME_OF_BLUR

IMEContentObserver 实例结束观察获得焦点的编辑器时,会将其同步发送到 widget,因为假定此通知既不会导致查询内容事件也不会导致合成事件。

如果 widget 即使在所有窗口都处于非活动状态时也希望接收通知,则 IMEContentObserver 不会结束观察获得焦点的编辑器。即,在这种情况下,当具有合成的窗口被激活或失活时,不会将 NOTIFY_IME_OF_FOCUSNOTIFY_IME_OF_BLUR 发送到 widget

widget 在非活动期间希望接收通知时,widget 会将 NOTIFY_DURING_DEACTIVE 包含到 nsIWidget::GetIMEUpdatePreference() 的结果中。

如果尝试在发送 NOTIFY_IME_OF_FOCUS 之前发送此通知,则所有挂起的通知和 NOTIFY_IME_OF_BLUR 本身都会被取消。

NOTIFY_IME_OF_TEXT_CHANGE

当获得焦点的编辑器的文本发生更改时,会将其与更改范围一起发送到 widget。但只有当 nsIWidget::GetIMEUpdatePreference() 的结果包含 NOTIFY_TEXT_CHANGE 时才会发送此通知。

如果在先前的 NOTIFY_IME_OF_TEXT_CHANGENOTIFY_IME_OF_FOCUS 之后发生了两个或多个文本更改,则所有更改的范围将合并。例如,如果第一个更改是从 15,第二个更改是从 510,则通知的范围是从 110

如果所有合并的文本更改都是由合成引起的,则 IMENotification::mTextChangeData::mCausedOnlyByComposition 将设置为 true。如果本地 IME 处理程序希望忽略本地 IME 预期的所有文本更改,这将很有用。

如果合并的文本更改中至少有一个文本更改是由当前合成引起的,则 IMENotification::mTextChangeData::mIncludingChangesDuringComposition 将设置为 true。如果本地 IME 处理程序希望忽略延迟的文本更改通知,这将很有用。

如果合并的文本更改中至少有一个文本更改是在没有合成的情况下引起的,则 IMENotification::mTextChangeData::mIncludingChangesWithoutComposition 将设置为 true。

NOTIFY_IME_OF_SELECTION_CHANGE

当获得焦点的编辑器中的选择(或插入符号位置)发生更改时,会将此通知到窗口小部件。

如果上次选择更改是由合成事件处理引起的,则 IMENotification::mSelectionChangeData::mCausedByComposition 将设置为 true。如果本地 IME 处理程序希望忽略本地 IME 预期的上次选择更改,这将很有用。

如果上次选择更改是由 eSetSelection 事件引起的,则 IMENotification::mSelectionChangeData::mCausedBySelectionEvent 将设置为 true。如果本地 IME 处理程序希望忽略本地 IME 请求的上次选择更改,这将很有用。

如果上次选择是在合成期间发生的,则 IMENotification::mSelectionChangeData::mOccurredDuringComposition 将设置为 true。如果本地 IME 处理程序希望忽略由 Web 应用程序的 compositionstartcompositionupdate 事件处理程序在插入合成字符串之前发生的上次选择更改,这将很有用。

NOTIFY_IME_OF_POSITION_CHANGE

当文档中发生回流或滚动时,会将其发送到窗口小部件,但只有当 nsIWidget::GetIMEUpdatePreference() 的结果包含 NOTIFY_POSITION_CHANGE 时才会发送此通知。

这可能有助于更新候选窗口位置或其他内容。

NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED

TextComposition 处理 eCompositionStarteCompositionChangeeComposiitionCommiteCompositionCommitAsIs 之后,会将此通知发送到窗口小部件。这可能有助于更新候选窗口位置或其他内容。

通知 IME 鼠标按钮事件

当聚焦的编辑器中的字符触发 mousedown 事件或 mouseup 事件时,此事件会发送到 widget。但只有当 nsIWidget::GetIMEUpdatePreference() 的结果包含 NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR 时才会发送此事件。它会发送各种信息。有关详细信息,请参阅 IMEData.h 中的 IMENotification::mMouseButtonEventData

如果原生 IME 支持鼠标按钮事件处理,则 widget 应使用此方法通知 IME 鼠标按钮事件。如果 IME 消费了事件,则 widget 应从 nsIWidget::NotifyIME() 返回 NS_SUCCESS_EVENT_CONSUMED。然后,EditorBase 不会处理鼠标事件。

请注意,如果 mousedown 事件或 mouseup 事件被 Web 应用程序(在聚焦的编辑器处理它之前)消费,则此通知不会发送到 widget。这意味着 Web 应用程序可以在 IME 之前处理鼠标按钮事件。

对 IME 的请求

Gecko 的 XP 部分可以请求 IME 提交或取消合成。此请求必须通过 IMEStateManager::NotifyIME() 进行。然后,IMEStateManager 会查找合适的 TextComposition 实例。如果找到,则 TextComposition::RequestToCommit() 用于调用 nsIWidget::NotifyIME() 并处理一些额外的工作。

如果可用,widget 应该调用相应的原生 API。即使提交或取消合成不是同步发生的,widget 也无需模拟它,因为 TextComposition 会自动模拟它。换句话说,widget 应该只请求 IME 提交或取消合成。

REQUEST_TO_COMMIT_COMPOSITION

请求将当前合成提交到 IME。有关其他信息,另请参阅以下“强制提交合成”部分。

REQUEST_TO_CANCEL_COMPOSITION

请求取消当前合成到 IME。换句话说,请求使用空字符串提交当前合成。

强制提交合成

TextComposition::RequestToCommit() 调用 nsIWidget::NotifyIME() 时,它保证同步提交或取消合成。

为了将其付诸实践,我们需要处理以下四种情况

合成以非空字符串同步提交

这是最常见的情况。在这种情况下,TextComposition 在请求期间正常处理 WidgetCompositionEvent 实例。但是,在 e10s 模式下的远程进程中,这种情况永远不会发生,因为对原生 IME 的请求是异步处理的。

合成不是同步提交,而是稍后提交

这是 e10s 模式下的远程进程中唯一可能发生的情况,或者在非 e10s 模式下的 Linux 上发生,如果原生 IME 是 iBus。 NotifyIME(REQUEST_TO_COMMIT_COMPOSITION) 的调用者可能会期望合成字符串立即提交以进行下一步操作。对于这种情况,TextComposition::RequestToCommit() 会合成 DOM 合成事件和 DOM 文本事件以模拟同步提交合成。此外,当 widget 接收原生 IME 事件时,TextComposition 会忽略 widget 分派的提交事件。

在这种情况下,使用最后一个合成字符串作为提交字符串。

但是,如果最后一个合成字符串只是一个全角空格(全角空格),则合成字符串可能是 Windows 上某些旧版中文 IME 的占位符。

aScreenshot of ChangJie (Traditional Chinese IME) which puts an ideographic space into composition string for placeholder

在这种情况下,虽然我们不应该提交占位符字符,因为它不是用户想要输入的字符,但我们按原样提交它。原因是,输入全角空格会导致合成。因此,我们无法区分提交合成是否意外。如果用户使用此类旧版中文 IME,则 "intl.ime.remove_placeholder_character_at_commit" 首选项可能有用,但我们不再在默认设置中支持它们(除非有人找到解决此问题的好方法)。

合成以空字符串同步提交

这种情况可能在 Linux 上或其他平台上的一些 IME 中发生。如果 Web 应用程序实现了自动完成功能,则使用不同的字符串(尤其是空字符串)进行提交可能会导致混淆。

在这种情况下,TextComposition 会覆盖 widget 分派的 eCompositionChange 事件的提交字符串。但是,如果最后一个合成字符串只是一个全角空格,则不应提交它。请参阅上一案例。

请注意,当合成在 e10s 模式下的远程进程中时,此案例无法按预期工作。

合成未提交

在 Linux 上,没有 API 可以强制请求提交或取消合成。相反,Gecko 使用 gtk_im_context_reset() API 用于此目的,因为大多数 IME 使用它取消合成。但有一些 IME 在 Gecko 调用它时什么也不做。

如果发生这种情况,Gecko 应该使用 DOM compositionstart 事件、DOM compositionupdate 事件和 DOM text 事件在插入点处重新启动合成。

注意

此问题尚未得到支持。

IME 状态管理

IME 是一种文本输入系统。这意味着,除非用户想要输入一些文本,否则 IME 不应该可用。例如,按空格键尝试滚动页面可能会被 IME 消费并阻止。此外,密码编辑器需要请求 IME 的特殊行为。

为了解决此问题,Gecko 在 DOM 焦点更改时设置正确的 IME 状态。

首先,当 DOM 节点获得焦点时,nsFocusManager 会通知 IMEStateManager 新的焦点节点(调用 IMEStateManager::OnChangeFocus())。IMEStateManager 通过调用节点的 nsIContent::GetDesiredIMEState() 来询问所需的 IME 状态。如果节点拥有 TextEditor 实例,它会向编辑器询问所需的 IME 状态并返回结果。

接下来,IMEStateManager 使用所需的 IME 状态和节点信息初始化 InputContext(在 IMEData.h 中定义)。然后,它使用 InputContext 调用 nsIWidget::SetInputContext()

最后,widget 存储 InputContext,并在平台具有此类 API 时启用或禁用 IME。

InputContext

InputContext 是一个结构体。它的 mIMEStatemHTMLInputTypemHTMLInputInputModemActionHint 在调用 nsIWidget::SetInputContext() 时设置。

mIMEState

IME 状态具有两种功能。一种是启用状态

ENABLED

这意味着 IME 完全可用。例如,当可编辑元素(如 <input type="text"><textarea><foo contenteditable>)获得焦点时。

DISABLED

这意味着 IME 不可用。例如,当不可编辑元素获得焦点或没有元素获得焦点时,所需的 IME 状态为 DISABLED

PASSWORD

这意味着 IME 状态应与原生密码字段获得焦点时的状态相同。仅当 <input type="password"> (ime-mode: auto;)<input type="text" style="ime-mode: disabled;"><textarea style="ime-mode: disabled;"> 时设置此状态。

另一个是 IME 打开状态

DONT_CHANGE_OPEN_STATE

IME 的打开状态不应更改。即,Gecko 应保持上次 IME 的打开状态。

OPEN

打开 IME。仅当新聚焦元素的 ime-mode 为 active 时指定。

CLOSE

关闭 IME。仅当新聚焦元素的 ime-mode 为 inactive 时指定。

注意

例如,在 Linux 上,应用程序无法管理 IME 打开状态。在这些平台上,此操作会被忽略。

注意

IME 打开状态应仅在 DOM 焦点更改时调用 nsIWidget::SetInputContext() 时更改,因为在编辑器具有焦点时更改 IME 打开状态会让用户感到困惑。调用 nsIWidget::SetInputContext() 的原因存储在 InputContextAction::mCause 中。

Gecko 如何在 Windows 上的 IMM 模式下禁用 IME

Windows 上的每个窗口都关联了一个 IMContext。当 Gecko 禁用 IME 时,mozilla::widget::IMEHandler::SetInputContext() 会将上下文与窗口分离。

Gecko 如何在 Windows 上的 TSF 模式下禁用 IME

mozilla::widget::TSFTextStore 将焦点设置为一个虚拟上下文,该上下文禁用键盘。

Gecko 如何在 Mac 上禁用 IME

mozilla::widget::TextInputHandler::HandleKeyDownEvent() 不会调用聚焦视图的 interpretKeyEvents。这会阻止将原生键盘事件传递到 IME。

Gecko 如何在 GTK 上禁用 IME

mozilla::widget::IMContextWrapper 将焦点设置为一个虚拟上下文,该上下文没有 IME 合成。

Gecko 如何在 Android 上禁用 IME

?

mHTMLInputType

该值是一个表示聚焦编辑器的字符串。

"text""password""number" 等。

<input> 元素获得焦点时,该值是输入元素的类型。

"textarea"

<textarea> 元素获得焦点时,该值为 "textarea"

""

当 HTML 编辑器(contenteditable 属性为 true 的元素或 designMode"on" 的文档)获得焦点时,该值为为空。当其他元素获得焦点时也是如此。

mHTMLInputMode

该值为聚焦编辑器的 inputmode 属性值。

mActionHint

该值是当 "dom.forms.enterkeyhint" 首选项为 true 时,获得焦点的编辑器的 enterkeyhint 属性值。这对于确定虚拟键盘中提交按钮的标题很有用。例如,该值可以是 "Go""Next""Search"

原生输入法处理器

以下类处理每个平台上的输入法

Windows

mozilla::widget::IMEHandler

此类管理每个窗口的输入法上下文,并使 IMMHandlerTSFTextStore 与活动的输入法和获得焦点的编辑器一起工作。此类仅包含静态成员,即从未创建其实例。

mozilla::widget::IMMHandler

当 TSF 模式被首选项禁用(从 108 版本开始为 "intl.tsf.enabled",以前称为 "intl.tsf.enable")或活动的输入法为 IMM(即,不是用于 TSF 的 TIP)时,将使用此类。

此类处理 WM_IME_* 消息并使用 Imm*() API。这是一个单例类,因为 Gecko 在一个进程中只支持一个输入法上下文。通常,一个进程会创建带有默认输入法上下文的窗口。因此,这种设计就足够了(理想情况下,应该为每个输入法上下文创建一个实例,尽管如此)。当需要时会创建单例实例。

mozilla::widget::TSFTextStore

此类在 TSF 模式下以及 TIP(使用 TSF 实现的输入法)处于活动状态时处理输入法事件。当可编辑元素获得焦点时创建这些实例,并在失去焦点时释放。

TSFTextStore 实现了一些 COM 接口,这些接口对于与 TIP 一起工作是必要的。类似地,还有一个单例类 TSFStaticSink 用于观察活动 TIP 的变化。

TSF 是所有平台上最复杂的输入法 API,因此,此类的设计也非常复杂。

首先,TSF/TIP 请求锁定编辑器内容以查询或修改内容或选择。但是,Web 标准没有这样的机制。因此,当请求时,TSFTextStore 使用 WidgetQueryContentEvent 缓存当前内容和选择。然后,它使用缓存来回复查询请求,并根据请求修改缓存。此时,TSFTextStore 将修改请求保存到名为 PendingAction 的队列中。最后,在解锁内容后,它使用 TextEventDispatcher 通过分派 WidgetCompositionEvent``s 来刷新挂起的操作。

然后,IMEContentObserver 将通知由分派的 WidgetCompositionEvents 引起的一些更改(在 chrome 或非 e10s 模式下同步通知,但在 e10s 模式下异步通知来自远程进程)。此时,TSFTextStore 可能会收到通知,这些通知表明 Web 应用程序以与 TSFTextStore 中的缓存不同的方式更改内容。但是,TSFTextStore 会暂时忽略此事实,直到合成完全完成。原因是,在合成过程中向 TSF 和/或 TIP 通知意外的文本或选择更改可能会导致它们出现异常行为。

当合成提交并且它收到 NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED 时,TSFTextStore 清除内容缓存并通知 TSF 合并的文本更改和最后的选择更改(如果它们不是由合成引起的)。通过此步骤,TSF 和 TIP 可以将其内部缓存与实际内容同步。

请注意,如果在 NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED 通知之前开始新的合成,则 TSFTextStore 将使用可能与实际内容不同的缓存内容处理合成。因此,例如,在这种情况下,插入点周围的重新转换可能无法按预期工作,但我们对此问题没有好的解决方案。

另一方面,TSFTextStore 无法缓存字符矩形,因为如果字符很多,缓存矩形需要大量的 CPU 成本(计算每个矩形)和内存。因此,TSFTextStore 将对它们使用插入点相对查询 错误 1286157。然后,即使 TSFTextStore 的缓存与实际内容不同,它也可以检索预期字符的矩形,因为 TIP 通常需要插入点字符的矩形(用于弹出窗口以指示当前输入模式或下一个单词建议列表)或当前合成的目标子句的第一个字符的矩形(用于转换的候选列表窗口)。

Mac

IME 和按键事件都在 TextInputHandler.mm 中处理。

mozilla::widget::TextInputHandlerBase 是最基本的类。mozilla::widget::IMEInputHandler 继承 TextInputHandlerBase 并处理与 IME 相关的事件。mozilla::widget::TextInputHandler 继承 TextInputHandlerBase 并实现 Cocoa 的 NSTextInput 协议。其实例为每个 nsChildView 实例创建。

GTK

mozilla::widget::IMContextWrapper 处理输入法。该实例为每个顶级窗口创建。

Android

org.mozilla.geckoview.GeckoEditable 处理原生输入法事件,而 mozilla::widget::GeckoEditableSupport 分派 Widget*Event