| /* | |
| * 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 = calloc(1, 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; | |
| } |