IME 处理指南¶
本文档解释了 Gecko 如何处理 IME。
简介¶
IME 是输入法编辑器的缩写。这是一个来自 Windows 的技术术语,但如今在其他平台上也使用它。
IME 是用户文本输入的辅助应用程序。它在聚焦应用程序之前或之后(取决于平台)处理原生键盘事件,并创建组合字符串(也称为预编辑字符串),建议用户尝试输入的内容列表,将组合字符串作为列表中的选定项提交,并在没有任何转换的情况下提交组合字符串。IME 被中国、日本、韩国和台湾用户用来输入汉字,因为汉字的数量超过数千个,无法直接从键盘输入。然而,尤其是在当今的移动设备上,IME 也被用于输入拉丁语,例如自动完成。此外,IME 可以在某些平台上用于手写系统或语音输入系统。
如果聚焦元素上启用了 IME,我们称该状态为“已启用”。如果 IME 无法完全使用(即用户无法启用 IME),我们称该状态为“已禁用”。
如果 IME 已启用,但用户使用直接输入模式(例如,输入拉丁字符),我们称之为“IME 已关闭”。否则,我们称之为“IME 已打开”。(仅供参考:“打开”也称为“激活”或“已打开”。“关闭”也称为“未激活”或“已关闭”)
因此,当您尝试修复 Gecko 中文本输入的错误时,本文档很有用。
组合字符串和分句¶
典型的日语 IME 可以将两个或多个单词输入到组合字符串中。当用户将平假名字符转换为汉字时,日语 IME 会将组合字符串分成多个分句。例如,如果用户输入“watasinonamaehanakanodesu”,它会自动转换为平假名字符“わたしのなまえはなかのです”(在下面的屏幕截图中,组合字符串带有波浪下划线,只有一个分句称为“原始输入分句”)。
当用户按下Convert
键时,日语 IME 将组合字符串分成“わたしの”(我的),“なまえは”(名字是)和“なかのです”(中野)。然后,使用汉字转换每个分句:“私の”,“名前は”和“中野です”(在下面的屏幕截图中,每个分句都带下划线,并且不连续连接。这些分句称为“已转换分句”)。
如果一个或多个分句未按预期转换,用户可以使用方向键选择其中一个分句,并在下拉菜单中查找预期的结果(在下面的屏幕截图中,带粗下划线的句称为“已选择分句”)。
基本上,组合字符串和每个分句的样式由 Gecko 呈现。下拉菜单由 IME 创建。
每个分句都用编辑器中的选择表示。从 chrome 脚本中,您可以使用nsISelectionController
检查它。在原生代码中,您可以使用nsISelectionController
或mozilla::SelectionType
访问它(后者推荐使用,因为它类型更安全)。编辑器从mozilla::TextRangeType
设置这些 IME 选择,这些选择由mozilla::WidgetCompositionEvent
作为mozilla::TextRangeArray
发送。下表解释了它们之间的映射关系。
插入符号 |
|
|
|
用户输入的原始文本 |
|
|
|
用户输入的原始文本的选定分句 |
|
|
|
IME 转换的分句 |
|
|
|
用户或 IME 选择的分句,并且也由 IME 转换 |
|
|
|
请注意,通常不使用“用户输入的原始文本的选定分句”,因为当组合字符串已被分成多个分句时,这意味着组合字符串至少已由 IME 转换过一次。
处理 IME 组合的模块¶
小部件¶
每个小部件都处理原生 IME 事件,并使用mozilla::widget::TextEventDispatcher
分派WidgetCompositionEvent
来表示聚焦编辑器中 IME 的行为。
这是唯一依赖于用户平台的模块。有关每个平台实现的详细信息,另请参见原生 IME 处理程序部分。
注意
Android 小部件仍不使用TextEventDispatcher
来分派WidgetCompositionEvents
,请参见错误 1137567。
mozilla::widget::TextEventDispatcher¶
此类由每个平台上的原生 IME 处理程序使用。它封装了分派WidgetCompositionEvent
和WidgetKeyboardEvent
的逻辑,以使每个平台上的行为完全相同。例如,如果在 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¶
当分派eCompositionCommitAsIs
或eCompositionCommit
事件时,TextComposition
会分派此事件。这表示 DOM compositionend
事件。
eCompositionChange¶
仅在内部使用。在修改组合字符串、提交组合、更改插入符号位置和/或更改分句范围时分派。这表示 DOM 文本事件,该事件不在任何标准中。mRanges
在此消息中不应为空。
eCompositionCommitAsIs¶
仅在内部使用。当组合使用字符串提交时分派。mData
值应始终为空字符串。这会导致不带分句信息的 DOM 文本事件和 DOM compositionend
事件。
eCompositionCommit¶
仅在内部使用。当组合使用特定字符串提交时分派。mData
值是提交字符串。这会导致不带分句信息的 DOM 文本事件和 DOM compositionend
事件。
mData 的含义 |
谁设置 |
|
表示 DOM 事件 |
|
---|---|---|---|---|
|
开始组合前的选定字符串 |
|
|
|
|
新的组合字符串 |
|
|
|
|
提交字符串 |
|
|
|
|
新的组合字符串 |
小部件(或 |
不能是 |
|
|
N/A(必须为空) |
没有人 |
|
None |
|
提交字符串 |
小部件(或 |
|
None |
PresShell¶
PresShell
接收小部件事件,并从聚焦文档和元素中确定事件目标。然后,它将事件和事件目标发送到IMEStateManager
。
mozilla::IMEStateManager¶
IMEStateManager
会查找一个其原生 IME 上下文与分发小部件事件的小部件相同的 TextComposition
实例。如果没有合适的 TextComposition
实例,它会创建该实例。然后,它将事件发送到 TextComposition
实例。
请注意,所有 TextComposition
实例都由 IMEStateManager
管理。创建实例时,会将其注册到列表中。当合成完全结束时,它将从列表中注销(并自动释放)。
mozilla::TextComposition¶
TextComposition
管理合成并分发 DOM compositionupdate
事件。
当它接收到 eCompositionChange
、eCompositionCommit
或 eCompositionCommitAsIs
事件时,它会将事件分发到存储的节点,该节点是 eCompositionStart
事件的事件目标。因此,此类保证同一合成的所有合成事件都将在相同的元素上触发。
当它接收到 eCompositionChange
或 eCompositionCommit
时,它会检查新的合成字符串(或提交字符串)是否与 TextComposition
存储的最后数据不同。如果合成事件正在更改合成字符串,则 TextComposition
实例会直接向 DOM 树分发带有 eCompositionUpdate
的 WidgetCompositionEvent
并修改最后的数据。 eCompositionUpdate
事件将导致 DOM compositionupdate
事件。
当它接收到 eCompositionCommitAsIs
或 eCompositionCommit
时,它会分发一个 eCompositionEnd
事件,该事件将在分发 eCompositionUpdate
事件和/或 eCompositionChange
事件(如果必要)后导致 DOM compositionend
事件。
它的另一个重要工作是,当聚焦的编辑器处理分发的 eCompositionChange
事件时,它会修改存储的合成字符串及其子句信息。编辑器引用存储的信息来创建或修改表示合成字符串的文本节点。
并且在分发 eComposition*
事件之前,此类会从默认设置中分发合成事件数据的 ASCII 控制字符中删除。尽管可以通过 "dom.compositionevent.allow_control_characters"
首选项禁用此功能。
最后,此类保证同步执行请求向 IME 提交或取消当前合成的操作。有关详细信息,请参阅 强制提交合成 部分。
editor/libeditor¶
mozilla::EditorEventListener 侦听受信任的 DOM compositionstart
、text
和 compositionend
事件,并将事件通知 mozilla::EditorBase 和 mozilla::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
观察聚焦编辑器的各种更改。当 TextEditor
或 HTMLEditor
实例的对应元素获得焦点时, 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¶
这是 ContentCacheInChild
和 ContentCacheInParent
的基类,并且支持 IPC。它包含它们的公共成员,包括所有缓存数据:
mText
聚焦编辑器中的全部文本。这可能太大,但 IME 可能会请求编辑器中的所有文本。
如果我们能够按段落分隔编辑器内容,则段落之间的选择移动会生成伪焦点移动,我们可以减少
ContentEventHandler
的大小和运行时成本。但是,我们还没有计划这样做。请注意,Microsoft Word 使用此技巧。mCompositionStart
mText 中合成字符串的偏移量。当没有合成时,它是
UINT32_MAX
。mSelection::mAnchor
、mSelection::mFocus
mText 中选择锚点和焦点的偏移量。
mSelection::mWritingMode
选择开始处的书写模式。
mSelection::mAnchorCharRect
、mSelection::mFocusCharRect
mSelection::mAnchor
和mSelection::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
。如果所有发送的 WidgetCompositionEvents
和 WidgetSelectionEvents
已经在远程进程中处理,则 ContentCacheInParent
会将通知发送到小部件。
此外,它还使用其缓存处理 WidgetQueryContentEvents
。它们支持的事件消息为:
eQuerySelectedText
(仅适用于SelectionType::eNormal
)eQueryTextContent
eQueryTextRect
eQueryCaretRect
eQueryEditorRect
此外,它不支持带有 XP 换行符的内容查询事件,但这不应有任何问题,因为原生 IME 处理程序使用原生换行符查询内容。
ContentCacheInParent
还管理发送的 WidgetCompositionEvents
和 WidgetSelectionEvents
。这些事件在远程进程中处理后, 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
才会存储在 WidgetCompositionEvent
的 mRanges
中。
合成字符串的生命周期¶
当本地 IME 通知 Gecko 开始合成时,窗口小部件会分派带有 eCompositionStart
的 WidgetCompositionEvent
,这将导致 DOM compositionstart
事件。
当本地 IME 通知 Gecko 合成字符串更改、插入符号位置更改和/或分句长度更改时,窗口小部件会分派带有 eCompositionChange
事件的 WidgetCompositionEvent
。当合成字符串正在更改时,它将导致 DOM compositionupdate
事件。这是由 TextComposition
自动分派。之后,当获得焦点的编辑器的窗口小部件和 PresShell
尚未被销毁时,eCompositionChange
将导致一个 DOM 文本事件,该事件不在任何 Web 标准中。
当本地 IME 通知 Gecko 合成结束时,窗口小部件会分派带有 eCompositionCommitAsIs
或 eCompositionCommit
的 WidgetCompositionEvent
。如果提交的字符串与最后一组数据不同(即,如果事件消息为 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_CHANGE
、NOTIFY_IME_OF_SELECTION_CHANGE
、NOTIFY_IME_OF_POSITION_CHANGE
和 NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED
始终按以下顺序发送
NOTIFY_IME_OF_TEXT_CHANGE
NOTIFY_IME_OF_SELECTION_CHANGE
NOTIFY_IME_OF_POSITION_CHANGE
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_FOCUS
和 NOTIFY_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_CHANGE
或 NOTIFY_IME_OF_FOCUS
之后发生了两个或多个文本更改,则所有更改的范围将合并。例如,如果第一个更改是从 1
到 5
,第二个更改是从 5
到 10
,则通知的范围是从 1
到 10
。
如果所有合并的文本更改都是由合成引起的,则 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 应用程序的 compositionstart
或 compositionupdate
事件处理程序在插入合成字符串之前发生的上次选择更改,这将很有用。
NOTIFY_IME_OF_POSITION_CHANGE¶
当文档中发生回流或滚动时,会将其发送到窗口小部件,但只有当 nsIWidget::GetIMEUpdatePreference()
的结果包含 NOTIFY_POSITION_CHANGE
时才会发送此通知。
这可能有助于更新候选窗口位置或其他内容。
NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED¶
在 TextComposition
处理 eCompositionStart
、eCompositionChange
、eComposiitionCommit
或 eCompositionCommitAsIs
之后,会将此通知发送到窗口小部件。这可能有助于更新候选窗口位置或其他内容。
对 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 的占位符。
在这种情况下,虽然我们不应该提交占位符字符,因为它不是用户想要输入的字符,但我们按原样提交它。原因是,输入全角空格会导致合成。因此,我们无法区分提交合成是否意外。如果用户使用此类旧版中文 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 是一个结构体。它的 mIMEState
、mHTMLInputType
、mHTMLInputInputMode
和 mActionHint
在调用 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¶
此类管理每个窗口的输入法上下文,并使 IMMHandler
或 TSFTextStore
与活动的输入法和获得焦点的编辑器一起工作。此类仅包含静态成员,即从未创建其实例。
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
。