用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()
) - 核心数据没有指针, 全部是偏移量