richedit: Implemented undo coalescing to group typing events.

Consecutively typed characters are grouped together to be undone
together.  The grouping of typed characters can be stopped by certain
events that are mentioned in MSDN's remarks on the EM_STOPGROUPTYPING
message, which is also implemented by this patch.
This commit is contained in:
Dylan Smith 2008-06-25 10:29:19 -04:00 committed by Alexandre Julliard
parent 9b67a38f1a
commit d1f1346f54
6 changed files with 159 additions and 36 deletions

View File

@ -124,7 +124,7 @@
- EM_SETWORDWRAPMODE 1.0asian
+ EM_SETZOOM 3.0
+ EM_SHOWSCROLLBAR 2.0
- EM_STOPGROUPTYPING 2.0
+ EM_STOPGROUPTYPING 2.0
+ EM_STREAMIN
+ EM_STREAMOUT
+ EM_UNDO
@ -190,7 +190,6 @@
* RICHED20 TODO (incomplete):
*
* - messages/styles/notifications listed above
* - Undo coalescing
* - add remaining CHARFORMAT/PARAFORMAT fields
* - right/center align should strip spaces from the beginning
* - pictures/OLE objects (not just smiling faces that lack API support ;-) )
@ -1546,6 +1545,7 @@ ME_KeyDown(ME_TextEditor *editor, WORD nKey)
case VK_END:
case VK_PRIOR:
case VK_NEXT:
ME_CommitUndo(editor); /* End coalesced undos for typed characters */
ME_ArrowKey(editor, nKey, shift_is_down, ctrl_is_down);
return TRUE;
case VK_BACK:
@ -1554,12 +1554,26 @@ ME_KeyDown(ME_TextEditor *editor, WORD nKey)
if (GetWindowLongW(editor->hWnd, GWL_STYLE) & ES_READONLY)
return FALSE;
if (ME_IsSelection(editor))
{
ME_DeleteSelection(editor);
else if (nKey == VK_DELETE || ME_ArrowKey(editor, VK_LEFT, FALSE, FALSE))
ME_CommitUndo(editor);
}
else if (nKey == VK_DELETE)
{
/* Delete stops group typing.
* (See MSDN remarks on EM_STOPGROUPTYPING message) */
ME_DeleteTextAtCursor(editor, 1, 1);
ME_CommitUndo(editor);
}
else if (ME_ArrowKey(editor, VK_LEFT, FALSE, FALSE))
{
/* Backspace can be grouped for a single undo */
ME_ContinueCoalescingTransaction(editor);
ME_DeleteTextAtCursor(editor, 1, 1);
ME_CommitCoalescingUndo(editor);
}
else
return TRUE;
ME_CommitUndo(editor);
ME_UpdateSelectionLinkAttribute(editor);
ME_UpdateRepaint(editor);
ME_SendRequestResize(editor, FALSE);
@ -2047,11 +2061,9 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam,
return editor->pRedoStack != NULL;
case WM_UNDO: /* FIXME: actually not the same */
case EM_UNDO:
ME_Undo(editor);
return 0;
return ME_Undo(editor);
case EM_REDO:
ME_Redo(editor);
return 0;
return ME_Redo(editor);
case EM_GETOPTIONS:
{
/* these flags are equivalent to the ES_* counterparts */
@ -2947,6 +2959,7 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam,
SetWindowLongPtrW(hWnd, 0, 0);
return 0;
case WM_LBUTTONDOWN:
ME_CommitUndo(editor); /* End coalesced undos for typed characters */
if ((editor->nEventMask & ENM_MOUSEEVENTS) &&
!ME_FilterEvent(editor, msg, &wParam, &lParam))
return 0;
@ -2989,6 +3002,7 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam,
break;
case WM_RBUTTONUP:
case WM_RBUTTONDOWN:
ME_CommitUndo(editor); /* End coalesced undos for typed characters */
if ((editor->nEventMask & ENM_MOUSEEVENTS) &&
!ME_FilterEvent(editor, msg, &wParam, &lParam))
return 0;
@ -3014,6 +3028,7 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam,
ME_SendOldNotify(editor, EN_SETFOCUS);
return 0;
case WM_KILLFOCUS:
ME_CommitUndo(editor); /* End coalesced undos for typed characters */
ME_HideCaret(editor);
editor->bHaveFocus = FALSE;
ME_SendOldNotify(editor, EN_KILLFOCUS);
@ -3099,12 +3114,13 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam,
{
ME_Style *style = ME_GetInsertStyle(editor, 0);
ME_SaveTempStyle(editor);
ME_ContinueCoalescingTransaction(editor);
if (wstr == '\r' && (GetKeyState(VK_SHIFT) & 0x8000))
ME_InsertEndRowFromCursor(editor, 0);
else
ME_InsertTextFromCursor(editor, 0, &wstr, 1, style);
ME_ReleaseStyle(style);
ME_CommitUndo(editor);
ME_CommitCoalescingUndo(editor);
}
if (editor->AutoURLDetect_bEnable) ME_UpdateSelectionLinkAttribute(editor);
@ -3113,6 +3129,9 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam,
}
return 0;
}
case EM_STOPGROUPTYPING:
ME_CommitUndo(editor); /* End coalesced undos for typed characters */
return 0;
case EM_SCROLL: /* fall through */
case WM_VSCROLL:
{

View File

@ -288,8 +288,10 @@ void ME_UpdateSelectionLinkAttribute(ME_TextEditor *editor);
/* undo.c */
ME_UndoItem *ME_AddUndoItem(ME_TextEditor *editor, ME_DIType type, const ME_DisplayItem *pdi);
void ME_CommitUndo(ME_TextEditor *editor);
void ME_Undo(ME_TextEditor *editor);
void ME_Redo(ME_TextEditor *editor);
void ME_ContinueCoalescingTransaction(ME_TextEditor *editor);
void ME_CommitCoalescingUndo(ME_TextEditor *editor);
BOOL ME_Undo(ME_TextEditor *editor);
BOOL ME_Redo(ME_TextEditor *editor);
void ME_EmptyUndoStack(ME_TextEditor *editor);
/* writer.c */

View File

@ -85,8 +85,9 @@ typedef enum {
diUndoSplitParagraph, /* 14 */
diUndoSetParagraphFormat, /* 15 */
diUndoSetCharFormat, /* 16 */
diUndoEndTransaction, /* 17 */
diUndoEndTransaction, /* 17 - marks the end of a group of changes for undo */
diUndoSetDefaultCharFormat, /* 18 */
diUndoPotentialEndTransaction, /* 19 - allows grouping typed chars for undo */
} ME_DIType;
/******************************** run flags *************************/

View File

@ -169,6 +169,7 @@ const char *ME_GetDITypeName(ME_DIType type)
case diTextEnd: return "diTextEnd";
case diStartRow: return "diStartRow";
case diUndoEndTransaction: return "diUndoEndTransaction";
case diUndoPotentialEndTransaction: return "diUndoPotentialEndTransaction";
case diUndoSetParagraphFormat: return "diUndoSetParagraphFormat";
case diUndoSetCharFormat: return "diUndoSetCharFormat";
case diUndoInsertRun: return "diUndoInsertRun";

View File

@ -4396,20 +4396,20 @@ static void test_undo_coalescing(void)
result = SendMessage(hwnd, EM_CANUNDO, 0, 0);
ok (result == TRUE, "Cannot undo typed characters.\n");
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "EM_UNDO Failed to undo typed characters.\n");
ok (result == TRUE, "EM_UNDO Failed to undo typed characters.\n");
result = SendMessage(hwnd, EM_CANREDO, 0, 0);
ok (result == TRUE, "Cannot redo after undo.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "one two three");
todo_wine ok (result == 0, "expected '%s' but got '%s'\n", "one two three", buffer);
ok (result == 0, "expected '%s' but got '%s'\n", "one two three", buffer);
result = SendMessage(hwnd, EM_CANUNDO, 0, 0);
ok (result == TRUE, "Cannot undo typed characters.\n");
result = SendMessage(hwnd, WM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "");
todo_wine ok (result == 0, "expected '%s' but got '%s'\n", "", buffer);
ok (result == 0, "expected '%s' but got '%s'\n", "", buffer);
/* Test the effect of focus changes during typing on undo transactions*/
simulate_typing_characters(hwnd, "one two three");
@ -4419,10 +4419,10 @@ static void test_undo_coalescing(void)
SendMessage(hwnd, WM_SETFOCUS, (WPARAM)NULL, 0);
simulate_typing_characters(hwnd, " four five six");
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "one two three");
todo_wine ok (result == 0, "expected '%s' but got '%s'\n", "one two three", buffer);
ok (result == 0, "expected '%s' but got '%s'\n", "one two three", buffer);
/* Test the effect of the back key during typing on undo transactions */
SendMessage(hwnd, EM_EMPTYUNDOBUFFER, 0, 0);
@ -4435,10 +4435,10 @@ static void test_undo_coalescing(void)
SendMessage(hwnd, WM_KEYUP, VK_BACK, 1);
simulate_typing_characters(hwnd, "e four five six");
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "");
todo_wine ok (result == 0, "expected '%s' but got '%s'\n", "", buffer);
ok (result == 0, "expected '%s' but got '%s'\n", "", buffer);
/* Test the effect of the delete key during typing on undo transactions */
SendMessage(hwnd, EM_EMPTYUNDOBUFFER, 0, 0);
@ -4450,12 +4450,12 @@ static void test_undo_coalescing(void)
SendMessage(hwnd, WM_KEYDOWN, VK_DELETE, 1);
SendMessage(hwnd, WM_KEYUP, VK_DELETE, 1);
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "acd");
ok (result == 0, "expected '%s' but got '%s'\n", "acd", buffer);
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "abcd");
ok (result == 0, "expected '%s' but got '%s'\n", "abcd", buffer);
@ -4469,16 +4469,16 @@ static void test_undo_coalescing(void)
ok (result == 0, "expected %d but got %d\n", 0, result);
simulate_typing_characters(hwnd, " four five six");
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "one two three");
todo_wine ok (result == 0, "expected '%s' but got '%s'\n", "one two three", buffer);
ok (result == 0, "expected '%s' but got '%s'\n", "one two three", buffer);
result = SendMessage(hwnd, EM_UNDO, 0, 0);
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
todo_wine ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
ok (result == TRUE, "Failed to undo typed characters.\n");
SendMessageA(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
result = strcmp(buffer, "");
todo_wine ok (result == 0, "expected '%s' but got '%s'\n", "", buffer);
ok (result == 0, "expected '%s' but got '%s'\n", "", buffer);
DestroyWindow(hwnd);
}

