From 734912a3778a0df643d07c49dd5d6bed970c8328 Mon Sep 17 00:00:00 2001 From: liujinchang Date: Fri, 23 Oct 2026 18:22:00 +0800 Subject: [PATCH] fix: merge column-edit undo commands for single-step undo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Root cause: InsertTextUndoCommand lacked id()/mergeWith() overrides, so QUndoStack never merged consecutive single-char column-edit insert commands; each keypress produced a separate undo step. Additionally, mergeWith() had no session continuity check, causing cross-session incorrect merging of column-edit undo commands 2. Fix: add id() returning kColumnEditInsertCommandId and mergeWith() that appends text when both commands have non-empty m_ColumnEditSelections and share the same m_sessionId; add m_columnEditSessionId member to TextEdit, increment it in mousePressEvent at session boundaries (Alt+click enter and non-Alt click exit); pass session ID to InsertTextUndoCommand constructor; update m_ColumnEditSelections after merge so undo operates on latest cursor positions; fix redo() selection to use m_sInsertText.length() without +1 3. Impact: column-edit undo now removes entire typed text in one step within the same session; cross-session typing is not incorrectly merged; redo path correct after merge; normal text insert unaffected (m_ColumnEditSelections is empty, so mergeWith returns false) Log: fix column-edit undo to remove all typed text in a single undo step Influence: 1. Test column-edit mode: type multiple chars then Ctrl+Z, verify single-step undo 2. Test column-edit redo after undo: Ctrl+Shift+Z then Ctrl+Z again, verify no data corruption 3. Test cross-session column-edit: enter column-edit, type, exit, re-enter column-edit, type again, verify undo does not merge across sessions 4. Test normal text editing undo/redo remains unchanged fix: 修复列编辑模式撤销需逐字符撤销的问题 1. 根因:InsertTextUndoCommand 未重写 id()/mergeWith(),QUndoStack 无法合并列编辑模式下连续按键产生的单字符插入命令,每次按键产生 独立的撤销步骤;此外 mergeWith() 无会话连续性判断,导致跨会话 错误合并列编辑撤销命令 2. 方案:添加 id() 返回 kColumnEditInsertCommandId 和 mergeWith(), 当双方 m_ColumnEditSelections 均非空且 m_sessionId 相同时合并文本; 在 TextEdit 中添加 m_columnEditSessionId 成员,在 mousePressEvent 会话边界(Alt+点击进入和非 Alt 点击退出)递增;将 session ID 传入 InsertTextUndoCommand 构造函数;合并后更新 m_ColumnEditSelections 使撤销基于最新光标位置;修复 redo() 选区计算移除 +1 3. 影响:列编辑撤销现在可在同一会话内一步撤销全部输入;跨会话输入 不会被错误合并;redo 路径在合并后同样正确;普通文本编辑不受影响 (m_ColumnEditSelections 为空,mergeWith 返回 false) Log: 修复列编辑模式撤销需逐字符撤销的问题 Influence: 1. 测试列编辑模式:输入多个字符后 Ctrl+Z,验证一步撤销 2. 测试列编辑撤销后重做再撤销:Ctrl+Shift+Z 后 Ctrl+Z,验证无数据损坏 3. 测试跨会话列编辑:进入列编辑输入,退出后重新进入列编辑再输入, 验证撤销不会跨会话合并 4. 测试普通文本编辑的撤销/重做行为保持不变 PMS: BUG-254289 --- src/editor/dtextedit.cpp | 4 +++- src/editor/dtextedit.h | 1 + src/editor/inserttextundocommand.cpp | 33 ++++++++++++++++++++++++++-- src/editor/inserttextundocommand.h | 5 +++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/editor/dtextedit.cpp b/src/editor/dtextedit.cpp index 1893f77cd..496de6eaf 100644 --- a/src/editor/dtextedit.cpp +++ b/src/editor/dtextedit.cpp @@ -273,7 +273,7 @@ void TextEdit::insertColumnEditTextEx(QString text) for (int i = 0; i < m_altModSelections.size(); i++) { if (m_altModSelections[i].cursor.hasSelection()) deleteTextEx(m_altModSelections[i].cursor); } - QUndoCommand *pInsertStack = new InsertTextUndoCommand(m_altModSelections, text, this); + QUndoCommand *pInsertStack = new InsertTextUndoCommand(m_altModSelections, text, this, m_columnEditSessionId); m_pUndoStack->push(pInsertStack); ensureCursorVisible(); } @@ -6691,6 +6691,7 @@ void TextEdit::mousePressEvent(QMouseEvent *e) m_altStartTextCursor.clearSelection(); this->setTextCursor(m_altStartTextCursor); m_altModSelections.clear(); + m_columnEditSessionId++; } else { @@ -6698,6 +6699,7 @@ void TextEdit::mousePressEvent(QMouseEvent *e) { // 右键,调用右键菜单时候不能清空 m_bIsAltMod = false; m_altModSelections.clear(); + m_columnEditSessionId++; } } diff --git a/src/editor/dtextedit.h b/src/editor/dtextedit.h index de2206a5c..e1a807837 100644 --- a/src/editor/dtextedit.h +++ b/src/editor/dtextedit.h @@ -839,6 +839,7 @@ private slots: QTextCursor m_altStartTextCursor;//开始按住alt鼠标点击光标位置 QTextCursor m_altEndTextCursor;//结束按住alt鼠标点击光标位置 bool m_bIsAltMod = false; + int m_columnEditSessionId = 0; int m_redoCount = 0; QStringList m_pastText; bool m_hasColumnSelection = false; diff --git a/src/editor/inserttextundocommand.cpp b/src/editor/inserttextundocommand.cpp index b02864ff4..c779a01f9 100644 --- a/src/editor/inserttextundocommand.cpp +++ b/src/editor/inserttextundocommand.cpp @@ -16,11 +16,13 @@ InsertTextUndoCommand::InsertTextUndoCommand(const QTextCursor &textcursor, cons InsertTextUndoCommand::InsertTextUndoCommand(QList &selections, const QString &text, QPlainTextEdit *edit, + int sessionId, QUndoCommand *parent) : QUndoCommand(parent) , m_pEdit(edit) , m_sInsertText(text) , m_ColumnEditSelections(selections) + , m_sessionId(sessionId) { m_sInsertText.replace("\r\n", "\n"); } @@ -44,7 +46,12 @@ void InsertTextUndoCommand::undo() } else { int cnt = m_ColumnEditSelections.size(); for (int i = 0; i < cnt; i++) { - m_ColumnEditSelections[i].cursor.deleteChar(); + QTextCursor &cursor = m_ColumnEditSelections[i].cursor; + int endPos = cursor.selectionEnd(); + int startPos = endPos - m_sInsertText.length(); + cursor.setPosition(startPos); + cursor.setPosition(endPos, QTextCursor::KeepAnchor); + cursor.deleteChar(); } if (m_pEdit && !m_ColumnEditSelections.isEmpty()) { @@ -76,7 +83,7 @@ void InsertTextUndoCommand::redo() int cnt = m_ColumnEditSelections.size(); for (int i = 0; i < cnt; i++) { m_ColumnEditSelections[i].cursor.insertText(m_sInsertText); - m_ColumnEditSelections[i].cursor.setPosition(m_ColumnEditSelections[i].cursor.position() - m_sInsertText.length() + 1, + m_ColumnEditSelections[i].cursor.setPosition(m_ColumnEditSelections[i].cursor.position() - m_sInsertText.length(), QTextCursor::KeepAnchor); } @@ -88,6 +95,28 @@ void InsertTextUndoCommand::redo() } } +int InsertTextUndoCommand::id() const +{ + return kColumnEditInsertCommandId; +} + +bool InsertTextUndoCommand::mergeWith(const QUndoCommand *other) +{ + if (other->id() != id()) + return false; + + const auto *cmd = static_cast(other); + if (m_ColumnEditSelections.isEmpty() || cmd->m_ColumnEditSelections.isEmpty()) + return false; + + if (m_sessionId != cmd->m_sessionId) + return false; + + m_sInsertText += cmd->m_sInsertText; + m_ColumnEditSelections = cmd->m_ColumnEditSelections; + return true; +} + /** * @class MidButtonInsertTextUndoCommand * @brief 用于鼠标中键黏贴的插入撤销项,使用 QClipboard::Selection 类型插入选中的数据, diff --git a/src/editor/inserttextundocommand.h b/src/editor/inserttextundocommand.h index 393dc95c0..4eeab9bc3 100644 --- a/src/editor/inserttextundocommand.h +++ b/src/editor/inserttextundocommand.h @@ -18,9 +18,12 @@ class InsertTextUndoCommand : public QUndoCommand explicit InsertTextUndoCommand(QList &selections, const QString &text, QPlainTextEdit *edit, + int sessionId = 0, QUndoCommand *parent = nullptr); virtual void undo(); virtual void redo(); + int id() const override; + bool mergeWith(const QUndoCommand *other) override; private: QPlainTextEdit *m_pEdit = nullptr; @@ -29,7 +32,9 @@ class InsertTextUndoCommand : public QUndoCommand int m_endPostion{0}; QString m_sInsertText; QList m_ColumnEditSelections; + int m_sessionId = 0; QString m_selectText = QString(); + static constexpr int kColumnEditInsertCommandId = 1001; }; /**