wine/dlls/riched20/table.c
Michael Stefaniuc 60b2167901 riched20: Use wide-char string literals.
Signed-off-by: Michael Stefaniuc <mstefani@winehq.org>
Signed-off-by: Huw Davies <huw@codeweavers.com>
Signed-off-by: Alexandre Julliard <julliard@winehq.org>
2020-11-19 21:23:22 +01:00

566 lines
18 KiB
C

/*
* RichEdit functions dealing with on tables
*
* Copyright 2008 by Dylan Smith
*
* 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
*/
/*
* The implementation of tables differs greatly between version 3.0
* (in riched20.dll) and version 4.1 (in msftedit.dll) of richedit controls.
* Currently Wine is not distinguishing between version 3.0 and version 4.1,
* so v4.1 is assumed unless v1.0 is being emulated (i.e. riched32.dll is used).
* If this lack of distinction causes a bug in a Windows application, then Wine
* will need to start making this distinction.
*
* Richedit version 1.0 - 3.0:
* Tables are implemented in these versions using tabs at the end of cells,
* and tab stops to position the cells. The paragraph format flag PFE_TABLE
* will indicate that the paragraph is a table row. Note that in this
* implementation there is one paragraph per table row.
*
* Richedit version 4.1:
* Tables are implemented such that cells can contain multiple paragraphs,
* each with its own paragraph format, and cells may even contain tables
* nested within the cell.
*
* There is also a paragraph at the start of each table row that contains
* the rows paragraph format (e.g. to change the row alignment to row), and a
* paragraph at the end of the table row with the PFE_TABLEROWDELIMITER flag
* set. The paragraphs at the start and end of the table row should always be
* empty, but should have a length of 2.
*
* Wine implements this using display items (ME_DisplayItem) with a type of
* diCell. These cell display items store the cell properties, and are
* inserted into the editors linked list before each cell, and at the end of
* the last cell. The cell display item for a cell comes before the paragraphs
* for the cell, but the last cell display item refers to no cell, so it is
* just a delimiter.
*/
#include "editor.h"
#include "rtf.h"
static ME_Paragraph* table_insert_end_para( ME_TextEditor *editor, ME_Cursor *cursor,
const WCHAR *eol_str, int eol_len, int para_flags )
{
ME_Style *style = style_get_insert_style( editor, cursor );
ME_Paragraph *para;
if (cursor->nOffset) run_split( editor, cursor );
para = para_split( editor, cursor->run, style, eol_str, eol_len, para_flags );
ME_ReleaseStyle( style );
cursor->para = para;
cursor->run = para_first_run( para );
return para;
}
ME_Paragraph* table_insert_row_start( ME_TextEditor *editor, ME_Cursor *cursor )
{
ME_Paragraph *para;
para = table_insert_end_para( editor, cursor, L"\r\n", 2, MEPF_ROWSTART );
return para_prev( para );
}
ME_Paragraph* table_insert_row_start_at_para( ME_TextEditor *editor, ME_Paragraph *para )
{
ME_Paragraph *prev_para, *end_para, *start_row;
ME_Cursor cursor;
cursor.para = para;
cursor.run = para_first_run( para );
cursor.nOffset = 0;
start_row = table_insert_row_start( editor, &cursor );
end_para = para_next( editor->pCursors[0].para );
prev_para = para_next( start_row );
para = para_next( prev_para );
while (para != end_para)
{
para->cell = para_cell( prev_para );
para->nFlags |= MEPF_CELL;
para->nFlags &= ~(MEPF_ROWSTART | MEPF_ROWEND);
para->fmt.dwMask |= PFM_TABLE | PFM_TABLEROWDELIMITER;
para->fmt.wEffects |= PFE_TABLE;
para->fmt.wEffects &= ~PFE_TABLEROWDELIMITER;
prev_para = para;
para = para_next( para );
}
return start_row;
}
/* Inserts a diCell and starts a new paragraph for the next cell.
*
* Returns the first paragraph of the new cell. */
ME_Paragraph* table_insert_cell( ME_TextEditor *editor, ME_Cursor *cursor )
{
WCHAR tab = '\t';
return table_insert_end_para( editor, editor->pCursors, &tab, 1, MEPF_CELL );
}
ME_Paragraph* table_insert_row_end( ME_TextEditor *editor, ME_Cursor *cursor )
{
ME_Paragraph *para;
para = table_insert_end_para( editor, cursor, L"\r\n", 2, MEPF_ROWEND );
return para_prev( para );
}
ME_Paragraph* table_row_end( ME_Paragraph *para )
{
ME_Cell *cell;
if (para->nFlags & MEPF_ROWEND) return para;
if (para->nFlags & MEPF_ROWSTART) para = para_next( para );
cell = para_cell( para );
while (cell_next( cell ))
cell = cell_next( cell );
para = &ME_FindItemFwd( cell_get_di( cell ), diParagraph )->member.para;
assert( para && para->nFlags & MEPF_ROWEND );
return para;
}
ME_Paragraph* table_row_start( ME_Paragraph *para )
{
ME_Cell *cell;
if (para->nFlags & MEPF_ROWSTART) return para;
if (para->nFlags & MEPF_ROWEND) para = para_prev( para );
cell = para_cell( para );
while (cell_prev( cell ))
cell = cell_prev( cell );
para = &ME_FindItemBack( cell_get_di( cell ), diParagraph )->member.para;
assert( para && para->nFlags & MEPF_ROWSTART );
return para;
}
ME_Paragraph* table_outer_para( ME_Paragraph *para )
{
if (para->nFlags & MEPF_ROWEND) para = para_prev( para );
while (para_cell( para ))
{
para = table_row_start( para );
if (!para_cell( para )) break;
para = &ME_FindItemBack( cell_get_di( para_cell( para ) ), diParagraph )->member.para;
}
return para;
}
ME_Cell *table_row_first_cell( ME_Paragraph *para )
{
if (!para_in_table( para )) return NULL;
para = para_next( table_row_start( para ) );
return para_cell( para );
}
ME_Cell *table_row_end_cell( ME_Paragraph *para )
{
if (!para_in_table( para )) return NULL;
para = para_prev( table_row_end( para ));
return cell_next( para_cell( para ) );
}
ME_Cell *cell_create( void )
{
ME_DisplayItem *item = ME_MakeDI( diCell );
return &item->member.cell;
}
ME_Cell *cell_next( ME_Cell *cell )
{
return cell->next_cell;
}
ME_Cell *cell_prev( ME_Cell *cell )
{
return cell->prev_cell;
}
ME_Paragraph *cell_first_para( ME_Cell *cell )
{
return &ME_FindItemFwd( cell_get_di( cell ), diParagraph )->member.para;
}
ME_Paragraph *cell_end_para( ME_Cell *cell )
{
ME_Cell *next = cell_next( cell );
if (!next) return cell_first_para( cell ); /* End of row */
return &ME_FindItemBack( cell_get_di( next ), diParagraph )->member.para;
}
/* Table rows should either be deleted completely or not at all. */
void table_protect_partial_deletion( ME_TextEditor *editor, ME_Cursor *c, int *num_chars )
{
int start_ofs = ME_GetCursorOfs( c );
ME_Cursor c2 = *c;
ME_Paragraph *this_para = c->para, *end_para;
ME_MoveCursorChars( editor, &c2, *num_chars, FALSE );
end_para = c2.para;
if (c2.run->nFlags & MERF_ENDPARA)
{
/* End offset might be in the middle of the end paragraph run.
* If this is the case, then we need to use the next paragraph as the last
* paragraphs.
*/
int remaining = start_ofs + *num_chars - c2.run->nCharOfs - end_para->nCharOfs;
if (remaining)
{
assert( remaining < c2.run->len );
end_para = para_next( end_para );
}
}
if (!editor->bEmulateVersion10) /* v4.1 */
{
if (para_cell( this_para ) != para_cell( end_para ) ||
((this_para->nFlags | end_para->nFlags) & (MEPF_ROWSTART | MEPF_ROWEND)))
{
while (this_para != end_para)
{
ME_Paragraph *next_para = para_next( this_para );
BOOL truancate_del = FALSE;
if (this_para->nFlags & MEPF_ROWSTART)
{
/* The following while loop assumes that next_para is MEPF_ROWSTART,
* so moving back one paragraph lets it be processed as the start
* of the row. */
next_para = this_para;
this_para = para_prev( this_para );
}
else if (para_cell( next_para) != para_cell( this_para ) || this_para->nFlags & MEPF_ROWEND)
{
/* Start of the deletion from after the start of the table row. */
truancate_del = TRUE;
}
while (!truancate_del && next_para->nFlags & MEPF_ROWSTART)
{
next_para = para_next( table_row_end( next_para ) );
if (next_para->nCharOfs > start_ofs + *num_chars)
{
/* End of deletion is not past the end of the table row. */
next_para = para_next( this_para );
/* Delete the end paragraph preceding the table row if the
* preceding table row will be empty. */
if (this_para->nCharOfs >= start_ofs) next_para = para_next( next_para );
truancate_del = TRUE;
}
else this_para = para_prev( next_para );
}
if (truancate_del)
{
ME_Run *end_run = para_end_run( para_prev( next_para ) );
int new_chars = next_para->nCharOfs - start_ofs - end_run->len;
new_chars = max( new_chars, 0 );
assert( new_chars <= *num_chars);
*num_chars = new_chars;
break;
}
this_para = next_para;
}
}
}
else /* v1.0 - 3.0 */
{
ME_Run *run;
int chars_to_boundary;
if ((this_para->nCharOfs != start_ofs || this_para == end_para) && para_in_table( this_para ))
{
run = c->run;
/* Find the next tab or end paragraph to use as a delete boundary */
while (!(run->nFlags & (MERF_TAB | MERF_ENDPARA)))
run = run_next( run );
chars_to_boundary = run->nCharOfs - c->run->nCharOfs - c->nOffset;
*num_chars = min( *num_chars, chars_to_boundary );
}
else if (para_in_table( end_para ))
{
/* The deletion starts from before the row, so don't join it with
* previous non-empty paragraphs. */
ME_Paragraph *cur_para;
run = NULL;
if (start_ofs > this_para->nCharOfs)
{
cur_para = para_prev( end_para );
run = para_end_run( cur_para );
}
if (!run)
{
cur_para = end_para;
run = para_first_run( end_para );
}
if (run)
{
chars_to_boundary = cur_para->nCharOfs + run->nCharOfs - start_ofs;
if (chars_to_boundary >= 0) *num_chars = min( *num_chars, chars_to_boundary );
}
}
if (*num_chars < 0) *num_chars = 0;
}
}
ME_Paragraph* table_append_row( ME_TextEditor *editor, ME_Paragraph *table_row )
{
WCHAR endl = '\r', tab = '\t';
ME_Run *run;
int i;
if (!editor->bEmulateVersion10) /* v4.1 */
{
ME_Cell *new_cell, *cell;
ME_Paragraph *para, *prev_table_end, *new_row_start;
cell = table_row_first_cell( table_row );
prev_table_end = table_row_end( table_row );
para = para_next( prev_table_end );
run = para_first_run( para );
editor->pCursors[0].para = para;
editor->pCursors[0].run = run;
editor->pCursors[0].nOffset = 0;
editor->pCursors[1] = editor->pCursors[0];
new_row_start = table_insert_row_start( editor, editor->pCursors );
new_cell = table_row_first_cell( new_row_start );
/* Copy cell properties */
new_cell->nRightBoundary = cell->nRightBoundary;
new_cell->border = cell->border;
while (cell_next( cell ))
{
cell = cell_next( cell );
para = table_insert_cell( editor, editor->pCursors );
new_cell = para_cell( para );
/* Copy cell properties */
new_cell->nRightBoundary = cell->nRightBoundary;
new_cell->border = cell->border;
};
para = table_insert_row_end( editor, editor->pCursors );
para->fmt = prev_table_end->fmt;
/* return the table row start for the inserted paragraph */
return new_row_start;
}
else /* v1.0 - 3.0 */
{
run = para_end_run( table_row );
assert( para_in_table( table_row ) );
editor->pCursors[0].para = table_row;
editor->pCursors[0].run = run;
editor->pCursors[0].nOffset = 0;
editor->pCursors[1] = editor->pCursors[0];
ME_InsertTextFromCursor( editor, 0, &endl, 1, run->style );
run = editor->pCursors[0].run;
for (i = 0; i < table_row->fmt.cTabCount; i++)
ME_InsertTextFromCursor( editor, 0, &tab, 1, run->style );
return para_next( table_row );
}
}
/* Selects the next table cell or appends a new table row if at end of table */
static void table_select_next_cell_or_append( ME_TextEditor *editor, ME_Run *run )
{
ME_Paragraph *para = run->para;
ME_Cell *cell;
int i;
assert( para_in_table( para ) );
if (!editor->bEmulateVersion10) /* v4.1 */
{
/* Get the initial cell */
if (para->nFlags & MEPF_ROWSTART) cell = para_cell( para_next( para ) );
else if (para->nFlags & MEPF_ROWEND) cell = para_cell( para_prev( para ) );
else cell = para_cell( para );
/* Get the next cell. */
if (cell_next( cell ) && cell_next( cell_next( cell ) ))
cell = cell_next( cell );
else
{
para = para_next( table_row_end( para ) );
if (para->nFlags & MEPF_ROWSTART) cell = para_cell( para_next( para ) );
else
{
/* Insert row */
para = para_prev( para );
para = table_append_row( editor, table_row_start( para ) );
/* Put cursor at the start of the new table row */
para = para_next( para );
editor->pCursors[0].para = para;
editor->pCursors[0].run = para_first_run( para );
editor->pCursors[0].nOffset = 0;
editor->pCursors[1] = editor->pCursors[0];
ME_WrapMarkedParagraphs(editor);
return;
}
}
/* Select cell */
editor->pCursors[1].para = cell_first_para( cell );
editor->pCursors[1].run = para_first_run( editor->pCursors[1].para );
editor->pCursors[1].nOffset = 0;
editor->pCursors[0].para = cell_end_para( cell );
editor->pCursors[0].run = para_end_run( editor->pCursors[0].para );
editor->pCursors[0].nOffset = 0;
}
else /* v1.0 - 3.0 */
{
if (run->nFlags & MERF_ENDPARA && para_in_table( para_next( para ) ))
{
run = run_next_all_paras( run );
assert(run);
}
for (i = 0; i < 2; i++)
{
while (!(run->nFlags & MERF_TAB))
{
if (!run_next( run ))
{
para = para_next( run->para );
if (para_in_table( para ))
{
run = para_first_run( para );
editor->pCursors[0].para = para;
editor->pCursors[0].run = run;
editor->pCursors[0].nOffset = 0;
i = 1;
}
else
{
/* Insert table row */
para = table_append_row( editor, para_prev( para ) );
/* Put cursor at the start of the new table row */
editor->pCursors[0].para = para;
editor->pCursors[0].run = para_first_run( para );
editor->pCursors[0].nOffset = 0;
editor->pCursors[1] = editor->pCursors[0];
ME_WrapMarkedParagraphs(editor);
return;
}
}
else run = run_next( run );
}
if (i == 0) run = run_next_all_paras( run );
editor->pCursors[i].run = run;
editor->pCursors[i].para = run->para;
editor->pCursors[i].nOffset = 0;
}
}
}
void table_handle_tab( ME_TextEditor *editor, BOOL selected_row )
{
/* FIXME: Shift tab should move to the previous cell. */
ME_Cursor fromCursor, toCursor;
ME_InvalidateSelection(editor);
{
int from, to;
from = ME_GetCursorOfs(&editor->pCursors[0]);
to = ME_GetCursorOfs(&editor->pCursors[1]);
if (from <= to)
{
fromCursor = editor->pCursors[0];
toCursor = editor->pCursors[1];
}
else
{
fromCursor = editor->pCursors[1];
toCursor = editor->pCursors[0];
}
}
if (!editor->bEmulateVersion10) /* v4.1 */
{
if (!para_in_table( toCursor.para ))
{
editor->pCursors[0] = toCursor;
editor->pCursors[1] = toCursor;
}
else table_select_next_cell_or_append( editor, toCursor.run );
}
else /* v1.0 - 3.0 */
{
if (!para_in_table( fromCursor.para ))
{
editor->pCursors[0] = fromCursor;
editor->pCursors[1] = fromCursor;
/* FIXME: For some reason the caret is shown at the start of the
* previous paragraph in v1.0 to v3.0 */
}
else if ((selected_row || !para_in_table( toCursor.para )))
table_select_next_cell_or_append( editor, fromCursor.run );
else
{
ME_Run *run = run_prev( toCursor.run );
if (ME_IsSelection(editor) && !toCursor.nOffset && run && run->nFlags & MERF_TAB)
table_select_next_cell_or_append( editor, run );
else
table_select_next_cell_or_append( editor, toCursor.run );
}
}
ME_InvalidateSelection(editor);
ME_Repaint(editor);
update_caret(editor);
ME_SendSelChange(editor);
}
/* Make sure the cursor is not in the hidden table row start paragraph
* without a selection. */
void table_move_from_row_start( ME_TextEditor *editor )
{
ME_Paragraph *para = editor->pCursors[0].para;
if (para == editor->pCursors[1].para && para->nFlags & MEPF_ROWSTART)
{
/* The cursors should not be at the hidden start row paragraph without
* a selection, so the cursor is moved into the first cell. */
para = para_next( para );
editor->pCursors[0].para = para;
editor->pCursors[0].run = para_first_run( para );
editor->pCursors[0].nOffset = 0;
editor->pCursors[1] = editor->pCursors[0];
}
}
struct RTFTable *ME_MakeTableDef(ME_TextEditor *editor)
{
RTFTable *tableDef = heap_alloc_zero(sizeof(*tableDef));
if (!editor->bEmulateVersion10) /* v4.1 */
tableDef->gapH = 10;
return tableDef;
}
void ME_InitTableDef(ME_TextEditor *editor, struct RTFTable *tableDef)
{
ZeroMemory(tableDef->cells, sizeof(tableDef->cells));
ZeroMemory(tableDef->border, sizeof(tableDef->border));
tableDef->numCellsDefined = 0;
tableDef->leftEdge = 0;
if (!editor->bEmulateVersion10) /* v4.1 */
tableDef->gapH = 10;
else /* v1.0 - 3.0 */
tableDef->gapH = 0;
}