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();
		}
	}
}