X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTEditorWidget.java;h=bea25eda3e74c9c0e2c60c012dfd4e976d1198e4;hb=HEAD;hp=823dea2d113c2d62b66e164dc06b2ce070e8cb69;hpb=b1a8acec8adc13327268b1e600d367b2ba0cccc0;p=fanfix.git diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java index 823dea2..bea25ed 100644 --- a/src/jexer/TEditorWidget.java +++ b/src/jexer/TEditorWidget.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -29,34 +29,47 @@ package jexer; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import jexer.bits.CellAttributes; +import jexer.bits.StringUtils; +import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.teditor.Document; import jexer.teditor.Line; import jexer.teditor.Word; +import static jexer.TCommand.*; import static jexer.TKeypress.*; /** * TEditorWidget displays an editable text document. It is unaware of * scrolling behavior, but can respond to mouse and keyboard events. */ -public final class TEditorWidget extends TWidget { +public class TEditorWidget extends TWidget implements EditMenuUser { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * The number of lines to scroll on mouse wheel up/down. */ private static final int wheelScrollSize = 3; + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The document being edited. */ - private Document document; + protected Document document; /** - * The default color for the TEditor class. + * The default color for the editable text. */ private CellAttributes defaultColor = null; @@ -70,6 +83,71 @@ public final class TEditorWidget extends TWidget { */ private int leftColumn = 0; + /** + * If true, the mouse is dragging a selection. + */ + private boolean inSelection = false; + + /** + * Selection starting column. + */ + private int selectionColumn0; + + /** + * Selection starting line. + */ + private int selectionLine0; + + /** + * Selection ending column. + */ + private int selectionColumn1; + + /** + * Selection ending line. + */ + private int selectionLine1; + + /** + * The list of undo/redo states. + */ + private List undoList = new ArrayList(); + + /** + * The position in undoList for undo/redo. + */ + private int undoListI = 0; + + /** + * The maximum size of the undo list. + */ + private int undoLevel = 50; + + /** + * The saved state for an undo/redo operation. + */ + private class SavedState { + /** + * The Document state. + */ + public Document document; + + /** + * The topmost line number in the visible area. 0-based. + */ + public int topLine = 0; + + /** + * The leftmost column number in the visible area. 0-based. + */ + public int leftColumn = 0; + + } + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor. * @@ -92,32 +170,9 @@ public final class TEditorWidget extends TWidget { document = new Document(text, defaultColor); } - /** - * Draw the text box. - */ - @Override - public void draw() { - for (int i = 0; i < getHeight(); i++) { - // Background line - getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor); - - // Now draw document's line - if (topLine + i < document.getLineCount()) { - Line line = document.getLine(topLine + i); - int x = 0; - for (Word word: line.getWords()) { - // For now, we are cheating: draw outside the left region - // if needed and let screen do the clipping. - getScreen().putStringXY(x - leftColumn, i, word.getText(), - word.getColor()); - x += word.getDisplayLength(); - if (x - leftColumn > getWidth()) { - break; - } - } - } - } - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle mouse press events. @@ -146,9 +201,23 @@ public final class TEditorWidget extends TWidget { } if (mouse.isMouse1()) { - // Set the row and column + // Selection. int newLine = topLine + mouse.getY(); int newX = leftColumn + mouse.getX(); + + inSelection = true; + if (newLine > document.getLineCount() - 1) { + selectionLine0 = document.getLineCount() - 1; + } else { + selectionLine0 = topLine + mouse.getY(); + } + selectionColumn0 = leftColumn + mouse.getX(); + selectionColumn0 = Math.max(0, Math.min(selectionColumn0, + document.getLine(selectionLine0).getDisplayLength() - 1)); + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + + // Set the row and column if (newLine > document.getLineCount() - 1) { // Go to the end document.setLineNumber(document.getLineCount() - 1); @@ -159,19 +228,29 @@ public final class TEditorWidget extends TWidget { setCursorY(mouse.getY()); } alignCursor(); + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } return; } document.setLineNumber(newLine); setCursorY(mouse.getY()); - if (newX > document.getCurrentLine().getDisplayLength()) { + if (newX >= document.getCurrentLine().getDisplayLength()) { document.end(); alignCursor(); } else { document.setCursor(newX); setCursorX(mouse.getX()); } + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } return; + } else { + inSelection = false; } // Pass to children @@ -179,94 +258,67 @@ public final class TEditorWidget extends TWidget { } /** - * Align visible area with document current line. + * Handle mouse motion events. * - * @param topLineIsTop if true, make the top visible line the document - * current line if it was off-screen. If false, make the bottom visible - * line the document current line. + * @param mouse mouse motion event */ - private void alignTopLine(final boolean topLineIsTop) { - int line = document.getLineNumber(); + @Override + public void onMouseMotion(final TMouseEvent mouse) { - if ((line < topLine) || (line > topLine + getHeight() - 1)) { - // Need to move topLine to bring document back into view. - if (topLineIsTop) { - topLine = line - (getHeight() - 1); - if (topLine < 0) { - topLine = 0; - } - assert (topLine >= 0); - } else { - topLine = line; - assert (topLine >= 0); + if (mouse.isMouse1()) { + // Set the row and column + int newLine = topLine + mouse.getY(); + int newX = leftColumn + mouse.getX(); + if ((newLine < 0) || (newX < 0)) { + return; } - } - - /* - System.err.println("line " + line + " topLine " + topLine); - */ - - // Document is in view, let's set cursorY - assert (line >= topLine); - setCursorY(line - topLine); - alignCursor(); - } - /** - * Align document current line with visible area. - * - * @param topLineIsTop if true, make the top visible line the document - * current line if it was off-screen. If false, make the bottom visible - * line the document current line. - */ - private void alignDocument(final boolean topLineIsTop) { - int line = document.getLineNumber(); - int cursor = document.getCursor(); + // Selection. + if (inSelection) { + selectionColumn1 = newX; + selectionLine1 = newLine; + } else { + inSelection = true; + selectionColumn0 = newX; + selectionLine0 = newLine; + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + } - if ((line < topLine) || (line > topLine + getHeight() - 1)) { - // Need to move document to ensure it fits view. - if (topLineIsTop) { - document.setLineNumber(topLine); + if (newLine > document.getLineCount() - 1) { + // Go to the end + document.setLineNumber(document.getLineCount() - 1); + document.end(); + if (newLine > document.getLineCount() - 1) { + setCursorY(document.getLineCount() - 1 - topLine); + } else { + setCursorY(mouse.getY()); + } + alignCursor(); + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } + return; + } + document.setLineNumber(newLine); + setCursorY(mouse.getY()); + if (newX >= document.getCurrentLine().getDisplayLength()) { + document.end(); + alignCursor(); } else { - document.setLineNumber(topLine + (getHeight() - 1)); + document.setCursor(newX); + setCursorX(mouse.getX()); } - if (cursor < document.getCurrentLine().getDisplayLength()) { - document.setCursor(cursor); + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); } + return; } - /* - System.err.println("getLineNumber() " + document.getLineNumber() + - " topLine " + topLine); - */ - - // Document is in view, let's set cursorY - setCursorY(document.getLineNumber() - topLine); - alignCursor(); - } - - /** - * Align visible cursor with document cursor. - */ - private void alignCursor() { - int width = getWidth(); - - int desiredX = document.getCursor() - leftColumn; - if (desiredX < 0) { - // We need to push the screen to the left. - leftColumn = document.getCursor(); - } else if (desiredX > width - 1) { - // We need to push the screen to the right. - leftColumn = document.getCursor() - (width - 1); - } - - /* - System.err.println("document cursor " + document.getCursor() + - " leftColumn " + leftColumn); - */ - - - setCursorX(document.getCursor() - leftColumn); + // Pass to children + super.onMouseDown(mouse); } /** @@ -276,25 +328,93 @@ public final class TEditorWidget extends TWidget { */ @Override public void onKeypress(final TKeypressEvent keypress) { - if (keypress.equals(kbLeft)) { + if (keypress.getKey().isShift()) { + if (keypress.equals(kbShiftLeft) + || keypress.equals(kbShiftRight) + || keypress.equals(kbShiftUp) + || keypress.equals(kbShiftDown) + || keypress.equals(kbShiftPgDn) + || keypress.equals(kbShiftPgUp) + || keypress.equals(kbShiftHome) + || keypress.equals(kbShiftEnd) + ) { + // Shifted navigation keys enable selection + if (!inSelection) { + inSelection = true; + selectionColumn0 = document.getCursor(); + selectionLine0 = document.getLineNumber(); + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + } + } + } else { + if (keypress.equals(kbLeft) + || keypress.equals(kbRight) + || keypress.equals(kbUp) + || keypress.equals(kbDown) + || keypress.equals(kbPgDn) + || keypress.equals(kbPgUp) + || keypress.equals(kbHome) + || keypress.equals(kbEnd) + ) { + // Non-shifted navigation keys disable selection. + inSelection = false; + } + if ((selectionColumn0 == selectionColumn1) + && (selectionLine0 == selectionLine1) + ) { + // The user clicked a spot and started typing. + inSelection = false; + } + } + + if (keypress.equals(kbLeft) + || keypress.equals(kbShiftLeft) + ) { document.left(); alignTopLine(false); - } else if (keypress.equals(kbRight)) { + } else if (keypress.equals(kbRight) + || keypress.equals(kbShiftRight) + ) { document.right(); alignTopLine(true); - } else if (keypress.equals(kbUp)) { + } else if (keypress.equals(kbAltLeft) + || keypress.equals(kbCtrlLeft) + || keypress.equals(kbAltShiftLeft) + || keypress.equals(kbCtrlShiftLeft) + ) { + document.backwardsWord(); + alignTopLine(false); + } else if (keypress.equals(kbAltRight) + || keypress.equals(kbCtrlRight) + || keypress.equals(kbAltShiftRight) + || keypress.equals(kbCtrlShiftRight) + ) { + document.forwardsWord(); + alignTopLine(true); + } else if (keypress.equals(kbUp) + || keypress.equals(kbShiftUp) + ) { document.up(); alignTopLine(false); - } else if (keypress.equals(kbDown)) { + } else if (keypress.equals(kbDown) + || keypress.equals(kbShiftDown) + ) { document.down(); alignTopLine(true); - } else if (keypress.equals(kbPgUp)) { + } else if (keypress.equals(kbPgUp) + || keypress.equals(kbShiftPgUp) + ) { document.up(getHeight() - 1); alignTopLine(false); - } else if (keypress.equals(kbPgDn)) { + } else if (keypress.equals(kbPgDn) + || keypress.equals(kbShiftPgDn) + ) { document.down(getHeight() - 1); alignTopLine(true); - } else if (keypress.equals(kbHome)) { + } else if (keypress.equals(kbHome) + || keypress.equals(kbShiftHome) + ) { if (document.home()) { leftColumn = 0; if (leftColumn < 0) { @@ -302,37 +422,62 @@ public final class TEditorWidget extends TWidget { } setCursorX(0); } - } else if (keypress.equals(kbEnd)) { + } else if (keypress.equals(kbEnd) + || keypress.equals(kbShiftEnd) + ) { if (document.end()) { alignCursor(); } - } else if (keypress.equals(kbCtrlHome)) { + } else if (keypress.equals(kbCtrlHome) + || keypress.equals(kbCtrlShiftHome) + ) { document.setLineNumber(0); document.home(); topLine = 0; leftColumn = 0; setCursorX(0); setCursorY(0); - } else if (keypress.equals(kbCtrlEnd)) { + } else if (keypress.equals(kbCtrlEnd) + || keypress.equals(kbCtrlShiftEnd) + ) { document.setLineNumber(document.getLineCount() - 1); document.end(); alignTopLine(false); } else if (keypress.equals(kbIns)) { - document.setOverwrite(!document.getOverwrite()); + document.setOverwrite(!document.isOverwrite()); } else if (keypress.equals(kbDel)) { - document.del(); - alignCursor(); - } else if (keypress.equals(kbBackspace)) { - document.backspace(); - alignTopLine(false); - } else if (keypress.equals(kbTab)) { - // TODO: tab character. For now just add spaces until we hit - // modulo 8. - for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) { - document.addChar(' '); + if (inSelection) { + deleteSelection(); + alignCursor(); + } else { + saveUndo(); + document.del(); + alignCursor(); + } + } else if (keypress.equals(kbBackspace) + || keypress.equals(kbBackspaceDel) + ) { + if (inSelection) { + deleteSelection(); + alignTopLine(false); + } else { + saveUndo(); + document.backspace(); + alignTopLine(false); } + } else if (keypress.equals(kbTab)) { + deleteSelection(); + saveUndo(); + document.tab(); + alignCursor(); + } else if (keypress.equals(kbShiftTab)) { + deleteSelection(); + saveUndo(); + document.backTab(); alignCursor(); } else if (keypress.equals(kbEnter)) { + deleteSelection(); + saveUndo(); document.enter(); alignTopLine(true); } else if (!keypress.getKey().isFnKey() @@ -340,12 +485,19 @@ public final class TEditorWidget extends TWidget { && !keypress.getKey().isCtrl() ) { // Plain old keystroke, process it + deleteSelection(); + saveUndo(); document.addChar(keypress.getKey().getChar()); alignCursor(); } else { // Pass other keys (tab etc.) on to TWidget super.onKeypress(keypress); } + + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } } /** @@ -377,39 +529,328 @@ public final class TEditorWidget extends TWidget { } /** - * Get the number of lines in the underlying Document. + * Handle posted command events. * - * @return the number of lines + * @param command command event */ - public int getLineCount() { - return document.getLineCount(); - } + @Override + public void onCommand(final TCommandEvent command) { + if (command.equals(cmCut)) { + // Copy text to clipboard, and then remove it. + copySelection(); + deleteSelection(); + return; + } - /** - * Get the current editing row number. 1-based. - * - * @return the editing row number. Row 1 is the first row. - */ - public int getEditingRowNumber() { - return document.getLineNumber() + 1; - } + if (command.equals(cmCopy)) { + // Copy text to clipboard. + copySelection(); + return; + } - /** - * Set the current editing row number. 1-based. - * - * @param row the new editing row number. Row 1 is the first row. - */ - public void setEditingRowNumber(final int row) { - assert (row > 0); - if ((row > 0) && (row < document.getLineCount())) { - document.setLineNumber(row - 1); - alignTopLine(true); + if (command.equals(cmPaste)) { + // Delete selected text, then paste text from clipboard. + deleteSelection(); + + String text = getClipboard().pasteText(); + if (text != null) { + for (int i = 0; i < text.length(); ) { + int ch = text.codePointAt(i); + switch (ch) { + case '\n': + onKeypress(new TKeypressEvent(kbEnter)); + break; + case '\t': + onKeypress(new TKeypressEvent(kbTab)); + break; + default: + if ((ch >= 0x20) && (ch != 0x7F)) { + onKeypress(new TKeypressEvent(false, 0, ch, + false, false, false)); + } + break; + } + + i += Character.charCount(ch); + } + } + return; + } + + if (command.equals(cmClear)) { + // Remove text. + deleteSelection(); + return; } + } + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Get the current editing column number. 1-based. - * + * Draw the text box. + */ + @Override + public void draw() { + CellAttributes selectedColor = getTheme().getColor("teditor.selected"); + + boolean drawSelection = true; + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + if ((startCol == endCol) && (startRow == endRow)) { + drawSelection = false; + } + + for (int i = 0; i < getHeight(); i++) { + // Background line + getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor); + + // Now draw document's line + if (topLine + i < document.getLineCount()) { + Line line = document.getLine(topLine + i); + int x = 0; + for (Word word: line.getWords()) { + // For now, we are cheating: draw outside the left region + // if needed and let screen do the clipping. + getScreen().putStringXY(x - leftColumn, i, word.getText(), + word.getColor()); + x += word.getDisplayLength(); + if (x - leftColumn > getWidth()) { + break; + } + } + + // Highlight selected region + if (inSelection && drawSelection) { + if (startRow == endRow) { + if (topLine + i == startRow) { + for (x = startCol; x <= endCol; x++) { + putAttrXY(x - leftColumn, i, selectedColor); + } + } + } else { + if (topLine + i == startRow) { + for (x = startCol; x < line.getDisplayLength(); x++) { + putAttrXY(x - leftColumn, i, selectedColor); + } + } else if (topLine + i == endRow) { + for (x = 0; x <= endCol; x++) { + putAttrXY(x - leftColumn, i, selectedColor); + } + } else if ((topLine + i >= startRow) + && (topLine + i <= endRow) + ) { + for (x = 0; x < getWidth(); x++) { + putAttrXY(x, i, selectedColor); + } + } + } + } + + } + } + } + + // ------------------------------------------------------------------------ + // TEditorWidget ---------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Set the undo level. + * + * @param undoLevel the maximum number of undo operations + */ + public void setUndoLevel(final int undoLevel) { + this.undoLevel = undoLevel; + } + + /** + * Align visible area with document current line. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignTopLine(final boolean topLineIsTop) { + int line = document.getLineNumber(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move topLine to bring document back into view. + if (topLineIsTop) { + topLine = line - (getHeight() - 1); + if (topLine < 0) { + topLine = 0; + } + assert (topLine >= 0); + } else { + topLine = line; + assert (topLine >= 0); + } + } + + /* + System.err.println("line " + line + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + assert (line >= topLine); + setCursorY(line - topLine); + alignCursor(); + } + + /** + * Align document current line with visible area. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignDocument(final boolean topLineIsTop) { + int line = document.getLineNumber(); + int cursor = document.getCursor(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move document to ensure it fits view. + if (topLineIsTop) { + document.setLineNumber(topLine); + } else { + document.setLineNumber(topLine + (getHeight() - 1)); + } + if (cursor < document.getCurrentLine().getDisplayLength()) { + document.setCursor(cursor); + } + } + + /* + System.err.println("getLineNumber() " + document.getLineNumber() + + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + + /** + * Align visible cursor with document cursor. + */ + private void alignCursor() { + int width = getWidth(); + + int desiredX = document.getCursor() - leftColumn; + if (desiredX < 0) { + // We need to push the screen to the left. + leftColumn = document.getCursor(); + } else if (desiredX > width - 1) { + // We need to push the screen to the right. + leftColumn = document.getCursor() - (width - 1); + } + + /* + System.err.println("document cursor " + document.getCursor() + + " leftColumn " + leftColumn); + */ + + + setCursorX(document.getCursor() - leftColumn); + } + + /** + * Get the number of lines in the underlying Document. + * + * @return the number of lines + */ + public int getLineCount() { + return document.getLineCount(); + } + + /** + * Get the current visible top row number. 1-based. + * + * @return the visible top row number. Row 1 is the first row. + */ + public int getVisibleRowNumber() { + return topLine + 1; + } + + /** + * Set the current visible row number. 1-based. + * + * @param row the new visible row number. Row 1 is the first row. + */ + public void setVisibleRowNumber(final int row) { + assert (row > 0); + if ((row > 0) && (row < document.getLineCount())) { + topLine = row - 1; + alignDocument(true); + } + } + + /** + * Get the current editing row number. 1-based. + * + * @return the editing row number. Row 1 is the first row. + */ + public int getEditingRowNumber() { + return document.getLineNumber() + 1; + } + + /** + * Set the current editing row number. 1-based. + * + * @param row the new editing row number. Row 1 is the first row. + */ + public void setEditingRowNumber(final int row) { + assert (row > 0); + if ((row > 0) && (row < document.getLineCount())) { + document.setLineNumber(row - 1); + alignTopLine(true); + } + } + + /** + * Set the current visible column number. 1-based. + * + * @return the visible column number. Column 1 is the first column. + */ + public int getVisibleColumnNumber() { + return leftColumn + 1; + } + + /** + * Set the current visible column number. 1-based. + * + * @param column the new visible column number. Column 1 is the first + * column. + */ + public void setVisibleColumnNumber(final int column) { + assert (column > 0); + if ((column > 0) && (column < document.getLineLengthMax())) { + leftColumn = column - 1; + alignDocument(true); + } + } + + /** + * Get the current editing column number. 1-based. + * * @return the editing column number. Column 1 is the first column. */ public int getEditingColumnNumber() { @@ -447,6 +888,17 @@ public final class TEditorWidget extends TWidget { return document.getLineLengthMax() + 1; } + /** + * Get the current editing row plain text. 1-based. + * + * @param row the editing row number. Row 1 is the first row. + * @return the plain text of the row + */ + public String getEditingRawLine(final int row) { + Line line = document.getLine(row - 1); + return line.getRawString(); + } + /** * Get the dirty value. * @@ -456,6 +908,22 @@ public final class TEditorWidget extends TWidget { return document.isDirty(); } + /** + * Unset the dirty flag. + */ + public void setNotDirty() { + document.setNotDirty(); + } + + /** + * Get the overwrite value. + * + * @return true if new text will overwrite old text + */ + public boolean isOverwrite() { + return document.isOverwrite(); + } + /** * Save contents to file. * @@ -466,4 +934,508 @@ public final class TEditorWidget extends TWidget { document.saveToFilename(filename); } + /** + * Delete text within the selection bounds. + */ + private void deleteSelection() { + if (!inSelection) { + return; + } + + saveUndo(); + + inSelection = false; + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + /* + System.err.println("INITIAL: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + */ + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + + if (endRow >= document.getLineCount()) { + // The selection started beyond EOF, trim it to EOF. + endRow = document.getLineCount() - 1; + endCol = document.getLine(endRow).getDisplayLength(); + } else if (endRow == document.getLineCount() - 1) { + // The selection started beyond EOF, trim it to EOF. + if (endCol >= document.getLine(endRow).getDisplayLength()) { + endCol = document.getLine(endRow).getDisplayLength() - 1; + } + } + } + /* + System.err.println("FLIP: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + System.err.println(" --END: " + endRow + " " + document.getLineCount() + + " " + document.getLine(endRow).getDisplayLength()); + */ + + assert (endRow < document.getLineCount()); + if (endCol >= document.getLine(endRow).getDisplayLength()) { + endCol = document.getLine(endRow).getDisplayLength() - 1; + } + if (endCol < 0) { + endCol = 0; + } + if (startCol >= document.getLine(startRow).getDisplayLength()) { + startCol = document.getLine(startRow).getDisplayLength() - 1; + } + if (startCol < 0) { + startCol = 0; + } + + // Place the cursor on the selection end, and "press backspace" until + // the cursor matches the selection start. + /* + System.err.println("BEFORE: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + */ + document.setLineNumber(endRow); + document.setCursor(endCol + 1); + while (!((document.getLineNumber() == startRow) + && (document.getCursor() == startCol)) + ) { + /* + System.err.println("DURING: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + */ + + document.backspace(); + } + alignTopLine(true); + } + + /** + * Copy text within the selection bounds to clipboard. + */ + private void copySelection() { + if (!inSelection) { + return; + } + getClipboard().copyText(getSelection()); + } + + /** + * Set the selection. + * + * @param startRow the starting row number. 0-based: row 0 is the first + * row. + * @param startColumn the starting column number. 0-based: column 0 is + * the first column. + * @param endRow the ending row number. 0-based: row 0 is the first row. + * @param endColumn the ending column number. 0-based: column 0 is the + * first column. + */ + public void setSelection(final int startRow, final int startColumn, + final int endRow, final int endColumn) { + + inSelection = true; + selectionLine0 = startRow; + selectionColumn0 = startColumn; + selectionLine1 = endRow; + selectionColumn1 = endColumn; + } + + /** + * Copy text within the selection bounds to a string. + * + * @return the selection as a string, or null if there is no selection + */ + public String getSelection() { + if (!inSelection) { + return null; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + + StringBuilder sb = new StringBuilder(); + + if (endRow > startRow) { + // First line + String line = document.getLine(startRow).getRawString(); + int x = 0; + for (int i = 0; i < line.length(); ) { + int ch = line.codePointAt(i); + + if (x >= startCol) { + sb.append(Character.toChars(ch)); + } + x += StringUtils.width(ch); + i += Character.charCount(ch); + } + sb.append("\n"); + + // Middle lines + for (int y = startRow + 1; y < endRow; y++) { + sb.append(document.getLine(y).getRawString()); + sb.append("\n"); + } + + // Final line + line = document.getLine(endRow).getRawString(); + x = 0; + for (int i = 0; i < line.length(); ) { + int ch = line.codePointAt(i); + + if (x > endCol) { + break; + } + + sb.append(Character.toChars(ch)); + x += StringUtils.width(ch); + i += Character.charCount(ch); + } + } else { + assert (startRow == endRow); + + // Only one line + String line = document.getLine(startRow).getRawString(); + int x = 0; + for (int i = 0; i < line.length(); ) { + int ch = line.codePointAt(i); + + if ((x >= startCol) && (x <= endCol)) { + sb.append(Character.toChars(ch)); + } + + x += StringUtils.width(ch); + i += Character.charCount(ch); + } + } + return sb.toString(); + } + + /** + * Get the selection starting row number. + * + * @return the starting row number, or -1 if there is no selection. + * 0-based: row 0 is the first row. + */ + public int getSelectionStartRow() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return startRow; + } + + /** + * Get the selection starting column number. + * + * @return the starting column number, or -1 if there is no selection. + * 0-based: column 0 is the first column. + */ + public int getSelectionStartColumn() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return startCol; + } + + /** + * Get the selection ending row number. + * + * @return the ending row number, or -1 if there is no selection. + * 0-based: row 0 is the first row. + */ + public int getSelectionEndRow() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return endRow; + } + + /** + * Get the selection ending column number. + * + * @return the ending column number, or -1 if there is no selection. + * 0-based: column 0 is the first column. + */ + public int getSelectionEndColumn() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return endCol; + } + + /** + * Unset the selection. + */ + public void unsetSelection() { + inSelection = false; + } + + /** + * Replace whatever is being selected with new text. If not in + * selection, nothing is replaced. + * + * @param text the new replacement text + */ + public void replaceSelection(final String text) { + if (!inSelection) { + return; + } + + // Delete selected text, then paste text from clipboard. + deleteSelection(); + + for (int i = 0; i < text.length(); ) { + int ch = text.codePointAt(i); + switch (ch) { + case '\n': + onKeypress(new TKeypressEvent(kbEnter)); + break; + case '\t': + onKeypress(new TKeypressEvent(kbTab)); + break; + default: + if ((ch >= 0x20) && (ch != 0x7F)) { + onKeypress(new TKeypressEvent(false, 0, ch, + false, false, false)); + } + break; + } + i += Character.charCount(ch); + } + } + + /** + * Check if selection is available. + * + * @return true if a selection has been made + */ + public boolean hasSelection() { + return inSelection; + } + + /** + * Get the entire contents of the editor as one string. + * + * @return the editor contents + */ + public String getText() { + return document.getText(); + } + + /** + * Set the entire contents of the editor from one string. + * + * @param text the new contents + */ + public void setText(final String text) { + document = new Document(text, defaultColor); + unsetSelection(); + topLine = 0; + leftColumn = 0; + } + + // ------------------------------------------------------------------------ + // EditMenuUser ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut() { + return true; + } + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy() { + return true; + } + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste() { + return true; + } + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear() { + return true; + } + + /** + * Save undo state. + */ + private void saveUndo() { + SavedState state = new SavedState(); + state.document = document.dup(); + state.topLine = topLine; + state.leftColumn = leftColumn; + if (undoLevel > 0) { + while (undoList.size() > undoLevel) { + undoList.remove(0); + } + } + undoList.add(state); + undoListI = undoList.size() - 1; + } + + /** + * Undo an edit. + */ + public void undo() { + inSelection = false; + if ((undoListI >= 0) && (undoListI < undoList.size())) { + SavedState state = undoList.get(undoListI); + document = state.document.dup(); + topLine = state.topLine; + leftColumn = state.leftColumn; + undoListI--; + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + } + + /** + * Redo an edit. + */ + public void redo() { + inSelection = false; + if ((undoListI >= 0) && (undoListI < undoList.size())) { + SavedState state = undoList.get(undoListI); + document = state.document.dup(); + topLine = state.topLine; + leftColumn = state.leftColumn; + undoListI++; + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + } + + /** + * Trim trailing whitespace from lines and trailing empty + * lines from the document. + */ + public void cleanWhitespace() { + document.cleanWhitespace(); + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setHighlighting(final boolean enabled) { + document.setHighlighting(enabled); + } + }