views:

731

answers:

2

While creating a customized editor with RichTextBox, I've face the problem of finding deleted/inserted text with the provided information with TextChanged event.

The instance of TextChangedEventArgs has some useful data, but I guess it does not cover all the needs. Suppose a scenario which multiple paragraphs are inserted, and at the same time, the selected text (which itself spanned multiple paragraphs) has been deleted.

With the instance of TextChangedEventArgs, you have a collection of text changes, and each change only provides you with the number of removed or added symbols and the position of it.

The only solution I have in mind is, to keep a copy of document, and apply the given list of changes on it. But as the instances of TextChange only give us the number of inserted/removed symbols (and not the symbols), so we need to put some special symbol (for example, '?') to denote unknown symbols while we transform our original copy of document.

After applying all changes to the original copy of document, we can then compare it with the richtextbox's updated document and find the mappings between unknown symbols and the real ones. And finally, get what we want !!!

Anybody has tried this before? I need your suggestions on the whole strategy, and what you think about this approach.

Regards

A: 

this is a system.windows.forms richtextbox, not a WPF and it goes like this:

public partial class myRichTextBox : RichTextBox {

    private int _TotalLenght = 0;
    private int _CurrentPosition = 0;
    private int _SelectionLenght = 0;
    private bool _KeyPress = false;
    private char _LastChar = ' ';
    private int _UndoRedo = 0;
    private bool myModificare = false;

    private List<CLetter> myTraceLetters = new List<CLetter>();
    private List<CUndoRedo> myUndoRedo = new List<CUndoRedo>();

    public myRichTextBox() {
        InitializeComponent();

        this.WordWrap = true;
        this.ShortcutsEnabled = false;

        this.TextChanged += new EventHandler(myRichTextBox_TextChanged);
        this.SelectionChanged += new EventHandler(myRichTextBox_SelectionChanged);
        this.KeyPress += new KeyPressEventHandler(myRichTextBox_KeyPress);
    }

    #region Events

    protected override bool ProcessCmdKey(ref Message m, Keys keyData) {
        try {
            int add = 0, remove = 0, offset = 0;
            switch (keyData.ToString()) {
                case "Return":
                    return true;
                case "V, Control":
                    if (Clipboard.ContainsText()) {
                        this.SelectedText = Clipboard.GetText();
                    }
                    break;
                case "C, Control":
                    if (this.SelectionLength > 0)
                        Clipboard.SetText(this.SelectedText);
                    break;
                case "X, Control":
                    if (this.SelectionLength > 0) {
                        Clipboard.SetText(this.SelectedText);
                        this.SelectedText = "";
                    }
                    break;
                case "A, Control":
                    this.SelectAll();
                    break;
                case "Z, Control":
                    if (_UndoRedo - 1 >= 0) {
                        _UndoRedo--;

                        add = myUndoRedo[_UndoRedo]._add;
                        remove = myUndoRedo[_UndoRedo]._remove;
                        offset = myUndoRedo[_UndoRedo]._offset;

                        for (int i = 0; i < add; i++)
                            myTraceLetters.RemoveAt(offset);
                        for (int i = 0; i < remove; i++)
                            myTraceLetters.Insert(offset + i, myUndoRedo[_UndoRedo].removeLitere[i]);

                        myModificare = true;
                        this.Select(offset, add);
                        this.SelectedText = myUndoRedo[_UndoRedo].RemoveText;
                        this.Select(offset + remove, 0);

                        RefreshTextPointers();
                    }
                    break;
                case "Y, Control":
                    if (_UndoRedo < myUndoRedo.Count) {
                        _UndoRedo++;

                        add = myUndoRedo[_UndoRedo]._add;
                        remove = myUndoRedo[_UndoRedo]._remove;
                        offset = myUndoRedo[_UndoRedo]._offset;

                        for (int i = 0; i < add; i++)
                            myTraceLetters.Insert(offset + i, myUndoRedo[_UndoRedo].addLitere[i]);
                        for (int i = 0; i < remove; i++)
                            myTraceLetters.RemoveAt(offset); 

                        myModificare = true;
                        this.Select(offset, remove);
                        this.SelectedText = myUndoRedo[_UndoRedo].AddText;
                        this.Select(offset + add, 0);

                        RefreshTextPointers();
                    }
                    break;
            }
        } catch (Exception msg) {
            MessageBox.Show("Zi cuiva: " + msg.Message, "Failed at ProcessCmdKey: " + keyData.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Error);

            ResetMyText();
        }

        return base.ProcessCmdKey(ref m, keyData);
    }

