用C++實現富文本控制項(中): 撤銷
用C++實現富文本控制項: 撤銷
本節是關於撤銷重做相關實現. 項目地址: Github-RichED 本文備份地址: github
撤銷重做
這就被稱為UNDO/REDO之類的, 簡直是增量的代表. 這也算富文本編輯器實現的一個難點. 雖然叫做撤銷棧, 但是不完全是棧:
- 一般棧只需要棧底、棧頂指針, 撤銷棧多一個操作位置
- 撤銷是操作位-1, 棧頂不動, 直到達到棧底
- 重做是操作位+1, 棧頂不動, 直到達到棧頂
- 每一個操作(OP)入操作位棧, 操作位+1, 棧頂 = 操作位
具體實現中, 可以使用鏈表模擬棧. 然後設計上盡量使用可以直接調用free
就能釋放的Trivial數據, 原因後述.
裝飾操作
替換操作很常見:
- 將範圍[2, +2]的泥壕, 替換為長度為2的你好
- 這個按下撤銷鍵, 會先刪除你好再添上泥壕, 可以簡化為兩個OP:
- (1). 將範圍[2, +2]的泥壕刪掉
- (2). 在位置為2的地方插入長度為2的你好
這樣就是將現有問題轉換為已知的問題: 刪除和插入是最基本的操作(坐下坐下這是基本操作). 但是就有一個問題: 撤銷會變成兩次.
Scintilla
Scintilla是一個與本項目類似的代碼編輯控制項, 作者提到富文本編輯器會把格式的修改也壓入棧. 這個特性讓他實為煩躁, 於是出現了Scintilla.
以上兩點我們可以用一個裝飾的標誌解決, 將後面的裝飾性操作打上標誌:
STACK-TOP
OP <- 獨立的操作
OP +decorator <- 裝飾操作
OP <- 與上面的裝飾操作作為一個整體的操作
OP +decorator <- 裝飾操作
OP +decorator <- 裝飾操作
OP <- 與上面兩個裝飾操作作為一個整體的操作
例子: 將type替換成class , 然後輸入if
STACK-TOP
OP: 輸入
OP: [裝飾, 詞法解析器檢測到關鍵字] 將if設為藍色
OP: 插入if
OP: [裝飾, 詞法解析器檢測到關鍵字] 將class設為藍色
OP: [裝飾, 實際是替換操作插入部分] 插入class
OP: 刪除type
程序語言級可以用begin_op
和end_op
之類的函數包裹實現:
void demo(void) {
// 替換type->class
begin_op();
remove_text(type);
insert_text("class ");
do_parser();
end_op();
// 插入if
begin_op();
insert_text("if");
do_parser();
end_op();
// 輸入
begin_op();
insert_text(" ");
do_parser();
end_op();
}
void insert_text(const char[]) {
// 具體實現
// 操作記錄
if (op) {
// 撤銷棧操作以0開始
// 0到下一個0, 這個前閉後開區間為一個完整的撤銷棧操作
// 超過0的作為裝飾操作
gui_op.op = op - 1;
op++;
}
}
void begin_op(void) {
op = 1;
}
void end_op(void) {
op = 0;
}
當然, 這是將富文本操作作為裝飾的一種實現方式, 實際上還是把富文本操作記錄在撤銷棧上了.
使用begin_op
和end_op
之類的函數包裹的話, 還有一種實現就是完全將富文本操作排除在撤銷棧操作外, 每次都由詞法解析器進行富文本更新.
// 插入if
begin_op();
insert_text("if");
end_op();
do_parser();
好處是不會記錄在撤銷棧上, 壞處是撤銷/重做後還要調用do_parser()
.
換行符
編輯器是允許中途更換換行符的, 這時候直接使用撤銷棧會造成報道上的偏差.解決方法有:
- 修改換行符時修改撤銷棧上所有OP
- 不直接保存絕對長度信息(總偏移), 而是相對信息(行號+行內偏移)
這裡選擇了後者, 方便全局編寫
平平無奇撤銷棧
撤銷棧儲存的OP必須是平平無奇的(trivial), 主要有兩個特點:
- 沒有析構操作, 或者說唯一的析構操作就是釋放空間(
std::free()
) - 核心數據沒有指針, 全部是偏移量