View File

@ -55,10 +55,20 @@ ME_UndoItem *ME_AddUndoItem(ME_TextEditor *editor, ME_DIType type, const ME_Disp
return NULL;
else
{
ME_DisplayItem *pItem = (ME_DisplayItem *)ALLOC_OBJ(ME_UndoItem);
ME_DisplayItem *pItem;
if (editor->pUndoStack
&& editor->pUndoStack->type == diUndoPotentialEndTransaction)
{
editor->pUndoStack->type = diUndoEndTransaction;
}
pItem = (ME_DisplayItem *)ALLOC_OBJ(ME_UndoItem);
((ME_UndoItem *)pItem)->nCR = ((ME_UndoItem *)pItem)->nLF = -1;
switch(type)
{
case diUndoPotentialEndTransaction:
/* only should be added for manually typed chars, not undos or redos */
assert(editor->nUndoMode == umAddToUndo);
/* intentional fall-through to next case */
case diUndoEndTransaction:
break;
case diUndoSetParagraphFormat:
@ -105,7 +115,7 @@ ME_UndoItem *ME_AddUndoItem(ME_TextEditor *editor, ME_DIType type, const ME_Disp
TRACE("Pushing id=%s to undo stack\n", ME_GetDITypeName(type));
pItem->next = editor->pUndoStack;
if (type == diUndoEndTransaction)
if (type == diUndoEndTransaction || type == diUndoPotentialEndTransaction)
editor->nUndoStackSize++;
if (editor->pUndoStack)
editor->pUndoStack->prev = pItem;
@ -154,6 +164,18 @@ ME_UndoItem *ME_AddUndoItem(ME_TextEditor *editor, ME_DIType type, const ME_Disp
}
}
/**
* Commits preceding changes into a transaction that can be undone together.
*
* This should be called after all the changes occur associated with an event
* so that the group of changes can be undone atomically as a transaction.
*
* This will have no effect the undo mode is set to ignore changes, or if no
* changes preceded calling this function before the last time it was called.
*
* This can also be used to conclude a coalescing transaction (used for grouping
* typed characters).
*/
void ME_CommitUndo(ME_TextEditor *editor) {
if (editor->nUndoMode == umIgnore)
return;
@ -168,10 +190,84 @@ void ME_CommitUndo(ME_TextEditor *editor) {
if (editor->pUndoStack->type == diUndoEndTransaction)
return;
if (editor->pUndoStack->type == diUndoPotentialEndTransaction)
{
/* Previous transaction was as a result of characters typed,
* so the end of this transaction is confirmed. */
editor->pUndoStack->type = diUndoEndTransaction;
return;
}
ME_AddUndoItem(editor, diUndoEndTransaction, NULL);
ME_SendSelChange(editor);
}
/**
* Groups supsequent changes with previous ones for an undo if coalescing.
*
* Has no effect if the previous changes were followed by a ME_CommitUndo. This
* function will only have an affect if the previous changes were followed by
* a call to ME_CommitCoalescingUndo, which allows the transaction to be
* continued.
*
* This allows multiple consecutively typed characters to be grouped together
* to be undone by a single undo operation.
*/
void ME_ContinueCoalescingTransaction(ME_TextEditor *editor)
{
ME_DisplayItem* p;
if (editor->nUndoMode == umIgnore)
return;
assert(editor->nUndoMode == umAddToUndo);
p = editor->pUndoStack;
if (p && p->type == diUndoPotentialEndTransaction) {
assert(p->next); /* EndTransactions shouldn't be at bottom of undo stack */
editor->pUndoStack = p->next;
editor->pUndoStack->prev = NULL;
editor->nUndoStackSize--;
ME_DestroyDisplayItem(p);
}
}
/**
* Commits preceding changes into a undo transaction that can be expanded.
*
* This function allows the transaction to be reopened with
* ME_ContinueCoalescingTransaction in order to continue the transaction. If an
* undo item is added to the undo stack as a result of a change without the
* transaction being reopened, then the transaction will be ended, and the
* changes will become a part of the next transaction.
*
* This is used to allow typed characters to be grouped together since each
* typed character results in a single event, and each event adding undo items
* must be committed. Using this function as opposed to ME_CommitUndo allows
* multiple events to be grouped, and undone together.
*/
void ME_CommitCoalescingUndo(ME_TextEditor *editor)
{
if (editor->nUndoMode == umIgnore)
return;
assert(editor->nUndoMode == umAddToUndo);
/* no transactions, no need to commit */
if (!editor->pUndoStack)
return;
/* no need to commit empty transactions */
if (editor->pUndoStack->type == diUndoEndTransaction)
return;
if (editor->pUndoStack->type == diUndoPotentialEndTransaction)
return;
ME_AddUndoItem(editor, diUndoPotentialEndTransaction, NULL);
ME_SendSelChange(editor);
}
static void ME_PlayUndoItem(ME_TextEditor *editor, ME_DisplayItem *pItem)
{
ME_UndoItem *pUItem = (ME_UndoItem *)pItem;
@ -182,6 +278,7 @@ static void ME_PlayUndoItem(ME_TextEditor *editor, ME_DisplayItem *pItem)
switch(pItem->type)
{
case diUndoPotentialEndTransaction:
case diUndoEndTransaction:
assert(0);
case diUndoSetParagraphFormat:
@ -239,20 +336,21 @@ static void ME_PlayUndoItem(ME_TextEditor *editor, ME_DisplayItem *pItem)
}
}
void ME_Undo(ME_TextEditor *editor) {
BOOL ME_Undo(ME_TextEditor *editor) {
ME_DisplayItem *p;
ME_UndoMode nMode = editor->nUndoMode;
if (editor->nUndoMode == umIgnore)
return;
return FALSE;
assert(nMode == umAddToUndo || nMode == umIgnore);
/* no undo items ? */
if (!editor->pUndoStack)
return;
return FALSE;
/* watch out for uncommitted transactions ! */
assert(editor->pUndoStack->type == diUndoEndTransaction);
assert(editor->pUndoStack->type == diUndoEndTransaction
|| editor->pUndoStack->type == diUndoPotentialEndTransaction);
editor->nUndoMode = umAddToRedo;
p = editor->pUndoStack->next;
@ -270,19 +368,20 @@ void ME_Undo(ME_TextEditor *editor) {
p->prev = NULL;
editor->nUndoMode = nMode;
ME_UpdateRepaint(editor);
return TRUE;
}
void ME_Redo(ME_TextEditor *editor) {
BOOL ME_Redo(ME_TextEditor *editor) {
ME_DisplayItem *p;
ME_UndoMode nMode = editor->nUndoMode;
assert(nMode == umAddToUndo || nMode == umIgnore);
if (editor->nUndoMode == umIgnore)
return;
return FALSE;
/* no redo items ? */
if (!editor->pRedoStack)
return;
return FALSE;
/* watch out for uncommitted transactions ! */
assert(editor->pRedoStack->type == diUndoEndTransaction);
@ -302,4 +401,5 @@ void ME_Redo(ME_TextEditor *editor) {
p->prev = NULL;
editor->nUndoMode = nMode;
ME_UpdateRepaint(editor);
return TRUE;
}