    private void myRichTextBox_KeyPress(object sender, KeyPressEventArgs e) {
        _KeyPress = true;

        if (this.SelectionStart < this.Text.Length)
            _LastChar = this.Text[this.SelectionStart];
    }

    private void myRichTextBox_SelectionChanged(object sender, EventArgs e) {
        if (this.Text.Length == _TotalLenght) {
            _CurrentPosition = this.SelectionStart;

            if (_KeyPress)
                _KeyPress = false;
            else
                _SelectionLenght = this.SelectionLength;
        }
    }

    private void myRichTextBox_TextChanged(object sender, EventArgs e) {
        int offset = 0, add = 0, remove = 0;
        string CeOperatie = "";

        try {
            if (myModificare)
                myModificare = false;
            else {                    
                if (this.Text.Length == _TotalLenght) { // a fost sters atata text cat a fost adaugat
                    if (_SelectionLenght > 0) {
                        offset = this.SelectionStart - _SelectionLenght;
                        remove = _SelectionLenght;
                        add = _SelectionLenght;

                        CeOperatie = "1";
                    } else if (this.SelectionStart > 0 && this.Text[this.SelectionStart - 1] != _LastChar) {
                        offset = this.SelectionStart - 1;
                        remove = 1;
                        add = 1;

                        CeOperatie = "2";
                    }
                } else if (this.Text.Length > _TotalLenght) { //a fost adaugat text
                    if (_CurrentPosition < this.SelectionStart) {
                        offset = _CurrentPosition;
                        add = this.SelectionStart - _CurrentPosition;
                        remove = _SelectionLenght;

                        CeOperatie = "3";
                    } else
                        MessageBox.Show("Te rog anunta'ma: unu", "Radu Moldovean");

                } else if (this.Text.Length < _TotalLenght) { // a fost sters text
                    if (_SelectionLenght == 0) {
                        if (_CurrentPosition > this.SelectionStart) {
                            CeOperatie = "4";

                            offset = this.SelectionStart;
                            remove = _CurrentPosition - this.SelectionStart;
                        } else if (_CurrentPosition == this.SelectionStart) {
                            CeOperatie = "5";

                            offset = this.SelectionStart;
                            remove = (_TotalLenght - this.SelectionStart) - (this.Text.Length - this.SelectionStart);
                        } else {
                            CeOperatie = "6";

                            offset = _CurrentPosition;
                            remove = (_TotalLenght - _CurrentPosition) - (this.Text.Length - this.SelectionStart);
                            add = this.SelectionStart - _CurrentPosition;
                        }
                    } else {
                        remove = _SelectionLenght;
                        offset = _CurrentPosition;

                        if (_TotalLenght - this.Text.Length != remove) {
                            if (remove - (_TotalLenght - this.Text.Length) < 0) {
                                CeOperatie = "7";

                                remove += ((remove - (_TotalLenght - this.Text.Length)) * -1);
                            } else {
                                CeOperatie = "8";

                                add = remove - (_TotalLenght - this.Text.Length);
                            }
                        }
                    }
                }

                if (_UndoRedo < myUndoRedo.Count)
                    myUndoRedo.RemoveRange(_UndoRedo, myUndoRedo.Count - _UndoRedo);

                myUndoRedo.Add(new CUndoRedo(offset, remove, add));

                if (myUndoRedo.Count > 100)
                    myUndoRedo.RemoveAt(0);

                _UndoRedo = myUndoRedo.Count;

                for (int i = 0; i < remove; i++) {
                    myUndoRedo[_UndoRedo - 1].removeLitere.Add(myTraceLetters[offset]);
                    myTraceLetters.RemoveAt(offset);
                }

                for (int i = 0; i < add; i++) {
                    myTraceLetters.Insert(offset + i, new CLetter(this.Text[offset + i].ToString()));
                    myUndoRedo[_UndoRedo - 1].addLitere.Add(myTraceLetters[offset]);
                }

                if (GetTraceText() != this.Text)
                    MessageBox.Show("Nu'i acelasi text: \r\n\r\n" + this.Text + "\r\n\r\n" + GetTraceText(), CeOperatie);

                RefreshTextPointers();
            }
        } catch (ArgumentOutOfRangeException msg) {
            MessageBox.Show(msg.ToString());

            ResetMyText();
        }
    }

    #endregion


    #region Others

