當在 TextInput, TextArea 等文字組件設定 maxChars 最大字數屬性
使用中文輸入法打了數個字到文字佇列上,超過最大限制字數
然後用 Ctrl + Space 切換輸入法或是按下 Enter 方式輸入文字
結果會發現其它 Binding 到 text 屬性的目標無法取得正確的 text 字串
之後再利用按鈕事件 trace 組件的 text 屬性也是取到錯誤的字串資料

來回 trace 好多程式碼位置之後,發現問題主要是出在 RichEditableText 組件上
不過由於問題原因非常複雜,所以要分成很多段落先解釋一些 TLF 與文字組件運作方式

為什麼要花這麼大力氣找原因?
除了解決問題之外,主要還想要了解一下 TLF 文字引擎的運作方式
令我最驚訝的是,輸入法待選文字佇列功能,居然是 TLF 內建,完全用 AS3 寫的!

相關 Bug Report
http://bugs.adobe.com/jira/browse/SDK-28999
http://www.fxug.net/modules/xhnewbb/viewtopic.php?topic_id=4338

RichEditableText maxChars 與輸入法問題測試程式:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
     xmlns:s="library://ns.adobe.com/flex/spark" 
     xmlns:mx="library://ns.adobe.com/flex/mx" fontSize="14">
 <s:layout>
  <s:HorizontalLayout verticalAlign="middle" horizontalAlign="center" />
 </s:layout>
 
 <s:VGroup gap="12">
  <s:Label text="TextInput" />
  <s:TextInput id="txt1" contentBackgroundColor="#DDDDDD"
    maxChars="3" widthInChars="8" />
  <s:TextInput text="{txt1.text}"/>
  <s:Label text="txt1.text: {txt1.text}"/>
  <s:Button label="trace(txt1.text);" click="trace(txt1.text);"/>
 </s:VGroup>
 
 <s:VGroup gap="12">
  <s:Label text="TextArea" />
  <s:TextArea id="txt2" contentBackgroundColor="#DDDDDD"
    maxChars="3" heightInLines="1" widthInChars="8" />
  <s:TextInput text="{txt2.text}"/>
  <s:Label text="txt2.text: {txt2.text}"/>
  <s:Button label="trace(txt2.text);" click="trace(txt2.text);"/>
 </s:VGroup>
 
 <s:VGroup gap="12">
  <s:Label text="RichEditableText" />
  <s:RichEditableText id="txt3" backgroundColor="#DDDDDD"
    paddingLeft="4" paddingRight="4" paddingTop="4" paddingBottom="4"
    maxChars="3" heightInLines="1" widthInChars="8" />
  <s:TextInput text="{txt3.text}"/>
  <s:Label text="txt3.text: {txt3.text}"/>
  <s:Button label="trace(txt3.text);" click="trace(txt3.text);"/>
 </s:VGroup>
</s:Application>

※ RichEditableText 內部 TLF 文字管理
RichEditableText 本身並不直接控制文字表現
而是透過內部一個 TLF TextContainerManager 實體
負責表現、捲動、編輯文字以及文字輸入法佇列
每當文字發生變動
TextContainerManager 有三個事件會依序發出,當然還有其它的 Damage, Composition 等事件

FlowOperationEvent.FLOW_OPERATION_BEGIN
FlowOperationEvent.FLOW_OPERATION_END
FlowOperationEvent.FLOW_OPERATION_COMPLETE

RichEditable 內三個私有偵聽函式負責處理

textContainerManager_flowOperationBeginHandler
處理完立即發出 TextOperationEvent.CHANGING 事件

textContainerManager_flowOperationEndHandler

textContainerManager_flowOperationCompleteHandler
處理完立即發出 TextOperationEvent.CHANGE 事件

在 textContainerManager_flowOperationBeginHandler 內
也會檢查插入文字 + 原本文字總長是否超出 maxChars,並加以裁切
輸入法問題就是出在這裡

※ RichEditableText.text 屬性的內部 cache
RichEditableText 內部用一個私有變數存放 Plain Text 叫做 _text
每當 textFlow, content 被重新指定或是 damage 事件發生時,_text 會被重設為 null
假如取用 text 時 _text 為 null,會從 TextFlow 重新 export plain text 並存放到 _text

※ 從 TextFlow 取得 Plain 文字的方式
var staticPlainTextExporter:ITextExporter =
TextConverter.getExporter(TextConverter.PLAIN_TEXT_FORMAT);
var txt:String = staticPlainTextExporter.export(textFlow, ConversionType.STRING_TYPE) as String;

// 或是

textFlow.getText();

※ RichEditableText maxChars 與輸入法問題的成因
IME 輸入法輸入文字到佇列,然後確認文字是一連串複雜的動作
由於佇列文字是由 TLF 負責表現
當確認佇列文字時,必須先銷毀 TLF 佇列文字
然後復原畫面到不包含佇列文字的狀況
最後再合併輸入輸入文字

以上動作會在 FlowOperationEvent.FLOW_OPERATION_BEGIN 事件後開始進行
預設情況下,由於輸入佇列文字動作比較複雜
TLF EditManager 不會立刻完成所有的動作,而是會透過影格事件分批完成
然後才發出 FlowOperationEvent.FLOW_OPERATION_END 事件
導致此時從 TextFlow 取得的文字並非最後的結果

假如又要檢查 text.length 是否超過 maxChars 的話
會觸發 export plain text 存到 _text 快取
之後完全沒有更新的 _text 快取機會
導致取到的 text 資料都是舊的

※ 為什麼 Flex 4.0 沒有這問題
基本上 RichEditableText 沒有很大差異
而 Flex 4.0 是使用 TLF 1.1,Flex 4.5 是使用 TLF 2.0
主要是 TLF 1.1 TextContainerManager 最後完成所有操作之後
都會再多發一次 Damage 事件,讓 _text 快取被清掉了

※ 強迫 TLF EditManager 立刻完成所有的動作的方式
(Spark ComboBox 會設定 batchTextInput = false 所以才避開了這個問題)

RichEditableText Class:

/**
* @private
* The TLF edit manager will batch all inserted text until the next
* enter frame event. This includes text inserted via the GUI as well
* as api calls to EditManager.insertText(). Set this to false if you
* want every keystroke to be inserted into the text immediately which will
* result in a TextOperationEvent.CHANGE event for each character. One
* place this is needed is for the type-ahead feature of the editable combo
* box.
*/
mx_internal var batchTextInput:Boolean = true;
以上屬性會在 RichEditableTextContainerManager createEditManager 時
設定到 EditManager 上
editManager.allowDelayedOperations = textDisplay.batchTextInput;

※ 問題解決方式
方式 1.
修改 RichEditableText 內的 textContainerManager_flowOperationBeginHandler 函式
取用完 text.length 之後,指定 _text 為 null

if (maxChars != 0) {
var length1:int = text.length – delLen;
var length2:int = textToInsert.length;
if (length1 + length2 > maxChars)
textToInsert = textToInsert.substr(0, maxChars – length1);
_text = null;
}
方式 2.
在 RichEditableText 組件 change 事件上,重新指定 textFlow,強迫更新 _text 快取

var tf:TextFlow = event.target.textFlow;
event.target.textFlow = new TextFlow();
event.target.textFlow = tf;
Spark TextInput, TextArea 可以在自訂 Skin 裡面添加以上程式

方式 3. (建議)
在 RichEditableText 組件 preinitialize 事件中,設定 batchTextInput 為 false

event.target.mx_internal::batchTextInput = false;
Spark TextInput, TextArea 可以在自訂 Skin 裡面添加以上程式