I found there are people still need TMemo's Undo/Redo function, and Handoko messaged me, he wants to undo by word, not by character. So I rewrote UndoRedoDemo to implement undo by word and simplify the code.
The code test environment: Arch Linux, Lazarus 2.0.12, GTK 2
Since I don't use windows, this code has not been tested in windows. I don't think it will work properly in windows.
I give this code to the public domain, so everyone can use it, just like using your own code.
If there is no problem with this code, I will delete my previous post about Undo/Redo because the code posted before is imperfect and difficult to read.
Here I record my ideas, so that you can understand the code.
To record TMemo history data, it is necessary to operate in the OnChange event of TMemo, so we need to know the features of OnChange event. After some tests, I found the following features (arch Linux, Lazarus 2.0.12, GTK 2):
1. When typing, SelLength is 0, and SelStart is after the newly added content.
2. When deleting, SelLength is 0 and SelStart is before the deleted content.
3. If you select the text and then type, the selected content will be deleted first, and then new content will be added.
4. When dragging content inside TMemo, the selected content will be copied to the target location first (SelLength is the length of the selected content), and then the previously selected content will be deleted (SelLength is 0). The location of SelStart is the same as the following situation.
5. When you drag content from outside to TMemo, SelLength is 0. SelStart can be divided into two cases:
A. If you drag backward (drag to a position after the last typing position), SelStart will still stay in the position before the drag operation
B. If you drag forward (drag to a position before the last typing position, or just drag to the last typing position), SelStart remains in the position before the drag operation occurs, except that SelStart will be moved backwards because of the content inserted before (offset is the length of the selected content), which is similar to the normal type (or paste) operation, which is difficult to distinguish. At present, I can't distinguish them, so I must use the same inefficient algorithm to handle the common typing operation.
First of all, I captured the normal typing and deleting operations in the OnChange event, and did not consider the drag and drop operation for the time being.
In order to distinguish between typing and deleting operations, I need to compare the contents of TMemo before and after the OnChange event. If the length of the content before OnChange is less than that after the OnChange, it is typed, otherwise it is deleted.
The following code demonstrates this process (test results will be output to the terminal):
unit uHistory;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, StdCtrls, lazUTF8;
type
{ THistory }
THistory = class
FMemo : TMemo;
FOldOnChange : TNotifyEvent;
FPrevContent : String;
constructor Create(Memo: TMemo);
destructor Destroy; override;
procedure MemoOnChange(Sender: TObject);
end;
implementation
constructor THistory.Create(Memo: TMemo);
begin
FMemo := Memo;
FOldOnChange := FMemo.OnChange;
FMemo.OnChange := @MemoOnChange;
FPrevContent := FMemo.Text;
end;
destructor THistory.Destroy;
begin
FMemo.OnChange := FOldOnChange;
inherited Destroy;
end;
procedure THistory.MemoOnChange(Sender: TObject);
var
Content: String;
Len, SelStart: SizeInt;
begin
Content := FMemo.Text;
Len := UTF8Length(Content) - UTF8Length(FPrevContent);
SelStart := FMemo.SelStart;
if Len > 0 then begin
SelStart := SelStart - Len;
Write('On Add: ', SelStart, ' ', Len, ' ');
WriteLn(UTF8Copy(Content, SelStart + 1, Len));
end
else if Len < 0 then begin
Write('On Del: ', SelStart, ' ', -Len, ' ');
WriteLn(UTF8Copy(FPrevContent, SelStart + 1, - Len));
end
else
Exit;
FPrevContent := Content;
if Assigned(FOldOnChange) then
FOldOnChange(Sender);
end;
end.
The usage is as follows (you need to activate the OnCreate and OnDestroy events of TForm1):
unit Unit1;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, uHistory;
type
{ TForm1 }
TForm1 = class(TForm)
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FHistory: THistory;
public
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
Memo1.Text := '01234567890123456789';
FHistory := THistory.Create(Memo1);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FHistory.Free;
end;
end.
Next, I need to identify the drag operation.
For the drag inside TMemo, since its SelLength is not 0, it is easy to identify.
However, there is no good way to identify the operation dragged into TMemo from the outside of TMemo, which can not be distinguished from ordinary typing operations, and only a part of it can be recognized (5.A).
For ordinary typing operation, SelStart is different before and after typing, but drag operation (5.A) is the same, which can be distinguished.
To get the SelStart before typing, I can use the Application.OnIdle Event.
The demo code is as follows (The drag content is not parsed, so it is not accurately, but the drag operation can be recognized):
unit uHistory;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, StdCtrls, Forms, lazUTF8;
type
{ THistory }
THistory = class
FMemo : TMemo;
FOldOnChange : TNotifyEvent;
FPrevContent : String;
FOldApplicationIdle : TIdleEvent;
FPrevSelStart : SizeInt;
constructor Create(Memo: TMemo);
destructor Destroy; override;
procedure MemoOnChange(Sender: TObject);
procedure ApplicationIdle(Sender: TObject; var Done: Boolean);
end;
implementation
constructor THistory.Create(Memo: TMemo);
begin
FMemo := Memo;
FOldOnChange := FMemo.OnChange;
FMemo.OnChange := @MemoOnChange;
FOldApplicationIdle := Application.OnIdle;
Application.OnIdle := @ApplicationIdle;
FPrevContent := FMemo.Text;
FPrevSelStart := FMemo.SelStart;
end;
destructor THistory.Destroy;
begin
FMemo.OnChange := FOldOnChange;
Application.OnIdle := FOldApplicationIdle;
inherited Destroy;
end;
procedure THistory.MemoOnChange(Sender: TObject);
var
Content: String;
Len, SelStart: SizeInt;
begin
Content := FMemo.Text;
Len := UTF8Length(Content) - UTF8Length(FPrevContent);
if Len > 0 then begin
SelStart := FMemo.SelStart;
if FMemo.SelLength > 0 then
WriteLn('Drag from inside TMemo')
else if SelStart = FPrevSelStart then
WriteLn('Drag from outside TMemo and drop after Last Typing Position')
else begin
WriteLn('Typing or "Drag from outside TMemo and drop before Last Typing Position');
SelStart := SelStart - Len;
end;
Write('On Add: ', SelStart, ' ', Len, ' ');
WriteLn(UTF8Copy(Content, SelStart + 1, Len));
end
else if Len < 0 then begin
Write('On Del: ', FMemo.SelStart, ' ', -Len, ' ');
WriteLn(UTF8Copy(FPrevContent, FMemo.SelStart + 1, - Len));
end
else
Exit;
FPrevContent := Content;
if Assigned(FOldOnChange) then
FOldOnChange(Sender);
end;
procedure THistory.ApplicationIdle(Sender: TObject; var Done: Boolean);
begin
FPrevSelStart := FMemo.SelStart;
if Assigned(FOldApplicationIdle) then
FOldApplicationIdle(Sender, Done);
end;
end.
After all the different operations are distinguished, the historical data can be recorded.
For drag and drop operations, I must compare the full text to correctly obtain the changed data, and the drag and drop operations cannot be distinguished from ordinary typing operations, and drag operations cannot be disabled, so ordinary typing operations must also be analyzed by full text comparison.
Please refer to the code for specific implementation and usage(in the attachment).