Skip to content

Add actions to allow table cell alignment and fix backspace issues #203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions src/main/java/com/gluonhq/richtextarea/RichTextAreaSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.TextAlignment;

import java.util.Comparator;
import java.util.List;
Expand Down Expand Up @@ -142,18 +143,30 @@ interface ActionBuilder extends Function<KeyEvent, ActionCmd>{}
}),
entry( new KeyCodeCombination(BACK_SPACE, SHIFT_ANY), e -> {
int caret = viewModel.getCaretPosition();
return viewModel.getParagraphWithCaret()
.filter(p -> p.getStart() == caret)
.map(p -> {
ParagraphDecoration decoration = viewModel.getDecorationAtParagraph();
if (decoration.getGraphicType() != ParagraphDecoration.GraphicType.NONE) {
return ACTION_CMD_FACTORY.decorateParagraph(ParagraphDecoration.builder().fromDecoration(decoration).graphicType(ParagraphDecoration.GraphicType.NONE).build());
} else if (decoration.getIndentationLevel() > 0) {
return ACTION_CMD_FACTORY.decorateParagraph(ParagraphDecoration.builder().fromDecoration(decoration).indentationLevel(decoration.getIndentationLevel() - 1).build());
Paragraph paragraph = viewModel.getParagraphWithCaret().orElse(null);
ParagraphDecoration decoration = viewModel.getDecorationAtParagraph();
if (decoration != null && paragraph != null) {
if (decoration.hasTableDecoration()) {
Table table = new Table(viewModel.getTextBuffer().getText(paragraph.getStart(), paragraph.getEnd()),
paragraph.getStart(), decoration.getTableDecoration().getRows(), decoration.getTableDecoration().getColumns());
if (table.isCaretAtStartOfCell(caret)) {
// check backspace at beginning of each cell to prevent moving text from one cell to the other.
// and just move caret if cell was empty:
if (table.isCaretAtEmptyCell(caret)) {
return ACTION_CMD_FACTORY.caretMove(Direction.BACK, false, false, false);
}
return null;
})
.orElse(ACTION_CMD_FACTORY.removeText(-1));
}
} else if (paragraph.getStart() == caret) {
// check backspace at beginning of paragraph:
if (decoration.getGraphicType() != ParagraphDecoration.GraphicType.NONE) {
return ACTION_CMD_FACTORY.decorateParagraph(ParagraphDecoration.builder().fromDecoration(decoration).graphicType(ParagraphDecoration.GraphicType.NONE).build());
} else if (decoration.getIndentationLevel() > 0) {
return ACTION_CMD_FACTORY.decorateParagraph(ParagraphDecoration.builder().fromDecoration(decoration).indentationLevel(decoration.getIndentationLevel() - 1).build());
}
}
}
return ACTION_CMD_FACTORY.removeText(-1);
}),
entry( new KeyCodeCombination(DELETE), e -> ACTION_CMD_FACTORY.removeText(0)),
entry( new KeyCodeCombination(B, SHORTCUT_DOWN), e -> {
Expand Down Expand Up @@ -198,6 +211,7 @@ interface ActionBuilder extends Function<KeyEvent, ActionCmd>{}
private final SortedList<Paragraph> paragraphSortedList = new SortedList<>(viewModel.getParagraphList(), Comparator.comparing(Paragraph::getStart));

final ContextMenu contextMenu = new ContextMenu();
private ObservableList<MenuItem> tableCellContextMenuItems;
private ObservableList<MenuItem> tableContextMenuItems;
private ObservableList<MenuItem> editableContextMenuItems;
private ObservableList<MenuItem> nonEditableContextMenuItems;
Expand Down Expand Up @@ -420,6 +434,7 @@ public void dispose() {
getSkinnable().focusedProperty().removeListener(focusListener);
getSkinnable().removeEventHandler(DragEvent.ANY, dndHandler);
contextMenu.getItems().clear();
tableCellContextMenuItems = null;
tableContextMenuItems = null;
editableContextMenuItems = null;
nonEditableContextMenuItems = null;
Expand Down Expand Up @@ -590,6 +605,16 @@ private void keyTypedListener(KeyEvent e) {

private void populateContextMenu(boolean isEditable) {
if (isEditable && editableContextMenuItems == null) {
tableCellContextMenuItems = FXCollections.observableArrayList(
createMenuItem("Delete cell contents", ACTION_CMD_FACTORY.deleteTableCell()),
new SeparatorMenuItem(),
createMenuItem("Align left", ACTION_CMD_FACTORY.alignTableCell(TextAlignment.LEFT)),
createMenuItem("Centre", ACTION_CMD_FACTORY.alignTableCell(TextAlignment.CENTER)),
createMenuItem("Justify", ACTION_CMD_FACTORY.alignTableCell(TextAlignment.JUSTIFY)),
createMenuItem("Align right", ACTION_CMD_FACTORY.alignTableCell(TextAlignment.RIGHT))
);
Menu tableCellMenu = new Menu("Table Cell");
tableCellMenu.getItems().addAll(tableCellContextMenuItems);
tableContextMenuItems = FXCollections.observableArrayList(
createMenuItem("Insert table", ACTION_CMD_FACTORY.insertTable(new TableDecoration(1,2))),
createMenuItem("Delete table", ACTION_CMD_FACTORY.deleteTable()),
Expand All @@ -600,7 +625,9 @@ private void populateContextMenu(boolean isEditable) {
new SeparatorMenuItem(),
createMenuItem("Add row above", ACTION_CMD_FACTORY.insertTableRowAbove()),
createMenuItem("Add row below", ACTION_CMD_FACTORY.insertTableRowBelow()),
createMenuItem("Delete row", ACTION_CMD_FACTORY.deleteTableRow())
createMenuItem("Delete row", ACTION_CMD_FACTORY.deleteTableRow()),
new SeparatorMenuItem(),
tableCellMenu
);
Menu tableMenu = new Menu("Table");
tableMenu.getItems().addAll(tableContextMenuItems);
Expand Down
59 changes: 54 additions & 5 deletions src/main/java/com/gluonhq/richtextarea/model/Table.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
package com.gluonhq.richtextarea.model;

import com.gluonhq.richtextarea.Selection;

import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static com.gluonhq.richtextarea.viewmodel.RichTextAreaViewModel.Direction;

/**
* Table is a {@link Paragraph} with a single text string that contains
* {@link TextBuffer#ZERO_WIDTH_TABLE_SEPARATOR zero width separators} to
* indicate the cell separation, ending with an end of line character {@code "\n"}.
*
* The number of rows and columns of the table is defined by the {@link TableDecoration} that
* decorates the paragraph.
*
* For instance, the following string corresponds to a 1 row x 2 columns table:
*
* <pre>
* {@code some text|some more text<n>
* }</pre>
*
* The start of the paragraph defines the start of the first cell, and the end of line
* defines the end of the last cell. Zero width separators (in number of rows x columns - 1)
* define the separation of the inner cells.
*/
public class Table {

public static final Logger LOGGER = Logger.getLogger(Table.class.getName());
Expand All @@ -16,6 +37,7 @@ public class Table {
private final int rows;
private final int columns;
private final List<Integer> positions;
private final List<String> textCells;

public Table(String text, int start, int rows, int columns) {
this.text = text;
Expand All @@ -24,6 +46,7 @@ public Table(String text, int start, int rows, int columns) {
this.columns = columns;

positions = getTablePositions();
textCells = getTextForCells();
}

public int getRows() {
Expand All @@ -38,6 +61,24 @@ public int getTableTextLength() {
return text.length() - (text.endsWith("\n") ? 1 : 0);
}

public boolean isCaretAtStartOfCell(int caret) {
return caret == start || positions.stream().anyMatch(i -> i + 1 == caret);
}

public boolean isCaretAtEmptyCell(int caret) {
int currentCell = getCurrentCell(caret);
return textCells.get(currentCell).isEmpty();
}

public Selection getCellSelection(int caret) {
int currentCell = getCurrentCell(caret);
if (currentCell == 0) {
return new Selection(start, start + textCells.get(0).length());
} else {
return new Selection(positions.get(currentCell - 1) + 1, positions.get(currentCell));
}
}

public int getNextRow(int caret, Direction direction) {
return getCurrentRow(caret) + (direction == Direction.DOWN ? 1 : 0);
}
Expand Down Expand Up @@ -67,15 +108,12 @@ public int getCaretAtNextRow(int caret, Direction direction) {
} else {
// move caret to same column of previous row, or before start of paragraph
return currentRow == 0 ?
Math.max(start - 1, 0): positions.get((currentRow - 1) * columns + currentCol);
Math.max(start - 1, 0) : positions.get((currentRow - 1) * columns + currentCol);
}
}

public List<Integer> selectNextCell(int caret, Direction direction) {
int currentRow = getCurrentRow(caret);
int currentCol = getCurrentColumn(caret);
int currentCell = currentRow * columns + currentCol;
System.out.println(positions);
int currentCell = getCurrentCell(caret);
if (direction == Direction.FORWARD) {
// move caret to next cell, or after end of paragraph
return currentCell < positions.size() - 1 ?
Expand Down Expand Up @@ -154,6 +192,12 @@ public void printTable() {
LOGGER.fine("Table:\n" + sb);
}

private int getCurrentCell(int caret) {
int currentRow = getCurrentRow(caret);
int currentCol = getCurrentColumn(caret);
return currentRow * columns + currentCol;
}

private List<Integer> getTablePositions() {
List<Integer> positions = IntStream.iterate(text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR),
index -> index >= 0,
Expand All @@ -165,4 +209,9 @@ private List<Integer> getTablePositions() {
return positions;
}

private List<String> getTextForCells() {
return Stream.of(text.split("" + TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR))
.map(s -> s.replace("\n", ""))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.gluonhq.richtextarea.model.TableDecoration;
import com.gluonhq.richtextarea.model.TextDecoration;
import javafx.scene.input.KeyEvent;
import javafx.scene.text.TextAlignment;

public final class ActionCmdFactory {

Expand Down Expand Up @@ -95,6 +96,14 @@ public ActionCmd deleteTableRow() {
return new ActionCmdTable(ActionCmdTable.TableOperation.DELETE_ROW);
}

public ActionCmd deleteTableCell() {
return new ActionCmdTable(ActionCmdTable.TableOperation.DELETE_CELL_CONTENT);
}

public ActionCmd alignTableCell(TextAlignment textAlignment) {
return new ActionCmdTable(textAlignment);
}

public ActionCmd removeText(int caretOffset) {
return new ActionCmdRemoveText(caretOffset);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.gluonhq.richtextarea.viewmodel;

import com.gluonhq.richtextarea.Selection;
import com.gluonhq.richtextarea.model.Paragraph;
import com.gluonhq.richtextarea.model.ParagraphDecoration;
import com.gluonhq.richtextarea.model.Table;
import com.gluonhq.richtextarea.model.TableDecoration;
import com.gluonhq.richtextarea.model.TextBuffer;
import com.gluonhq.richtextarea.undo.CommandManager;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.text.TextAlignment;

import static com.gluonhq.richtextarea.viewmodel.RichTextAreaViewModel.Direction;

Expand All @@ -15,25 +18,38 @@ class ActionCmdTable implements ActionCmd {
public enum TableOperation {
CREATE_TABLE,
DELETE_TABLE,

ADD_COLUMN_BEFORE,
ADD_COLUMN_AFTER,
DELETE_COLUMN,
ADD_ROW_ABOVE,
ADD_ROW_BELOW,
DELETE_ROW
DELETE_ROW,

DELETE_CELL_CONTENT,
ALIGN_CELL_CONTENT
}
private final TableDecoration tableDecoration;
private final TableOperation tableOperation;
private final TextAlignment textAlignment;
private String text;

public ActionCmdTable(TableDecoration tableDecoration) {
this.tableDecoration = tableDecoration;
this.tableOperation = TableOperation.CREATE_TABLE;
this(tableDecoration, TableOperation.CREATE_TABLE, null);
}

public ActionCmdTable(TableOperation tableOperation) {
this.tableDecoration = null;
this(null, tableOperation, null);
}

public ActionCmdTable(TextAlignment textAlignment) {
this(null, TableOperation.ALIGN_CELL_CONTENT, textAlignment);
}

ActionCmdTable(TableDecoration tableDecoration, TableOperation tableOperation, TextAlignment textAlignment) {
this.tableDecoration = tableDecoration;
this.tableOperation = tableOperation;
this.textAlignment = textAlignment;
}

@Override
Expand Down Expand Up @@ -73,7 +89,6 @@ public void apply(RichTextAreaViewModel viewModel) {
Table table = new Table(text, p.getStart(), oldRows, oldColumns);
int currentRow = table.getCurrentRow(caret);
int currentCol = table.getCurrentColumn(caret);
System.out.println("currentCol = " + currentRow + " x " + currentCol);
switch (tableOperation) {
case ADD_ROW_BELOW:
case ADD_ROW_ABOVE: {
Expand Down Expand Up @@ -132,6 +147,17 @@ public void apply(RichTextAreaViewModel viewModel) {
commandManager.execute(new RemoveAndDecorateTableCmd(0, p.getEnd() - p.getStart() - 1, ParagraphDecoration.builder().presets().build()));
viewModel.setCaretPosition(Math.max(p.getStart() - 1, 0));
}
case DELETE_CELL_CONTENT: {
Selection cellSelection = table.getCellSelection(caret);
if (cellSelection.isDefined()) {
viewModel.setCaretPosition(cellSelection.getStart());
commandManager.execute(new RemoveTextCmd(0, cellSelection.getEnd() - cellSelection.getStart()));
}
}
case ALIGN_CELL_CONTENT: {
oldTableDecoration.getCellAlignment()[currentRow][currentCol] = textAlignment;
commandManager.execute(new DecorateCmd(ParagraphDecoration.builder().tableDecoration(oldTableDecoration).build()));
}
default:
break;
}
Expand Down Expand Up @@ -171,10 +197,24 @@ public BooleanBinding getDisabledBinding(RichTextAreaViewModel viewModel) {
// delete row disabled if columns <= 1
return oldColumns <= 1;
}
Paragraph paragraph = viewModel.getParagraphWithCaret().orElse(null);
if (paragraph != null) {
int caret = viewModel.getCaretPosition();
text = viewModel.getTextBuffer().getText(paragraph.getStart(), paragraph.getEnd());
Table table = new Table(text, paragraph.getStart(), oldRows, oldColumns);
if (tableOperation == TableOperation.DELETE_CELL_CONTENT) {
// disable if cell has no content
return table.isCaretAtEmptyCell(caret);
}
if (tableOperation == TableOperation.ALIGN_CELL_CONTENT) {
// disable if alignment is the same
return (tableDecoration.getCellAlignment()[table.getCurrentRow(caret)][table.getCurrentColumn(caret)] == textAlignment);
}
}
return false;
}
},
viewModel.editableProperty(), viewModel.decorationAtParagraphProperty());
viewModel.editableProperty(), viewModel.decorationAtParagraphProperty(), viewModel.caretPositionProperty());
}

}