TextAreaのundoの実装2
というわけで実装。これを書いているとだんだん他の方法で書き直したくなってくる、ふしぎ!
実装するために検出しなければいけないイベントは
- KeyboardEvent.KEY_DOWN
- Event.CHANGE
- Event.CUT
- Event.PASTE
の4つ。テキスト自体が変わる操作が対象。
例によってThreadを使うのだが、undo実装はThreadとはとても相性が悪いらしい。以前も書いたと思うが、event関数で同じコントロールにKeyboardEvent.KEY_DOWNとEvent.CHANGEを仕込むとKEY_DOWNしか返ってこないという現象があるので、最低でも2つのThreadが必要となる。
一応、textが変わると考えられる操作を挙げておく。
- 範囲選択なし/ありの状態で1文字入力。
- 範囲選択なし/ありの状態でBackSpaceを押下。
- 範囲選択なし/ありの状態でDeleteを押下。
- 範囲選択ありの状態でカット。
- 範囲選択なし/ありの状態でペースト。
Event.CHANGE
履歴の追加。ここでは差分を得なければならないが、差分の得方にも2通りある。
- 安直に操作前後のdiffをとる。
- どの操作かを推測して差分をとる。
1番目の方法は安直にやればO(L^2)の実行時間がかかる。O(LN)のメモリを避けているのにこれはない。というわけで2番目をやる。
記録する差分の形式は、幸いにして連結した箇所しか増減しないので、Object型で、プロパティは
- minus : 操作により消えた文字列
- plus : 操作により増えた文字列
- index : 操作前のカーソル位置
とする。
taをTextAreaとする。まず、KEY_DOWN, CUT, PASTEの時点(まだ操作が行われていない時点)のtext, selectionBeginIndexとselectionEndIndexを記録しておく。
private function prepareHistory() : void { prevselectionBeginIndex = ta.selectionBeginIndex; prevselectionEndIndex = ta.selectionEndIndex; }
これをprev, prevselectionBeginIndex, prevselectionEndIndexとする。そしてCHANGEのハンドラ(操作後の時点)で
if (ta.selectionBeginIndex == ta.selectionEndIndex) { var minus : String; var plus : String; if (ta.selectionBeginIndex < prevselectionBeginIndex) { // 1文字backspace minus = prev.substr(prevselectionBeginIndex - 1, 1); plus = ""; }else if (ta.selectionBeginIndex == prevselectionBeginIndex && prevselectionBeginIndex == prevselectionEndIndex) { // 1文字delete minus = prev.substr(prevselectionBeginIndex, 1); plus = ""; }else{ minus = prev.substring(prevselectionBeginIndex, prevselectionEndIndex); plus = ta.text.substring(prevselectionBeginIndex, ta.selectionBeginIndex); } if(!(minus == "" && plus == "")){ var item : Object = {minus : minus, plus : plus, index : prevselectionBeginIndex}; // trace(ObjectUtil.toString(item).replace(/\n/g, "\t")); history.push(item); // 履歴が長すぎたら前から消去 if (history.length > 1000) { history.shift(); } } }
と書く。historyは履歴。1文字backspaceと1文字deleteは、範囲選択をしていなくても文字が消えるので特別扱い。
これでは、prevという、ta.textと同じ長さのバッファが必要になるので、prevから得られる文字列をあらかじめ取得しておくことにする。(別に気にするほどの占有量でもないと思うが)
private function prepareHistory() : void { prevselectionBeginIndex = ta.selectionBeginIndex; prevselectionEndIndex = ta.selectionEndIndex; bs = ta.text.substr(prevselectionBeginIndex - 1, 1); del = ta.text.substr(prevselectionBeginIndex, 1); normal = ta.text.substring(prevselectionBeginIndex, prevselectionEndIndex); }
if (ta.selectionBeginIndex == ta.selectionEndIndex) { var minus : String; var plus : String; if (ta.selectionBeginIndex < prevselectionBeginIndex) { // 1文字backspace minus = bs; plus = ""; }else if (ta.selectionBeginIndex == prevselectionBeginIndex && prevselectionBeginIndex == prevselectionEndIndex) { // 1文字delete minus = del; plus = ""; }else{ minus = normal; plus = ta.text.substring(prevselectionBeginIndex, ta.selectionBeginIndex); } if(!(minus == "" && plus == "")){ var item : Object = {minus : minus, plus : plus, index : prevselectionBeginIndex}; // trace(ObjectUtil.toString(item).replace(/\n/g, "\t")); history.push(item); // 履歴が長すぎたら前から消去 if (history.length > 1000) { history.shift(); } } }