488 lines
14 KiB
C
488 lines
14 KiB
C
/*
|
|
* RichEdit - functions dealing with editor object
|
|
*
|
|
* Copyright 2004 by Krzysztof Foltman
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include "editor.h"
|
|
|
|
WINE_DEFAULT_DEBUG_CHANNEL(richedit);
|
|
|
|
static void destroy_undo_item( struct undo_item *undo )
|
|
{
|
|
switch( undo->type )
|
|
{
|
|
case undo_insert_run:
|
|
heap_free( undo->u.insert_run.str );
|
|
ME_ReleaseStyle( undo->u.insert_run.style );
|
|
break;
|
|
case undo_split_para:
|
|
ME_DestroyString( undo->u.split_para.eol_str );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
heap_free( undo );
|
|
}
|
|
|
|
static void empty_redo_stack(ME_TextEditor *editor)
|
|
{
|
|
struct undo_item *cursor, *cursor2;
|
|
LIST_FOR_EACH_ENTRY_SAFE( cursor, cursor2, &editor->redo_stack, struct undo_item, entry )
|
|
{
|
|
list_remove( &cursor->entry );
|
|
destroy_undo_item( cursor );
|
|
}
|
|
}
|
|
|
|
void ME_EmptyUndoStack(ME_TextEditor *editor)
|
|
{
|
|
struct undo_item *cursor, *cursor2;
|
|
if (editor->nUndoMode == umIgnore)
|
|
return;
|
|
|
|
TRACE("Emptying undo stack\n");
|
|
|
|
editor->nUndoStackSize = 0;
|
|
|
|
LIST_FOR_EACH_ENTRY_SAFE( cursor, cursor2, &editor->undo_stack, struct undo_item, entry )
|
|
{
|
|
list_remove( &cursor->entry );
|
|
destroy_undo_item( cursor );
|
|
}
|
|
|
|
empty_redo_stack( editor );
|
|
}
|
|
|
|
static struct undo_item *add_undo( ME_TextEditor *editor, enum undo_type type )
|
|
{
|
|
struct undo_item *undo, *item;
|
|
struct list *head;
|
|
|
|
if (editor->nUndoMode == umIgnore) return NULL;
|
|
if (editor->nUndoLimit == 0) return NULL;
|
|
|
|
undo = heap_alloc( sizeof(*undo) );
|
|
if (!undo) return NULL;
|
|
undo->type = type;
|
|
|
|
if (editor->nUndoMode == umAddToUndo || editor->nUndoMode == umAddBackToUndo)
|
|
{
|
|
|
|
head = list_head( &editor->undo_stack );
|
|
if (head)
|
|
{
|
|
item = LIST_ENTRY( head, struct undo_item, entry );
|
|
if (item->type == undo_potential_end_transaction)
|
|
item->type = undo_end_transaction;
|
|
}
|
|
|
|
if (editor->nUndoMode == umAddToUndo)
|
|
TRACE("Pushing id=%d to undo stack, deleting redo stack\n", type);
|
|
else
|
|
TRACE("Pushing id=%d to undo stack\n", type);
|
|
|
|
list_add_head( &editor->undo_stack, &undo->entry );
|
|
|
|
if (type == undo_end_transaction || type == undo_potential_end_transaction)
|
|
editor->nUndoStackSize++;
|
|
|
|
if (editor->nUndoStackSize > editor->nUndoLimit)
|
|
{
|
|
struct undo_item *cursor2;
|
|
/* remove oldest undo from stack */
|
|
LIST_FOR_EACH_ENTRY_SAFE_REV( item, cursor2, &editor->undo_stack, struct undo_item, entry )
|
|
{
|
|
BOOL done = (item->type == undo_end_transaction);
|
|
list_remove( &item->entry );
|
|
destroy_undo_item( item );
|
|
if (done) break;
|
|
}
|
|
editor->nUndoStackSize--;
|
|
}
|
|
|
|
/* any new operation (not redo) clears the redo stack */
|
|
if (editor->nUndoMode == umAddToUndo) empty_redo_stack( editor );
|
|
}
|
|
else if (editor->nUndoMode == umAddToRedo)
|
|
{
|
|
TRACE("Pushing id=%d to redo stack\n", type);
|
|
list_add_head( &editor->redo_stack, &undo->entry );
|
|
}
|
|
|
|
return undo;
|
|
}
|
|
|
|
BOOL add_undo_insert_run( ME_TextEditor *editor, int pos, const WCHAR *str, int len, int flags, ME_Style *style )
|
|
{
|
|
struct undo_item *undo = add_undo( editor, undo_insert_run );
|
|
if (!undo) return FALSE;
|
|
|
|
undo->u.insert_run.str = heap_alloc( (len + 1) * sizeof(WCHAR) );
|
|
if (!undo->u.insert_run.str)
|
|
{
|
|
ME_EmptyUndoStack( editor );
|
|
return FALSE;
|
|
}
|
|
memcpy( undo->u.insert_run.str, str, len * sizeof(WCHAR) );
|
|
undo->u.insert_run.str[len] = 0;
|
|
undo->u.insert_run.pos = pos;
|
|
undo->u.insert_run.len = len;
|
|
undo->u.insert_run.flags = flags;
|
|
undo->u.insert_run.style = style;
|
|
ME_AddRefStyle( style );
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL add_undo_set_para_fmt( ME_TextEditor *editor, const ME_Paragraph *para )
|
|
{
|
|
struct undo_item *undo = add_undo( editor, undo_set_para_fmt );
|
|
if (!undo) return FALSE;
|
|
|
|
undo->u.set_para_fmt.pos = para->nCharOfs;
|
|
undo->u.set_para_fmt.fmt = para->fmt;
|
|
undo->u.set_para_fmt.border = para->border;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL add_undo_set_char_fmt( ME_TextEditor *editor, int pos, int len, const CHARFORMAT2W *fmt )
|
|
{
|
|
struct undo_item *undo = add_undo( editor, undo_set_char_fmt );
|
|
if (!undo) return FALSE;
|
|
|
|
undo->u.set_char_fmt.pos = pos;
|
|
undo->u.set_char_fmt.len = len;
|
|
undo->u.set_char_fmt.fmt = *fmt;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL add_undo_join_paras( ME_TextEditor *editor, int pos )
|
|
{
|
|
struct undo_item *undo = add_undo( editor, undo_join_paras );
|
|
if (!undo) return FALSE;
|
|
|
|
undo->u.join_paras.pos = pos;
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL add_undo_split_para( ME_TextEditor *editor, const ME_Paragraph *para, ME_String *eol_str, const ME_Cell *cell )
|
|
{
|
|
struct undo_item *undo = add_undo( editor, undo_split_para );
|
|
if (!undo) return FALSE;
|
|
|
|
undo->u.split_para.pos = para->nCharOfs - eol_str->nLen;
|
|
undo->u.split_para.eol_str = eol_str;
|
|
undo->u.split_para.fmt = para->fmt;
|
|
undo->u.split_para.border = para->border;
|
|
undo->u.split_para.flags = para->prev_para->member.para.nFlags & ~MEPF_CELL;
|
|
|
|
if (cell)
|
|
{
|
|
undo->u.split_para.cell_border = cell->border;
|
|
undo->u.split_para.cell_right_boundary = cell->nRightBoundary;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL add_undo_delete_run( ME_TextEditor *editor, int pos, int len )
|
|
{
|
|
struct undo_item *undo = add_undo( editor, undo_delete_run );
|
|
if (!undo) return FALSE;
|
|
|
|
undo->u.delete_run.pos = pos;
|
|
undo->u.delete_run.len = len;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
struct undo_item *item;
|
|
struct list *head;
|
|
|
|
if (editor->nUndoMode == umIgnore)
|
|
return;
|
|
|
|
assert(editor->nUndoMode == umAddToUndo);
|
|
|
|
/* no transactions, no need to commit */
|
|
head = list_head( &editor->undo_stack );
|
|
if (!head) return;
|
|
|
|
/* no need to commit empty transactions */
|
|
item = LIST_ENTRY( head, struct undo_item, entry );
|
|
if (item->type == undo_end_transaction) return;
|
|
|
|
if (item->type == undo_potential_end_transaction)
|
|
{
|
|
item->type = undo_end_transaction;
|
|
return;
|
|
}
|
|
|
|
add_undo( editor, undo_end_transaction );
|
|
}
|
|
|
|
/**
|
|
* Groups subsequent 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)
|
|
{
|
|
struct undo_item *item;
|
|
struct list *head;
|
|
|
|
if (editor->nUndoMode == umIgnore)
|
|
return;
|
|
|
|
assert(editor->nUndoMode == umAddToUndo);
|
|
|
|
head = list_head( &editor->undo_stack );
|
|
if (!head) return;
|
|
|
|
item = LIST_ENTRY( head, struct undo_item, entry );
|
|
if (item->type == undo_potential_end_transaction)
|
|
{
|
|
list_remove( &item->entry );
|
|
editor->nUndoStackSize--;
|
|
destroy_undo_item( item );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
struct undo_item *item;
|
|
struct list *head;
|
|
|
|
if (editor->nUndoMode == umIgnore)
|
|
return;
|
|
|
|
assert(editor->nUndoMode == umAddToUndo);
|
|
|
|
head = list_head( &editor->undo_stack );
|
|
if (!head) return;
|
|
|
|
/* no need to commit empty transactions */
|
|
item = LIST_ENTRY( head, struct undo_item, entry );
|
|
if (item->type == undo_end_transaction ||
|
|
item->type == undo_potential_end_transaction)
|
|
return;
|
|
|
|
add_undo( editor, undo_potential_end_transaction );
|
|
}
|
|
|
|
static void ME_PlayUndoItem(ME_TextEditor *editor, struct undo_item *undo)
|
|
{
|
|
|
|
if (editor->nUndoMode == umIgnore)
|
|
return;
|
|
TRACE("Playing undo/redo item, id=%d\n", undo->type);
|
|
|
|
switch(undo->type)
|
|
{
|
|
case undo_potential_end_transaction:
|
|
case undo_end_transaction:
|
|
assert(0);
|
|
case undo_set_para_fmt:
|
|
{
|
|
ME_Cursor tmp;
|
|
ME_DisplayItem *para;
|
|
ME_CursorFromCharOfs(editor, undo->u.set_para_fmt.pos, &tmp);
|
|
para = ME_FindItemBack(tmp.pRun, diParagraph);
|
|
add_undo_set_para_fmt( editor, ¶->member.para );
|
|
para->member.para.fmt = undo->u.set_para_fmt.fmt;
|
|
para->member.para.border = undo->u.set_para_fmt.border;
|
|
para->member.para.nFlags |= MEPF_REWRAP;
|
|
break;
|
|
}
|
|
case undo_set_char_fmt:
|
|
{
|
|
ME_Cursor start, end;
|
|
ME_CursorFromCharOfs(editor, undo->u.set_char_fmt.pos, &start);
|
|
end = start;
|
|
ME_MoveCursorChars(editor, &end, undo->u.set_char_fmt.len, FALSE);
|
|
ME_SetCharFormat(editor, &start, &end, &undo->u.set_char_fmt.fmt);
|
|
break;
|
|
}
|
|
case undo_insert_run:
|
|
{
|
|
ME_Cursor tmp;
|
|
ME_CursorFromCharOfs(editor, undo->u.insert_run.pos, &tmp);
|
|
ME_InsertRunAtCursor(editor, &tmp, undo->u.insert_run.style,
|
|
undo->u.insert_run.str,
|
|
undo->u.insert_run.len,
|
|
undo->u.insert_run.flags);
|
|
break;
|
|
}
|
|
case undo_delete_run:
|
|
{
|
|
ME_Cursor tmp;
|
|
ME_CursorFromCharOfs(editor, undo->u.delete_run.pos, &tmp);
|
|
ME_InternalDeleteText(editor, &tmp, undo->u.delete_run.len, TRUE);
|
|
break;
|
|
}
|
|
case undo_join_paras:
|
|
{
|
|
ME_Cursor tmp;
|
|
ME_CursorFromCharOfs(editor, undo->u.join_paras.pos, &tmp);
|
|
ME_JoinParagraphs(editor, tmp.pPara, TRUE);
|
|
break;
|
|
}
|
|
case undo_split_para:
|
|
{
|
|
ME_Cursor tmp;
|
|
ME_DisplayItem *this_para, *new_para;
|
|
BOOL bFixRowStart;
|
|
int paraFlags = undo->u.split_para.flags & (MEPF_ROWSTART|MEPF_CELL|MEPF_ROWEND);
|
|
ME_CursorFromCharOfs(editor, undo->u.split_para.pos, &tmp);
|
|
if (tmp.nOffset)
|
|
ME_SplitRunSimple(editor, &tmp);
|
|
this_para = tmp.pPara;
|
|
bFixRowStart = this_para->member.para.nFlags & MEPF_ROWSTART;
|
|
if (bFixRowStart)
|
|
{
|
|
/* Re-insert the paragraph before the table, making sure the nFlag value
|
|
* is correct. */
|
|
this_para->member.para.nFlags &= ~MEPF_ROWSTART;
|
|
}
|
|
new_para = ME_SplitParagraph(editor, tmp.pRun, tmp.pRun->member.run.style,
|
|
undo->u.split_para.eol_str->szData, undo->u.split_para.eol_str->nLen, paraFlags);
|
|
if (bFixRowStart)
|
|
new_para->member.para.nFlags |= MEPF_ROWSTART;
|
|
new_para->member.para.fmt = undo->u.split_para.fmt;
|
|
new_para->member.para.border = undo->u.split_para.border;
|
|
if (paraFlags)
|
|
{
|
|
ME_DisplayItem *pCell = new_para->member.para.pCell;
|
|
pCell->member.cell.nRightBoundary = undo->u.split_para.cell_right_boundary;
|
|
pCell->member.cell.border = undo->u.split_para.cell_border;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
BOOL ME_Undo(ME_TextEditor *editor)
|
|
{
|
|
ME_UndoMode nMode = editor->nUndoMode;
|
|
struct list *head;
|
|
struct undo_item *undo, *cursor2;
|
|
|
|
if (editor->nUndoMode == umIgnore) return FALSE;
|
|
assert(nMode == umAddToUndo || nMode == umIgnore);
|
|
|
|
head = list_head( &editor->undo_stack );
|
|
if (!head) return FALSE;
|
|
|
|
/* watch out for uncommitted transactions ! */
|
|
undo = LIST_ENTRY( head, struct undo_item, entry );
|
|
assert(undo->type == undo_end_transaction
|
|
|| undo->type == undo_potential_end_transaction);
|
|
|
|
editor->nUndoMode = umAddToRedo;
|
|
|
|
list_remove( &undo->entry );
|
|
destroy_undo_item( undo );
|
|
|
|
LIST_FOR_EACH_ENTRY_SAFE( undo, cursor2, &editor->undo_stack, struct undo_item, entry )
|
|
{
|
|
if (undo->type == undo_end_transaction) break;
|
|
ME_PlayUndoItem( editor, undo );
|
|
list_remove( &undo->entry );
|
|
destroy_undo_item( undo );
|
|
}
|
|
|
|
ME_MoveCursorFromTableRowStartParagraph(editor);
|
|
add_undo( editor, undo_end_transaction );
|
|
ME_CheckTablesForCorruption(editor);
|
|
editor->nUndoStackSize--;
|
|
editor->nUndoMode = nMode;
|
|
ME_UpdateRepaint(editor, FALSE);
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL ME_Redo(ME_TextEditor *editor)
|
|
{
|
|
ME_UndoMode nMode = editor->nUndoMode;
|
|
struct list *head;
|
|
struct undo_item *undo, *cursor2;
|
|
|
|
assert(nMode == umAddToUndo || nMode == umIgnore);
|
|
|
|
if (editor->nUndoMode == umIgnore) return FALSE;
|
|
|
|
head = list_head( &editor->redo_stack );
|
|
if (!head) return FALSE;
|
|
|
|
/* watch out for uncommitted transactions ! */
|
|
undo = LIST_ENTRY( head, struct undo_item, entry );
|
|
assert( undo->type == undo_end_transaction );
|
|
|
|
editor->nUndoMode = umAddBackToUndo;
|
|
list_remove( &undo->entry );
|
|
destroy_undo_item( undo );
|
|
|
|
LIST_FOR_EACH_ENTRY_SAFE( undo, cursor2, &editor->redo_stack, struct undo_item, entry )
|
|
{
|
|
if (undo->type == undo_end_transaction) break;
|
|
ME_PlayUndoItem( editor, undo );
|
|
list_remove( &undo->entry );
|
|
destroy_undo_item( undo );
|
|
}
|
|
ME_MoveCursorFromTableRowStartParagraph(editor);
|
|
add_undo( editor, undo_end_transaction );
|
|
ME_CheckTablesForCorruption(editor);
|
|
editor->nUndoMode = nMode;
|
|
ME_UpdateRepaint(editor, FALSE);
|
|
return TRUE;
|
|
}
|