    private void RefreshTextPointers() {
        _SelectionLenght = this.SelectionLength;
        _CurrentPosition = this.SelectionStart;
        _TotalLenght = this.Text.Length;
    }

    private string GetTraceText() {
        string text = "";

        for (int i = 0; i < myTraceLetters.Count; i++) {
            text += myTraceLetters[i].Litera;
        }

        return text;
    }

    public void ResetMyText() {
        myModificare = true;
        this.Text = "";

        myUndoRedo.Clear();
        myTraceLetters.Clear();

        _UndoRedo = 0;
        _SelectionLenght = 0;
        _CurrentPosition = 0;
        _TotalLenght = 0;
        _KeyPress = false;
    }

    #endregion

    private class CUndoRedo {
        public int _offset, _remove, _add;

        public List<CLetter> addLitere = new List<CLetter>();
        public List<CLetter> removeLitere = new List<CLetter>();

        public CUndoRedo(int offset, int remove, int add) {
            this._offset = offset;
            this._add = add;
            this._remove = remove;
        }

        public string RemoveText {
            get {
                string text = "";

                for (int i = 0; i < removeLitere.Count; i++)
                    text += removeLitere[i].Litera;

                return text;
            }
        }

        public string AddText {
            get {
                string text = "";

                for (int i = 0; i < addLitere.Count; i++)
                    text += addLitere[i].Litera;

                return text;
            }
        }
    }
}

public class CLetter {
    private string _litera;
    private Rectangle _coordonate;

    public CLetter(string litera) {
        this._litera = litera;
        _coordonate = Rectangle.Empty;
    }

    public CLetter(string litera, int X, int Y, int Width, int Height) {
        this._litera = litera;
        _coordonate = new Rectangle(X, Y, Width, Height);
    }

    public string Litera {
        get { return this._litera; }
    }

    public bool Details {
        get {
            if (this._coordonate == Rectangle.Empty)
                return true;
            else
                return false;
        }
    }

    public int X {
        get { return this._coordonate.X; }
    }

    public int Y {
        get { return this._coordonate.Y; }
    }

    public int Width {
        get { return this._coordonate.Width; }
    }

    public int Height {
        get { return this._coordonate.Height; }
    }
}

public class CExNull : Exception { }

it's not finished but I think it's an idea ...

A: 

It primarily depends on your use of the text changes. When the sequence includes both inserts and deletes it is theoretically impossible to know the details of each insert, since some of the symbols inserted may have subsequently been deleted. Therefore you have to choose what results you really want:

  • For some purposes you must to know the exact sequence of changes even if some of the inserted symbols must be left as "?".
  • For other purposes you must know exactly how the new text differs from the old but not the exact sequence in which the changes were made.

I will techniques to achieve each of these results. I have used both techniques in the past, so I know they are effective.

To get the exact sequence

This is more appropriate if you are implementing a history or undo log or searching for specific actions.

For these uses, the process you describe is probably best, with one possible change: Instead of "finding the mappings between the unknown symbols and the real ones", simply run the scan forward to find the text of each "Delete" then run it backward to find the text of each "Insert".

In other words:

  1. Start with the initial text and process the changes in order. For each insert, insert '?' symbols. For each delete, remove the specified number of symbols and record them as the text deleted.

  2. Start with the final text and process the changes in reverse order. For each delete, insert '?' symbols. For each insert, remove the specified number of symbols and record them as the text inserted.

When this is complete, all of your "Insert" and "Delete" change entries will have the associated text to the best of our knowledge, and any text that was inserted and immediately deleted will be '?' symbols.

To get the difference

This is more appropriate for revision marking or version comparison.

For these uses, simply use the text change information to compute a set of integer ranges in which changes might be found, then use a standard diff algorithm to find the actual changes. This tends to be very efficient in processing incremental changes but still gives you the best updates.

This is particularly nice when you paste in a replacement paragraph that is almost identical to the original: Using the text change information will indicate the whole paragraph is new, but using diff (ie. this technique) will mark only those symbol runs that are actually different.

The code for computing the change range is simple: Represent the change as four integers (oldstart, oldend, newstart, newend). Run through each change:

  1. If changestart is before newstart, reduce newstart to changestart and reduce oldstart an equal amount
  2. If changeend is after newend, increase newend to changeend and increase oldend an equal amount

Once this is done, extract range [oldstart, oldend] from the old document and the range [newstart, newend] from the new document, then use the standard diff algorithm to compare them.

Ray Burns