forked from: SiON MML Edtor 2

by ohisama forked from SiON MML Edtor 2 (diff: 298)
SiON MML Editor
[USAGE] 
Write MML in the text field and Shift+Enter to play.
All MMLs in all pages are concatenated and play.
Ctrl+Z to undo, Ctrl+Y to redo (codes from psyark's Psycode)
♥0 | Line 751 | Modified 2013-01-28 14:16:39 | MIT License
play

ActionScript3 source code

/**
 * Copyright ohisama ( http://wonderfl.net/user/ohisama )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/cXmB
 */

// forked from keim_at_Si's SiON MML Edtor 2 
// SiON MML Editor
// [USAGE] 
//   Write MML in the text field and Shift+Enter to play.
//   All MMLs in all pages are concatenated and play.
//   Ctrl+Z to undo, Ctrl+Y to redo (codes from psyark's Psycode)
package 
{
    import flash.display.*;
    import flash.events.*;
    import flash.ui.Keyboard;
    [SWF(backgroundColor="#ffffff", frameRate="20")]
    public class main extends Sprite 
    {
        function main() 
        {
            stage.scaleMode = "noScale";
            stage.align = "TL";
            stage.stageFocusRect = false;
            addEventListener("addedToStage", _onAddedToStage);
            stage.addEventListener("resize", _onResize);
            stage.addEventListener("keyDown", _onKeyDown);
            driver.addEventListener("error", _onError);
            control = new ControlPanel(this);
            mmlEditor = new MMLEditor(this, 0, 0, _onTextChange);
            //mmlEditor.mml = "t10";
            pageSelector = new PageSelector(this);
            message = new MessageBox(this);
            searchWindow = new SearchWindow(this);
        }        
        private function _onAddedToStage(event : Event) : void 
        {
            _updateSize();
            loadCookie();
        }        
        private function _onResize(event : Event) : void 
        {
            _updateSize();
        }        
        private function _onKeyDown(event : KeyboardEvent) : void 
        {
            switch (event.keyCode) 
            {
            case Keyboard.ENTER:
                if (event.shiftKey) togglePlay();
            break;
            }
        }
        private function _onTextChange(length : int) : void 
        {
            textLength = length;
            control.updateTrackLabel(false);
        }        
        private function _onError(e : ErrorEvent) : void 
        {
            message.show(e.text);
        }        
        private function _updateSize() : void 
        {
            control.setSize(stage.stageWidth, 24);
            mmlEditor.y = 24;
            mmlEditor.setSize(stage.stageWidth, stage.stageHeight - 48);
            pageSelector.y = stage.stageHeight - 24;
            pageSelector.setSize(stage.stageWidth, 24);
        }
    }
}
import flash.display.*;
import flash.geom.Rectangle;
import flash.text.*;
import flash.events.*;
import flash.ui.Keyboard;
import flash.net.*;
import com.bit101.components.*;
import org.si.sion.*;
import org.si.sion.events.*;
import org.si.sion.utils.Translator;
// Global variables
var cookieName : String = "savedData";
var driver : SiONDriver = new SiONDriver();
var message : MessageBox;
var mmlEditor : MMLEditor;
var control : ControlPanel;
var pageSelector : PageSelector;
var searchWindow : SearchWindow;
var pageIndex : int = 0;
var soloIndex : int = -1;
var textLength : int = 0;
// Global functions
function loadCookie() : void 
{
    var so : SharedObject = SharedObject.getLocal(cookieName);
    mmlEditor.xml = so.data.mmlxml;
}
function saveCookie() : void 
{    
    var so : SharedObject = SharedObject.getLocal(cookieName);
    so.data.mmlxml = mmlEditor.xml;
    so.flush();
}
function togglePlay() : void 
{
    if (driver.isPlaying) 
    {
        driver.stop();
        control.cbPlay(false);
    } 
    else 
    {
        saveCookie();
        driver.position = control.position * 1000;
        driver.play(mmlEditor.mml);
        control.cbPlay(true);
    }
}
function selectPage(index:int) : void 
{
    if (pageIndex != index) 
    {
        control.cbPageChanged(index);
        mmlEditor.cbPageChanged(index);
        pageSelector.cbPageChanged(index);
        pageIndex = index;
    }
}
function setMute(index:int, bool:Boolean) : void 
{
    mmlEditor.cbMuteChanged(index, bool);
    pageSelector.cbMuteChanged(index, bool);
    control.cbPageChanged(pageIndex);
    soloIndex = -1;
}
function setSolo(index : int, bool : Boolean) : void 
{
    var i : int, mute : Boolean, imax : int = MMLEditor.FIELD_COUNT;
    for (i = 0; i < imax; i++) 
    {
        mute = bool && (i != index);
        mmlEditor.cbMuteChanged(i, mute);
        pageSelector.cbMuteChanged(i, mute);
    }
    soloIndex = (bool) ? index : -1;
    control.cbPageChanged(pageIndex);
}
// Message box
class MessageBox extends Window 
{
    private var _text:Text, _button:PushButton;
    function MessageBox(parent : DisplayObjectContainer=null, title : String = "Error") 
    {
        super(parent, 100, 100, title);
        setSize(200, 160);
        _text   = new Text(content, 0, 0, "");
        _text.setSize(200, 120);
        _button = new PushButton(content, 76, 122, "OK", function(e : Event) : void{visible = false;});
        _button.setSize(48,16);
        visible = false;
    }    
    public function show(message : String = "") : void 
    {
        _text.text = message;
        visible = true;
    }
}
// Menu window
class MenuPopup extends Panel 
{
    function MenuPopup(parent : DisplayObjectContainer = null) 
    {
        super(parent, 263, 0);
        var y : int = 4;        
        newButton("clear", clear);
        newButton("load", load);
        newButton("save as xml", saveXML);
        newButton("save as mml", saveMML);
        newButton("translate from TSSCP MML", transTSSCP);
        newButton("show search window", showSearch);
        setSize(200, y + 2);
        this.y = -y - 4;
        visible = false;        
        function newButton(label : String, func : Function) : PushButton 
        {
            var newButton : PushButton = new PushButton(content, 4, y, label, func);
            newButton.setSize(193, 16);
            y += 18;
            return newButton;
        }
    }    
    public function clear(e : Event) : void 
    {
        mmlEditor.mml = "t90 l8 o5e o5e o5e o5f o5e o5e o5c o5c o5d o5d o5c  o5d o5e o4g+ o4b o5e o5e o5e o5f o5e o5e  o5c o5d o5d o5c o4b o4a o5a o5a o5a o5a o5a o5a o5e o5e o5e o5e o5f o5g o5f o5e o5e o5f o5e o5e o5c o5c o5d o5d o5c o4b o4a o4e o4e o4f o4e o4e o4c o4c o4d o4d o4c o4d o4e o4e o4e o4f o4e o4e o4c o4c o4d o4d o4c o3b o3a o5a o5a o5a o5a o5a o5a o5a o5e o5e o5e o5f o5g o5f o5e o5e o5f o5e o5e o5c o5c o5d o5d o5c o4b";
        visible = false;
    }    
    public function load(e : Event) : void 
    {
        var fr : FileReference = new FileReference();
        fr.addEventListener("select", function(e:Event) : void { fr.load(); });
        fr.addEventListener("complete", _onLoaded);
        fr.browse();
        visible = false;
    }    
    private function _onLoaded(e : Event) : void 
    {
        var text : String = String(e.target.data);
        if (/^<MMLList/.test(text)) mmlEditor.xml = new XML(text);
        else mmlEditor.mml = text;
    }    
    public function saveXML(e : Event) : void 
    {
        new FileReference().save(mmlEditor.xml, "sionmml.xml");
        visible = false;
    }    
    public function saveMML(e : Event) : void 
    {
        new FileReference().save(mmlEditor.mml, "sionmml.txt");
        visible = false;
    }    
    public function transTSSCP(e : Event) : void 
    {
        mmlEditor.mml = Translator.tsscp(mmlEditor.mml, false);
        visible = false;
    }    
    public function showSearch(e : Event) : void 
    {
        searchWindow.visible = true;
        visible = false;
    }
}
// Search window
class SearchWindow extends Window 
{
    public var input : InputText, execute : PushButton, regexp : CheckBox;
    function SearchWindow(parent : DisplayObjectContainer = null) 
    {
        super(parent, 160, 160, "Search Window");
        input = new InputText(content, 4, 4, "");
        input.setSize(192, 16);
        execute = new PushButton(content, 148, 24, "sreach", _onSearch);
        execute.setSize(48, 16);
        regexp = new CheckBox(content, 30, 26, "use regular expression");
        new PushButton(this, 182, 3, "X", _onClose).setSize(15, 15);
        setSize(200, 60);
        visible = false;
    }    
    private function _onClose(e : Event) : void 
    {
        visible = false;
    }    
    private function _onSearch(e : Event) : void 
    {
        mmlEditor.colorSearchResult(input.text, regexp.selected);
    }
}
// Control panel
class ControlPanel extends Panel 
{
    public var playButton : PushButton;
    public var posLabel : Label, posInput : InputText;
    public var muteCheck : CheckBox, soloCheck : CheckBox;
    public var loadLabel : Label, trackLabel : Label;    
    function ControlPanel(parent : DisplayObjectContainer = null, xpos : Number = 0, ypos : Number = 0) 
    {
        super(parent, xpos, ypos);
        playButton = new PushButton(content, 2, 5, "play", function(e : Event) : void { togglePlay(); });
        playButton.setSize(40, 16);
        posLabel = new Label(content, 50, 2, "position");
        posInput = new InputText(content, 90, 5, "0");
        trackLabel = new Label(content, 250, 2, "");
        loadLabel = new Label(content, 300, 2, "");
        posInput.setSize(40, 16);
        muteCheck = new CheckBox(content, 150, 8, "mute", _onMuteClick);
        soloCheck = new CheckBox(content, 200, 8, "solo", _onSoloClick);
        driver.addEventListener(SiONEvent.STREAM, _onStream);
    }    
    public function get position() : int 
    {
        return int(posInput.text);
    }
    public function updateTrackLabel(isPlaying : Boolean) : void 
    {
        if (isPlaying) trackLabel.text = "track:" + String(driver.trackCount);
        else trackLabel.text = "letters:" + String(textLength);
    }
    public function cbPlay(bool : Boolean) : void 
    {
        playButton.label = (bool) ? "stop" : "play";
        loadLabel.visible = bool;
        updateTrackLabel(bool);
    }
    public function cbPageChanged(index : int) : void 
    {
        muteCheck.selected = mmlEditor.getMute(index);
        soloCheck.selected = (index == soloIndex);
    }
    private function _onStream(e : SiONEvent) : void { loadLabel.text = "load:" + String(driver.processTime) + "[ms]"; }
    private function _onMuteClick(e : Event) : void { setMute(pageIndex, muteCheck.selected); }
    private function _onSoloClick(e : Event) : void { setSolo(pageIndex, soloCheck.selected); }
}
class PageSelector extends Panel 
{
    public var pageSelector : Vector.<PushButton> = new Vector.<PushButton>(MMLEditor.FIELD_COUNT);
    public var pageMute : Vector.<Shape> = new Vector.<Shape>(MMLEditor.FIELD_COUNT);
    public var mergedButton : PushButton, menuButton:PushButton, menuPopup:MenuPopup;    
    function PageSelector(parent : DisplayObjectContainer=null, xpos:Number=0, ypos:Number=0) 
    {
        super(parent, xpos, ypos);
        for (var i : int = 0; i < MMLEditor.FIELD_COUNT; i++) 
        {
            pageSelector[i] = new PushButton(content, i * 50 + 64, 5, "page"+String(i+1), _onPageSelected);
            pageSelector[i].toggle = true;
            pageSelector[i].setSize(48, 16);
            pageSelector[i].addChild(pageMute[i] = new Shape());
            pageSelector[i].doubleClickEnabled = true;
            pageSelector[i].addEventListener("doubleClick", _onDoubleClick);
            pageMute[i].graphics.beginFill(0x000000, 0.25);
            pageMute[i].graphics.drawRect(0, 0, 48, 16);
            pageMute[i].graphics.endFill();
            pageMute[i].visible = false;
        }
        pageSelector[0].selected = true;
        mergedButton = new PushButton(content, 2, 5, "merged", _onMergedSelected);
        mergedButton.setSize(48, 16);
        menuButton = new PushButton(content, 415, 5, "menu", _onMenu);
        menuButton.setSize(48, 16);
        menuPopup = new MenuPopup(this);
    }
    public function cbPageChanged(index : int) : void 
    {
        for (var i : int = 0; i < MMLEditor.FIELD_COUNT; i++) pageSelector[i].selected = (index == i);
    }
    public function cbMuteChanged(index : int, mute : Boolean) : void 
    { 
        if (index < MMLEditor.FIELD_COUNT) pageMute[index].visible = mute;
    }
    private function _onMergedSelected(e : Event) : void 
    {
        mmlEditor.updateMergedMML();
        selectPage(MMLEditor.FIELD_COUNT);
    }
    private function _onMenu(e : Event) : void 
    {
        menuPopup.visible = !menuPopup.visible;
    }    
    private function _onPageSelected(e : Event) : void 
    {
        selectPage(int((e.target.x - 39) / 50));
    }    
    private function _onDoubleClick(e : Event) : void 
    {
        var index : int = int((e.target.x - 39) / 50);
        setMute(index, !mmlEditor.getMute(index));
    }    
    override public function setSize(w : Number, h : Number) : void 
    {
        super.setSize(w, h);
        if (menuButton) menuButton.x = width - 50;
        if (menuPopup) menuPopup.x = width - 202;
    }
}
class MMLEditor extends Sprite 
{
    static public const FIELD_COUNT : int = 5; 
    public var sion140 : Boolean = true;
    private var mmlFields : Vector.<MMLField> = new Vector.<MMLField>(FIELD_COUNT+1);
    function MMLEditor(parent : DisplayObjectContainer = null, xpos : Number = 0, ypos : Number = 0, func : Function = null) 
    {
        if (parent) parent.addChild(this);
        this.x = xpos;
        this.y = ypos;        
        MMLTextField.defaultFormat.size  = 12;
        MMLTextField.defaultFormat.font  = "MS Gothic, Courier, _typewriter";
        MMLTextField.defaultFormat.color = 0x606060;
        MMLTextField.searchFormat.color = 0xc06060;
        MMLTextField.searchFormat.underline = true;
        for (var i : int = 0; i <= FIELD_COUNT; i++) mmlFields[i] = new MMLField(this, (i == FIELD_COUNT), func);
        mmlFields[0].visible = true;
    }    
    public function cbPageChanged(index : int) : void 
    {
        for (var i : int = 0; i <= FIELD_COUNT; i++) mmlFields[i].visible = false;
        mmlFields[index].visible = true;
        mmlFields[index].setFocus();
    }
    public function cbMuteChanged(index : int, mute : Boolean) : void 
    {
        mmlFields[index].mute = mute;
    }    
    public function getMute(index : int) : Boolean 
    {
        return mmlFields[index].mute;
    }    
    public function setSize(width : Number, height : Number) : void 
    {
        for (var i : int = 0; i <= FIELD_COUNT; i++) mmlFields[i].setSize(width, height);
    }    
    public function set xml(list : XML) : void 
    {
        if (list) 
        {
            for (var i : int = 0; i < FIELD_COUNT; i++) mmlFields[i].text = list.MML[i];
        }
    }    
    public function get xml() : XML 
    {
        var xml : XML = <MMLList version='1'/>, cont : String;
        for (var i : int = 0; i < FIELD_COUNT; i++) 
        {
            cont = "<![CDATA["+ mmlFields[i].text + "]]>";
            xml.appendChild(new XML("<MML field='" + String(i) + "'>" + cont + "</MML>"));
        }
        return xml;
    }    
    public function set mml(text : String) : void 
    {
        mmlFields[0].text = text;
        for (var i : int = 1; i < FIELD_COUNT; i++) mmlFields[i].text = "";
    }    
    public function get mml() : String 
    {
        var i : int, result : String = "", text : String, rex : RegExp = /;\s*$/;
        for (i = 0; i < FIELD_COUNT; i++) 
        {
            text = mmlFields[i].text;
            if (!mmlFields[i].mute && text != "") 
            {
                if (sion140) text = text.replace(/([A-Z]+(=|@?[0-9]*{))/g, "#$1").replace(/#+/g, "#"); //}
                if (rex.test(text)) result += text;
                else result += text + ";\n";
            }
        }
        return result;
    }    
    public function updateMergedMML() : void 
    {
        mmlFields[FIELD_COUNT].text = mml;
    }    
    public function colorSearchResult(pattern : String, byRegExp : Boolean = false) : void 
    {
        mmlFields[pageIndex].colorSearchResult(pattern, byRegExp);
    }
}
class MMLField extends Panel 
{
    private var textField : MMLTextField;
    private var scrollBar : CustomSlider;
    private var funcTextChange : Function;    
    function MMLField(parent : DisplayObjectContainer, constant : Boolean = false, func : Function = null) 
    {
        super(parent);
        funcTextChange = func;
        scrollBar = new CustomSlider("vertical", content, 0, 0, _onSliderChanged);
        scrollBar.backClick = true;
        textField = new MMLTextField(content, constant);
        textField.addEventListener("change", _onTextChanged);
        textField.addEventListener("scroll", _onTextScroll);
        visible = false;
    }   
    public function get text() : String { return textField.text; }
    public function set text(mml : String) : void { textField.text = mml || ""; }
    public function get mute() : Boolean { return (textField.type == 'dynamic'); }
    public function set mute(bool : Boolean) : void 
    {
        if (textField.constant || bool) 
        {
            textField.type = 'dynamic';
            textField.backgroundColor = 0xc0c0c0;
        } 
        else 
        {
            textField.type = 'input';
            textField.backgroundColor = 0xffffff;
        }
    }    
    override public function setSize(width : Number, height : Number) : void 
    {
        super.setSize(width, height);
        if (textField) 
        {
            textField.setSize(width - 15, height);
            scrollBar.setSize(15, height);
            scrollBar.x = width - 15;
            scrollBar.setRange(1, textField.maxScrollV, textField.scrollV, textField.numLines - textField.maxScrollV);
        }
    }    
    public function setFocus() : void 
    {
        stage.focus = textField;
    }    
    private function _onSliderChanged(e:Event) : void 
    {
        textField.scrollV = scrollBar.value;    
    }    
    private function _onTextChanged(e:Event) : void 
    {
        scrollBar.setRange(1, textField.maxScrollV, textField.scrollV, textField.numLines - textField.maxScrollV);
        funcTextChange(textField.text.length);
    }    
    private function _onTextScroll(e:Event) : void 
    {
        scrollBar.setRange(1, textField.maxScrollV, textField.scrollV, textField.numLines - textField.maxScrollV);
    }    
    public function colorSearchResult(pattern : String, byRegExp : Boolean = false) : void 
    {
        textField.setTextFormat(MMLTextField.defaultFormat);        
        var mml : String = textField.text;
        if (pattern != "") 
        {            
            if (byRegExp) 
            {
                var res : *, rex : RegExp = new RegExp(pattern, "g");
                while (res = rex.exec(mml)) textField.setTextFormat(MMLTextField.searchFormat, res.index, res.index+res[0].length);
            } 
            else 
            {
                for (var i : int = 0; i != -1;) 
                {
                    i = mml.indexOf(pattern, i);
                    if (i == -1) break;
                    textField.setTextFormat(MMLTextField.searchFormat, i, i+pattern.length);
                    i += pattern.length+1;
                }
            }
        }
    }
}
// Slider with stretching handle
class CustomSlider extends Slider 
{
    protected var _handleLength : Number = 64, _isH : Boolean;
    function CustomSlider(orientation : String = "horizontal", parent : DisplayObjectContainer = null, xpos:Number = 0, ypos:Number =  0, defaultHandler:Function = null) 
    {
        super(orientation, parent, xpos, ypos, defaultHandler);
        _isH = (_orientation == "horizontal");
    }
    public function setRange(min : Number, max : Number, value : Number, range : Number) : void 
    {
        setSliderParams(min, max, value);
        if (visible = (_min < _max)) 
        {
            _handleLength = range / (range + max - min) * ((_isH) ? _width : _height);
            invalidate();
        }
    }    
    override protected function drawHandle() : void 
    {   
        _handle.graphics.clear();
        _handle.graphics.beginFill(Style.BUTTON_FACE);
        if (_isH) _handle.graphics.drawRect(1, 1, _handleLength - 2, _height - 2);
        else _handle.graphics.drawRect(1, 1, _width - 2,  _handleLength - 2);
        _handle.graphics.endFill();
        positionHandle();
    }    
    override protected function onDrag(event : MouseEvent) : void 
    {
        stage.addEventListener(MouseEvent.MOUSE_UP, onDrop);
        stage.addEventListener(MouseEvent.MOUSE_MOVE, onSlide);
        if (_isH) _handle.startDrag(false, new Rectangle(0, 0, width - _handleLength, 0));
        else _handle.startDrag(false, new Rectangle(0, 0, 0, height - _handleLength));
    }    
    override protected function positionHandle() : void 
    {
        if (_isH) _handle.x = (_value - _min) / (_max - _min) * (_width  - _handleLength);
        else      _handle.y = (_value - _min) / (_max - _min) * (_height - _handleLength);
    }
    override protected function onBackClick(event : MouseEvent) : void 
    {
        if (_isH) 
        {
            _handle.x = mouseX - _height / 2;
            _handle.x = Math.max(_handle.x, 0);
            _handle.x = Math.min(_handle.x, _width - _handleLength);
            _value = _handle.x / (_width - _handleLength) * (_max - _min) + _min;
        } 
        else 
        {
            _handle.y = mouseY - _width / 2;
            _handle.y = Math.max(_handle.y, 0);
            _handle.y = Math.min(_handle.y, _height - _handleLength);
            _value = _handle.y / (_height - _handleLength) * (_max - _min) + _min;
        }
        dispatchEvent(new Event(Event.CHANGE));
    }    
    override protected function onSlide(event : MouseEvent) : void 
    {
        var oldValue:Number = _value;
        if (_isH) _value = _handle.x / (_width  - _handleLength) * (_max - _min + 1) + _min;
        else      _value = _handle.y / (_height - _handleLength) * (_max - _min + 1) + _min;
        if (_value != oldValue) dispatchEvent(new Event(Event.CHANGE));
    }
}
// undo and redo are from psyark's Psycode (modified)
class MMLTextField extends TextField 
{
    public var constant : Boolean;
    private var _preventFollowingTextInput : Boolean = false;
    private var _prevText : String = "";
    private var _prevSBI : int;
    private var _prevSEI : int;
    private var _history : Vector.<HistoryItem> = new Vector.<HistoryItem>();
    private var _historyIndex : int;
    private var _ignoreChange : Boolean = false;    
    static public var defaultFormat : TextFormat = new TextFormat();
    static public var searchFormat : TextFormat = new TextFormat();    
    function MMLTextField(parent : DisplayObjectContainer, constant:Boolean) 
    {
        this.constant = constant;        
        selectable = true; 
        alwaysShowSelection = true;
        mouseEnabled = true;
        type = (constant) ? 'dynamic' : 'input';
        multiline = true;
        wordWrap = false;
        background = true;
        backgroundColor = (constant) ? 0xc0c0c0 : 0xffffff;
        defaultTextFormat = defaultFormat;        
        if (!constant) 
        {
            addEventListener("change", _onChange);
            addEventListener("textInput", _onTextInput);
            addEventListener("keyDown", _onKeyDown);
        }        
        parent.addChild(this);        
        clearHistory();
    }    
    public function setSize(width : Number, height : Number) : void 
    {
        this.width  = width;
        this.height = height;
    }    
    public function clearHistory() : void 
    {
        _history.length = 0;
        _historyIndex = 0;
        _prevText = text;
    }    
    private function _onChange(event : Event) : void 
    {
        var res : *;
        if (_prevText != text) 
        {
            if (_preventFollowingTextInput) 
            {
                res = StringComparator.compare(_prevText, text);
                replaceText(res.l, length - res.r, _prevText.substring(res.l, _prevText.length - res.r).replace(/\r/g, "\n"));
                setSelection(_prevSBI, _prevSEI);
            } 
            else 
            {
                if (!_ignoreChange) 
                {
                    res = StringComparator.compare(_prevText, text);
                    var item:HistoryItem = new HistoryItem(res.l);
                    item.oldText = _prevText.substring(res.l, _prevText.length - res.r);
                    item.newText = text.substring(res.l, text.length - res.r);
                    _history.length = _historyIndex;
                    _history.push(item);
                    _historyIndex = _history.length;
                }
                _prevText = text;
                _prevSBI = selectionBeginIndex;
                _prevSEI = selectionEndIndex;
            }
        }
    }        
    private function _onTextInput(event : Event) : void 
    {
        if (_preventFollowingTextInput) event.preventDefault();
    }        
    private function _onKeyDown(event : KeyboardEvent) : void 
    {
        _preventFollowingTextInput = false;
        switch (event.keyCode) 
        {
        case Keyboard.BACKSPACE:
            break;
        case 89: // Y
            if (event.ctrlKey) doCtrlY();
            break;
        case 90: // Z
            if (event.ctrlKey) doCtrlZ();
            break;
        }
        function doCtrlZ() : void 
        {
            if (_history.length && _historyIndex > 0) 
            {
                var item : HistoryItem = _history[_historyIndex - 1];
                replaceText(item.index, item.index + item.newText.length, item.oldText.replace(/\r/g, "\n"));
                setSelection(item.index + item.oldText.length, item.index + item.oldText.length);
                _historyIndex--;
                _dispatchIgnorableChangeEvent();
            }
            event.preventDefault();
            _preventFollowingTextInput = true;
        }
        function doCtrlY() : void 
        {
            if (_history.length && _historyIndex < _history.length) 
            {
                var item : HistoryItem = _history[_historyIndex];
                replaceText(item.index, item.index + item.oldText.length, item.newText.replace(/\r/g, "\n"));
                setSelection(item.index + item.newText.length, item.index + item.newText.length);
                _historyIndex++;
                _dispatchIgnorableChangeEvent();
            }
            event.preventDefault();
            _preventFollowingTextInput = true;
        }
    }    
    private function _dispatchIgnorableChangeEvent() : void 
    {
        _ignoreChange = true;
        dispatchEvent(new Event("change"));
        _ignoreChange = false; 
    }
}
class HistoryItem 
{
    public var index : int;
    public var oldText : String;
    public var newText : String;    
    public function HistoryItem(index : int = 0, oldText : String = "", newText : String = "") 
    {
        this.index = index;
        this.oldText = oldText;
        this.newText = newText;
    }
}
class StringComparator 
{
    static public function compare(str1 : String, str2 : String) : * 
    {
        var minLength : int = Math.min(str1.length, str2.length);
        var step : int, l : int, r : int;        
        for (step = 0x1000000; step > minLength;) step >>= 1;
        for (l = 0; l < minLength;) 
        {
            if (str1.substr(0, l+step) != str2.substr(0, l+step)) 
            {
                if (step == 1) { break; }
                step >>= 1;
            } 
            else 
            {
                l += step;
            }
        }
        l = Math.min(l, minLength);
        minLength -= l;        
        for (step = 0x1000000; step > minLength;) step >>= 1;
        for (r = 0; r < minLength; ) 
        {
            if (str1.substr(-r - step) != str2.substr(-r - step)) 
            {
                if (step == 1) { break; }
                step >>= 1;
            } 
            else 
            {
                r += step;
            }
        }
        r = Math.min(r, minLength);
        return {"l" : l, "r" : r};
    }
}