From 12b55d76e3473407bf37fca3667860240cb8f3be Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Thu, 10 Aug 2017 16:49:55 -0400 Subject: [PATCH] more TEditor stubs --- src/jexer/TEditorWidget.java | 169 ++++++++++ src/jexer/TWidget.java | 29 +- src/jexer/TWindow.java | 6 + src/jexer/backend/TWindowBackend.java | 4 +- src/jexer/demos/Demo2.java | 11 +- src/jexer/demos/DemoEditorWindow.java | 108 ++++++ src/jexer/demos/DemoMainWindow.java | 16 +- src/jexer/teditor/Document.java | 250 ++++++++++++++ src/jexer/teditor/Fragment.java | 183 ---------- src/jexer/teditor/Line.java | 467 ++++---------------------- src/jexer/teditor/Word.java | 154 +++++++++ src/jexer/teditor/package-info.java | 2 +- 12 files changed, 798 insertions(+), 601 deletions(-) create mode 100644 src/jexer/TEditorWidget.java create mode 100644 src/jexer/demos/DemoEditorWindow.java create mode 100644 src/jexer/teditor/Document.java delete mode 100644 src/jexer/teditor/Fragment.java create mode 100644 src/jexer/teditor/Word.java diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java new file mode 100644 index 00000000..6fed1fc8 --- /dev/null +++ b/src/jexer/TEditorWidget.java @@ -0,0 +1,169 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import jexer.bits.CellAttributes; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.teditor.Document; +import jexer.teditor.Line; +import jexer.teditor.Word; +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 { + + /** + * The document being edited. + */ + private Document document; + + /** + * Public constructor. + * + * @param parent parent widget + * @param text text on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + */ + public TEditorWidget(final TWidget parent, final String text, final int x, + final int y, final int width, final int height) { + + // Set parent and window + super(parent, x, y, width, height); + + setCursorVisible(true); + document = new Document(text); + } + + /** + * Draw the text box. + */ + @Override + public void draw() { + // Setup my color + CellAttributes color = getTheme().getColor("teditor"); + + int lineNumber = document.getLineNumber(); + for (int i = 0; i < getHeight(); i++) { + // Background line + getScreen().hLineXY(0, i, getWidth(), ' ', color); + + // Now draw document's line + if (lineNumber + i < document.getLineCount()) { + Line line = document.getLine(lineNumber + i); + int x = 0; + for (Word word: line.getWords()) { + getScreen().putStringXY(x, i, word.getText(), + word.getColor()); + x += word.getDisplayLength(); + if (x > getWidth()) { + break; + } + } + } + } + + } + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouse.isMouseWheelUp()) { + document.up(); + return; + } + if (mouse.isMouseWheelDown()) { + document.down(); + return; + } + + // TODO: click sets row and column + + // Pass to children + super.onMouseDown(mouse); + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbLeft)) { + document.left(); + } else if (keypress.equals(kbRight)) { + document.right(); + } else if (keypress.equals(kbUp)) { + document.up(); + } else if (keypress.equals(kbDown)) { + document.down(); + } else if (keypress.equals(kbPgUp)) { + document.up(getHeight() - 1); + } else if (keypress.equals(kbPgDn)) { + document.down(getHeight() - 1); + } else if (keypress.equals(kbHome)) { + document.home(); + } else if (keypress.equals(kbEnd)) { + document.end(); + } else if (keypress.equals(kbCtrlHome)) { + document.setLineNumber(0); + document.home(); + } else if (keypress.equals(kbCtrlEnd)) { + document.setLineNumber(document.getLineCount() - 1); + document.end(); + } else if (keypress.equals(kbIns)) { + document.setOverwrite(!document.getOverwrite()); + } else if (keypress.equals(kbDel)) { + document.del(); + } else if (keypress.equals(kbBackspace)) { + document.backspace(); + } else if (!keypress.getKey().isFnKey() + && !keypress.getKey().isAlt() + && !keypress.getKey().isCtrl() + ) { + // Plain old keystroke, process it + document.addChar(keypress.getKey().getChar()); + } else { + // Pass other keys (tab etc.) on to TWidget + super.onKeypress(keypress); + } + } + +} diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 726e1371..08c0a45c 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -951,9 +951,15 @@ public abstract class TWidget implements Comparable { * @param resize resize event */ public void onResize(final TResizeEvent resize) { - // Default: do nothing, pass to children instead - for (TWidget widget: children) { - widget.onResize(resize); + // Default: change my width/height. + if (resize.getType() == TResizeEvent.Type.WIDGET) { + width = resize.getWidth(); + height = resize.getHeight(); + } else { + // Let children see the screen resize + for (TWidget widget: children) { + widget.onResize(resize); + } } } @@ -1223,6 +1229,23 @@ public abstract class TWidget implements Comparable { return new TText(this, text, x, y, width, height, "ttext"); } + /** + * Convenience function to add an editable text area box to this + * container/window. + * + * @param text text on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + * @return the new text box + */ + public final TEditorWidget addEditor(final String text, final int x, + final int y, final int width, final int height) { + + return new TEditorWidget(this, text, x, y, width, height); + } + /** * Convenience function to spawn a message box. * diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 8032fdc3..32907a8a 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -901,6 +901,12 @@ public class TWindow extends TWidget { } if (inWindowResize) { + // Do not permit resizing below the status line + if (mouse.getAbsoluteY() == application.getDesktopBottom()) { + inWindowResize = false; + return; + } + // Move window over setWidth(resizeWindowWidth + (mouse.getAbsoluteX() - moveWindowMouseX)); diff --git a/src/jexer/backend/TWindowBackend.java b/src/jexer/backend/TWindowBackend.java index 7de6229a..c3ed393d 100644 --- a/src/jexer/backend/TWindowBackend.java +++ b/src/jexer/backend/TWindowBackend.java @@ -382,8 +382,8 @@ public class TWindowBackend extends TWindow implements Backend { event.setY(mouse.getY() - 1); event.setAbsoluteX(event.getX()); event.setAbsoluteY(event.getY()); - otherMouseX = event.getX() + 1; - otherMouseY = event.getY() + 2; + otherMouseX = event.getX() + getX() + 1; + otherMouseY = event.getY() + getY() + 1; synchronized (eventQueue) { eventQueue.add(event); } diff --git a/src/jexer/demos/Demo2.java b/src/jexer/demos/Demo2.java index 74046b02..b6572af2 100644 --- a/src/jexer/demos/Demo2.java +++ b/src/jexer/demos/Demo2.java @@ -44,6 +44,7 @@ public class Demo2 { * @param args Command line arguments */ public static void main(final String [] args) { + ServerSocket server = null; try { if (args.length == 0) { System.err.printf("USAGE: java -cp jexer.jar jexer.demos.Demo2 port\n"); @@ -51,7 +52,7 @@ public class Demo2 { } int port = Integer.parseInt(args[0]); - ServerSocket server = new TelnetServerSocket(port); + server = new TelnetServerSocket(port); while (true) { Socket socket = server.accept(); System.out.printf("New connection: %s\n", socket); @@ -64,6 +65,14 @@ public class Demo2 { } } catch (Exception e) { e.printStackTrace(); + } finally { + if (server != null) { + try { + server.close(); + } catch (Exception e) { + // SQUASH + } + } } } diff --git a/src/jexer/demos/DemoEditorWindow.java b/src/jexer/demos/DemoEditorWindow.java new file mode 100644 index 00000000..5639ed7c --- /dev/null +++ b/src/jexer/demos/DemoEditorWindow.java @@ -0,0 +1,108 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.demos; + +import jexer.*; +import jexer.event.*; +import static jexer.TCommand.*; +import static jexer.TKeypress.*; + +/** + * This window demonstates the TText, THScroller, and TVScroller widgets. + */ +public class DemoEditorWindow extends TWindow { + + /** + * Hang onto my TEditor so I can resize it with the window. + */ + private TEditorWidget editField; + + /** + * Public constructor makes a text window out of any string. + * + * @param parent the main application + * @param title the text string + * @param text the text string + */ + public DemoEditorWindow(final TApplication parent, final String title, + final String text) { + + super(parent, title, 0, 0, 44, 22, RESIZABLE); + editField = addEditor(text, 0, 0, 42, 20); + + statusBar = newStatusBar("Editable text window"); + statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); + statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); + statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); + statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + } + + /** + * Public constructor. + * + * @param parent the main application + */ + public DemoEditorWindow(final TApplication parent) { + this(parent, "Editor", +"This is an example of an editable text field. Some example text follows.\n" + +"\n" + +"This library implements a text-based windowing system loosely\n" + +"reminiscient of Borland's [Turbo\n" + +"Vision](http://en.wikipedia.org/wiki/Turbo_Vision) library. For those\n" + +"wishing to use the actual C++ Turbo Vision library, see [Sergio\n" + +"Sigala's updated version](http://tvision.sourceforge.net/) that runs\n" + +"on many more platforms.\n" + +"\n" + +"This library is licensed MIT. See the file LICENSE for the full license\n" + +"for the details.\n"); + + } + + /** + * Handle window/screen resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + if (event.getType() == TResizeEvent.Type.WIDGET) { + // Resize the text field + TResizeEvent editSize = new TResizeEvent(TResizeEvent.Type.WIDGET, + event.getWidth() - 2, event.getHeight() - 2); + editField.onResize(editSize); + return; + } + + // Pass to children instead + for (TWidget widget: getChildren()) { + widget.onResize(event); + } + } + +} diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java index 587a2297..53f30d7d 100644 --- a/src/jexer/demos/DemoMainWindow.java +++ b/src/jexer/demos/DemoMainWindow.java @@ -122,17 +122,15 @@ public class DemoMainWindow extends TWindow { ); row += 2; - /* - if (!isModal()) { - addLabel("Editor window", 1, row); - addButton("Edito&r", 35, row, - { - new TEditor(application, 0, 0, 60, 15); + addLabel("Editor window", 1, row); + addButton("Edito&r", 35, row, + new TAction() { + public void DO() { + new DemoEditorWindow(getApplication()); } - ); - } + } + ); row += 2; - */ addLabel("Text areas", 1, row); addButton("&Text", 35, row, diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java new file mode 100644 index 00000000..cae6f471 --- /dev/null +++ b/src/jexer/teditor/Document.java @@ -0,0 +1,250 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.teditor; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Document represents a text file, as a collection of lines. + */ +public class Document { + + /** + * The list of lines. + */ + private ArrayList lines = new ArrayList(); + + /** + * The current line number being edited. Note that this is 0-based, the + * first line is line number 0. + */ + private int lineNumber = 0; + + /** + * The overwrite flag. When true, characters overwrite data. + */ + private boolean overwrite = false; + + /** + * Get the overwrite flag. + * + * @return true if addChar() overwrites data, false if it inserts + */ + public boolean getOverwrite() { + return overwrite; + } + + /** + * Set the overwrite flag. + * + * @param overwrite true if addChar() should overwrite data, false if it + * should insert + */ + public void setOverwrite(final boolean overwrite) { + this.overwrite = overwrite; + } + + /** + * Get the current line number being edited. + * + * @return the line number. Note that this is 0-based: 0 is the first + * line. + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Get a specific line by number. + * + * @param lineNumber the line number. Note that this is 0-based: 0 is + * the first line. + * @return the line + */ + public Line getLine(final int lineNumber) { + return lines.get(lineNumber); + } + + /** + * Set the current line number being edited. + * + * @param n the line number. Note that this is 0-based: 0 is the first + * line. + */ + public void setLineNumber(final int n) { + if ((n < 0) || (n > lines.size())) { + throw new IndexOutOfBoundsException("Line size is " + lines.size() + + ", requested index " + n); + } + lineNumber = n; + } + + /** + * Increment the line number by one. If at the last line, do nothing. + */ + public void down() { + if (lineNumber < lines.size() - 1) { + lineNumber++; + } + } + + /** + * Increment the line number by n. If n would go past the last line, + * increment only to the last line. + * + * @param n the number of lines to increment by + */ + public void down(final int n) { + lineNumber += n; + if (lineNumber > lines.size() - 1) { + lineNumber = lines.size() - 1; + } + } + + /** + * Decrement the line number by one. If at the first line, do nothing. + */ + public void up() { + if (lineNumber > 0) { + lineNumber--; + } + } + + /** + * Decrement the line number by n. If n would go past the first line, + * decrement only to the first line. + * + * @param n the number of lines to decrement by + */ + public void up(final int n) { + lineNumber -= n; + if (lineNumber < 0) { + lineNumber = 0; + } + } + + /** + * Decrement the cursor by one. If at the first column, do nothing. + */ + public void left() { + lines.get(lineNumber).left(); + } + + /** + * Increment the cursor by one. If at the last column, do nothing. + */ + public void right() { + lines.get(lineNumber).right(); + } + + /** + * Go to the first column of this line. + */ + public void home() { + lines.get(lineNumber).home(); + } + + /** + * Go to the last column of this line. + */ + public void end() { + lines.get(lineNumber).end(); + } + + /** + * Delete the character under the cursor. + */ + public void del() { + lines.get(lineNumber).del(); + } + + /** + * Delete the character immediately preceeding the cursor. + */ + public void backspace() { + lines.get(lineNumber).backspace(); + } + + /** + * Replace or insert a character at the cursor, depending on overwrite + * flag. + * + * @param ch the character to replace or insert + */ + public void addChar(final char ch) { + lines.get(lineNumber).addChar(ch); + } + + /** + * Get a (shallow) copy of the list of lines. + * + * @return the list of lines + */ + public List getLines() { + return new ArrayList(lines); + } + + /** + * Get the number of lines. + * + * @return the number of lines + */ + public int getLineCount() { + return lines.size(); + } + + /** + * Compute the maximum line length for this document. + * + * @return the number of cells needed to display the longest line + */ + public int getLineLengthMax() { + int n = 0; + for (Line line : lines) { + if (line.getDisplayLength() > n) { + n = line.getDisplayLength(); + } + } + return n; + } + + /** + * Construct a new Document from an existing text string. + * + * @param str the text string + */ + public Document(final String str) { + String [] rawLines = str.split("\n"); + for (int i = 0; i < rawLines.length; i++) { + lines.add(new Line(rawLines[i])); + } + } + +} diff --git a/src/jexer/teditor/Fragment.java b/src/jexer/teditor/Fragment.java deleted file mode 100644 index 0aa18b38..00000000 --- a/src/jexer/teditor/Fragment.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Jexer - Java Text User Interface - * - * The MIT License (MIT) - * - * Copyright (C) 2017 Kevin Lamonte - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Kevin Lamonte [kevin.lamonte@gmail.com] - * @version 1 - */ -package jexer.teditor; - -/** - * A Fragment is the root "item" to be operated upon by the editor. Each - * Fragment is a "piece of the stream" that will be rendered. - * - * Fragments are organized as a doubly-linked list. The have operations for - * traversing the list, splitting a Fragment into two, and joining two - * Fragments into one. - */ -public interface Fragment { - - /** - * Get the number of graphical cells represented by this text. Note that - * a Unicode grapheme cluster can take any number of pixels, but this - * editor is intended to be used with a fixed-width font. So this count - * returns the number of fixed-width cells, NOT the number of grapheme - * clusters. - * - * @return the number of fixed-width cells this fragment's text will - * render to - */ - public int getCellCount(); - - /** - * Get the next Fragment in the list, or null if this Fragment is the - * last node. - * - * @return the next Fragment, or null - */ - public Fragment next(); - - /** - * Set the next Fragment in the list. Note that this performs no sanity - * checking or modifications on fragment; this function can break - * connectivity in the list. - * - * @param fragment the next Fragment, or null - */ - public void setNext(final Fragment fragment); - - /** - * Get the previous Fragment in the list, or null if this Fragment is the - * first node. - * - * @return the previous Fragment, or null - */ - public Fragment prev(); - - /** - * Set the previous Fragment in the list. Note that this performs no - * sanity checking or modifications on fragment; this function can break - * connectivity in the list. - * - * @param fragment the previous Fragment, or null - */ - public void setPrev(final Fragment fragment); - - /** - * See if this Fragment can be joined with the next Fragment in list. - * - * @return true if the join was possible, false otherwise - */ - public boolean isNextJoinable(); - - /** - * Join this Fragment with the next Fragment in list. - * - * @return true if the join was successful, false otherwise - */ - public boolean joinNext(); - - /** - * See if this Fragment can be joined with the previous Fragment in list. - * - * @return true if the join was possible, false otherwise - */ - public boolean isPrevJoinable(); - - /** - * Join this Fragment with the previous Fragment in list. - * - * @return true if the join was successful, false otherwise - */ - public boolean joinPrev(); - - /** - * Split this Fragment into two. 'this' Fragment will contain length - * cells, 'this.next()' will contain (getCellCount() - length) cells. - * - * @param length the number of cells to leave in this Fragment - * @throws IndexOutOfBoundsException if length is negative, or 0, greater - * than (getCellCount() - 1) - */ - public void split(final int length); - - /** - * Insert a new Fragment at a position, splitting the contents of this - * Fragment into two around it. 'this' Fragment will contain the cells - * between 0 and index, 'this.next()' will be the inserted fragment, and - * 'this.next().next()' will contain the cells between 'index' and - * getCellCount() - 1. - * - * @param index the number of cells to leave in this Fragment - * @param fragment the Fragment to insert - * @throws IndexOutOfBoundsException if length is negative, or 0, greater - * than (getCellCount() - 1) - */ - public void split(final int index, Fragment fragment); - - /** - * Insert a new Fragment before this one. - * - * @param fragment the Fragment to insert - */ - public void insert(Fragment fragment); - - /** - * Append a new Fragment at the end of this one. - * - * @param fragment the Fragment to append - */ - public void append(Fragment fragment); - - /** - * Delete this Fragment from the list, and return its next(). - * - * @return this Fragment's next(), or null if it was at the end of the - * list - */ - public Fragment deleteGetNext(); - - /** - * Delete this Fragment from the list, and return its prev(). - * - * @return this Fragment's next(), or null if it was at the beginning of - * the list - */ - public Fragment deleteGetPrev(); - - /** - * Get the anchor position. - * - * @return the anchor number - */ - public int getAnchor(); - - /** - * Set the anchor position. - * - * @param x the new anchor number - */ - public void setAnchor(final int x); - -} diff --git a/src/jexer/teditor/Line.java b/src/jexer/teditor/Line.java index e36a6c9c..b89d8277 100644 --- a/src/jexer/teditor/Line.java +++ b/src/jexer/teditor/Line.java @@ -31,469 +31,132 @@ package jexer.teditor; import java.util.ArrayList; import java.util.List; -import jexer.bits.Cell; -import jexer.bits.CellAttributes; - /** - * A Line represents a single line of text on the screen. Each character is - * a Cell, so it can have color attributes in addition to the basic char. + * A Line represents a single line of text on the screen, as a collection of + * words. */ -public class Line implements Fragment { +public class Line { /** - * The cells of the line. + * The list of words. */ - private List cells; + private ArrayList words = new ArrayList(); /** - * The line number. - */ - private int lineNumber; - - /** - * The previous Fragment in the list. - */ - private Fragment prevFrag; - - /** - * The next Fragment in the list. - */ - private Fragment nextFrag; - - /** - * Construct a new Line from an existing text string. - */ - public Line() { - this(""); - } - - /** - * Construct a new Line from an existing text string. - * - * @param text the code points of the line + * The current cursor position on this line. */ - public Line(final String text) { - cells = new ArrayList(text.length()); - for (int i = 0; i < text.length(); i++) { - cells.add(new Cell(text.charAt(i))); - } - } + private int cursorX; /** - * Reset all colors of this Line to white-on-black. + * The current word that the cursor position is in. */ - public void resetColors() { - setColors(new CellAttributes()); - } + private Word currentWord; /** - * Set all colors of this Line to one color. - * - * @param color the new color to use + * We use getDisplayLength() a lot, so cache the value. */ - public void setColors(final CellAttributes color) { - for (Cell cell: cells) { - cell.setTo(color); - } - } + private int displayLength = -1; /** - * Set the color of one cell. + * Get a (shallow) copy of the list of words. * - * @param index a cell number, between 0 and getCellCount() - * @param color the new color to use - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() + * @return the list of words */ - public void setColor(final int index, final CellAttributes color) { - cells.get(index).setTo(color); + public List getWords() { + return new ArrayList(words); } /** - * Get the raw text that will be rendered. + * Get the on-screen display length. * - * @return the text + * @return the number of cells needed to display this line */ - public String getText() { - char [] text = new char[cells.size()]; - for (int i = 0; i < cells.size(); i++) { - text[i] = cells.get(i).getChar(); + public int getDisplayLength() { + if (displayLength != -1) { + return displayLength; } - return new String(text); - } - - /** - * Get the attributes for a cell. - * - * @param index a cell number, between 0 and getCellCount() - * @return the attributes - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public CellAttributes getColor(final int index) { - return cells.get(index); - } - - /** - * Get the number of graphical cells represented by this text. Note that - * a Unicode grapheme cluster can take any number of pixels, but this - * editor is intended to be used with a fixed-width font. So this count - * returns the number of fixed-width cells, NOT the number of grapheme - * clusters. - * - * @return the number of fixed-width cells this fragment's text will - * render to - */ - public int getCellCount() { - return cells.size(); - } - - /** - * Get the text to render for a specific fixed-width cell. - * - * @param index a cell number, between 0 and getCellCount() - * @return the codepoints to render for this fixed-width cell - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public Cell getCell(final int index) { - return cells.get(index); - } - - /** - * Get the text to render for several fixed-width cells. - * - * @param start a cell number, between 0 and getCellCount() - * @param length the number of cells to return - * @return the codepoints to render for this fixed-width cell - * @throws IndexOutOfBoundsException if start or (start + length) is - * negative or not less than getCellCount() - */ - public String getCells(final int start, final int length) { - char [] text = new char[length]; - for (int i = 0; i < length; i++) { - text[i] = cells.get(i + start).getChar(); + int n = 0; + for (Word word: words) { + n += word.getDisplayLength(); } - return new String(text); - } - - /** - * Sets (replaces) the text to render for a specific fixed-width cell. - * - * @param index a cell number, between 0 and getCellCount() - * @param ch the character for this fixed-width cell - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public void setCell(final int index, final char ch) { - cells.set(index, new Cell(ch)); - } - - /** - * Sets (replaces) the text to render for a specific fixed-width cell. - * - * @param index a cell number, between 0 and getCellCount() - * @param cell the new value for this fixed-width cell - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public void setCell(final int index, final Cell cell) { - cells.set(index, cell); - } - - /** - * Inserts a char to render for a specific fixed-width cell. - * - * @param index a cell number, between 0 and getCellCount() - 1 - * @param ch the character for this fixed-width cell - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public void insertCell(final int index, final char ch) { - cells.add(index, new Cell(ch)); - } - - /** - * Inserts a Cell to render for a specific fixed-width cell. - * - * @param index a cell number, between 0 and getCellCount() - 1 - * @param cell the new value for this fixed-width cell - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public void insertCell(final int index, final Cell cell) { - cells.add(index, cell); - } - - /** - * Delete a specific fixed-width cell. - * - * @param index a cell number, between 0 and getCellCount() - 1 - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public void deleteCell(final int index) { - cells.remove(index); + displayLength = n; + return displayLength; } /** - * Delete several fixed-width cells. - * - * @param start a cell number, between 0 and getCellCount() - 1 - * @param length the number of cells to delete - * @throws IndexOutOfBoundsException if index is negative or not less - * than getCellCount() - */ - public void deleteCells(final int start, final int length) { - for (int i = 0; i < length; i++) { - cells.remove(start); - } - } - - /** - * Appends a char to render for a specific fixed-width cell. - * - * @param ch the character for this fixed-width cell - */ - public void appendCell(final char ch) { - cells.add(new Cell(ch)); - } - - /** - * Inserts a Cell to render for a specific fixed-width cell. - * - * @param cell the new value for this fixed-width cell - */ - public void appendCell(final Cell cell) { - cells.add(cell); - } - - /** - * Get the next Fragment in the list, or null if this Fragment is the - * last node. - * - * @return the next Fragment, or null - */ - public Fragment next() { - return nextFrag; - } - - /** - * Get the previous Fragment in the list, or null if this Fragment is the - * first node. - * - * @return the previous Fragment, or null - */ - public Fragment prev() { - return prevFrag; - } - - /** - * See if this Fragment can be joined with the next Fragment in list. + * Construct a new Line from an existing text string. * - * @return true if the join was possible, false otherwise - */ - public boolean isNextJoinable() { - if ((nextFrag != null) && (nextFrag instanceof Line)) { - return true; + * @param str the text string + */ + public Line(final String str) { + currentWord = new Word(); + words.add(currentWord); + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + Word newWord = currentWord.addChar(ch); + if (newWord != currentWord) { + words.add(newWord); + currentWord = newWord; + } } - return false; } /** - * Join this Fragment with the next Fragment in list. - * - * @return true if the join was successful, false otherwise + * Decrement the cursor by one. If at the first column, do nothing. */ - public boolean joinNext() { - if ((nextFrag == null) || !(nextFrag instanceof Line)) { - return false; + public void left() { + if (cursorX == 0) { + return; } - Line q = (Line) nextFrag; - ArrayList newCells = new ArrayList(this.cells.size() + - q.cells.size()); - newCells.addAll(this.cells); - newCells.addAll(q.cells); - this.cells = newCells; - ((Line) q.nextFrag).prevFrag = this; - nextFrag = q.nextFrag; - return true; + // TODO } /** - * See if this Fragment can be joined with the previous Fragment in list. - * - * @return true if the join was possible, false otherwise + * Increment the cursor by one. If at the last column, do nothing. */ - public boolean isPrevJoinable() { - if ((prevFrag != null) && (prevFrag instanceof Line)) { - return true; + public void right() { + if (cursorX == getDisplayLength() - 1) { + return; } - return false; + // TODO } /** - * Join this Fragment with the previous Fragment in list. - * - * @return true if the join was successful, false otherwise + * Go to the first column of this line. */ - public boolean joinPrev() { - if ((prevFrag == null) || !(prevFrag instanceof Line)) { - return false; - } - Line p = (Line) prevFrag; - ArrayList newCells = new ArrayList(this.cells.size() + - p.cells.size()); - newCells.addAll(p.cells); - newCells.addAll(this.cells); - this.cells = newCells; - ((Line) p.prevFrag).nextFrag = this; - prevFrag = p.prevFrag; - return true; + public void home() { + // TODO } /** - * Set the next Fragment in the list. Note that this performs no sanity - * checking or modifications on fragment; this function can break - * connectivity in the list. - * - * @param fragment the next Fragment, or null + * Go to the last column of this line. */ - public void setNext(Fragment fragment) { - nextFrag = fragment; + public void end() { + // TODO } /** - * Set the previous Fragment in the list. Note that this performs no - * sanity checking or modifications on fragment; this function can break - * connectivity in the list. - * - * @param fragment the previous Fragment, or null + * Delete the character under the cursor. */ - public void setPrev(Fragment fragment) { - prevFrag = fragment; + public void del() { + // TODO } /** - * Split this Fragment into two. 'this' Fragment will contain length - * cells, 'this.next()' will contain (getCellCount() - length) cells. - * - * @param length the number of cells to leave in this Fragment - * @throws IndexOutOfBoundsException if length is negative, or 0, greater - * than (getCellCount() - 1) - */ - public void split(final int length) { - // Create the next node - Line q = new Line(); - q.nextFrag = nextFrag; - q.prevFrag = this; - ((Line) nextFrag).prevFrag = q; - nextFrag = q; - - // Split cells - q.cells = new ArrayList(cells.size() - length); - q.cells.addAll(cells.subList(length, cells.size())); - cells = cells.subList(0, length); - } - - /** - * Insert a new Fragment at a position, splitting the contents of this - * Fragment into two around it. 'this' Fragment will contain the cells - * between 0 and index, 'this.next()' will be the inserted fragment, and - * 'this.next().next()' will contain the cells between 'index' and - * getCellCount() - 1. - * - * @param index the number of cells to leave in this Fragment - * @param fragment the Fragment to insert - * @throws IndexOutOfBoundsException if length is negative, or 0, greater - * than (getCellCount() - 1) - */ - public void split(final int index, Fragment fragment) { - // Create the next node and insert into the list. - Line q = new Line(); - q.nextFrag = nextFrag; - q.nextFrag.setPrev(q); - q.prevFrag = fragment; - fragment.setNext(q); - fragment.setPrev(this); - nextFrag = fragment; - - // Split cells - q.cells = new ArrayList(cells.size() - index); - q.cells.addAll(cells.subList(index, cells.size())); - cells = cells.subList(0, index); - } - - /** - * Insert a new Fragment before this one. - * - * @param fragment the Fragment to insert - */ - public void insert(Fragment fragment) { - fragment.setNext(this); - fragment.setPrev(prevFrag); - prevFrag.setNext(fragment); - prevFrag = fragment; - } - - /** - * Append a new Fragment at the end of this one. - * - * @param fragment the Fragment to append - */ - public void append(Fragment fragment) { - fragment.setNext(nextFrag); - fragment.setPrev(this); - nextFrag.setPrev(fragment); - nextFrag = fragment; - } - - /** - * Delete this Fragment from the list, and return its next(). - * - * @return this Fragment's next(), or null if it was at the end of the - * list - */ - public Fragment deleteGetNext() { - Fragment result = nextFrag; - nextFrag.setPrev(prevFrag); - prevFrag.setNext(nextFrag); - prevFrag = null; - nextFrag = null; - return result; - } - - /** - * Delete this Fragment from the list, and return its prev(). - * - * @return this Fragment's next(), or null if it was at the beginning of - * the list - */ - public Fragment deleteGetPrev() { - Fragment result = prevFrag; - nextFrag.setPrev(prevFrag); - prevFrag.setNext(nextFrag); - prevFrag = null; - nextFrag = null; - return result; - } - - /** - * Get the anchor position. - * - * @return the anchor number + * Delete the character immediately preceeding the cursor. */ - public int getAnchor() { - return lineNumber; + public void backspace() { + // TODO } /** - * Set the anchor position. + * Replace or insert a character at the cursor, depending on overwrite + * flag. * - * @param x the new anchor number + * @param ch the character to replace or insert */ - public void setAnchor(final int x) { - lineNumber = x; + public void addChar(final char ch) { + // TODO } } diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java new file mode 100644 index 00000000..d7a65760 --- /dev/null +++ b/src/jexer/teditor/Word.java @@ -0,0 +1,154 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.teditor; + +import jexer.bits.CellAttributes; + +/** + * A Word represents text that was entered by the user. It can be either + * whitespace or non-whitespace. + */ +public class Word { + + /** + * The color to render this word as on screen. + */ + private CellAttributes color = new CellAttributes(); + + /** + * The actual text of this word. Average word length is 6 characters, + * with a lot of shorter ones, so start with 3. + */ + private StringBuilder text = new StringBuilder(3); + + /** + * Get the color used to display this word on screen. + * + * @return the color + */ + public CellAttributes getColor() { + return new CellAttributes(color); + } + + /** + * Set the color used to display this word on screen. + * + * @param color the color + */ + public void setColor(final CellAttributes color) { + color.setTo(color); + } + + /** + * Get the text to display. + * + * @return the text + */ + public String getText() { + return text.toString(); + } + + /** + * Get the on-screen display length. + * + * @return the number of cells needed to display this word + */ + public int getDisplayLength() { + // For now, just use the text length. In the future, this will be a + // grapheme count. + + // TODO: figure out how to handle the tab character. Do we have a + // global tab stops list and current word position? + return text.length(); + } + + /** + * See if this is a whitespace word. Note that empty string is + * considered whitespace. + * + * @return true if this word is whitespace + */ + public boolean isWhitespace() { + if (text.length() == 0) { + return true; + } + if (Character.isWhitespace(text.charAt(0))) { + return true; + } + return false; + } + + /** + * Construct a word with one character. + * + * @param ch the first character of the word + */ + public Word(final char ch) { + text.append(ch); + } + + /** + * Construct a word with an empty string. + */ + public Word() {} + + /** + * Add a character to this word. If this is a whitespace character + * adding to a non-whitespace word, create a new word and return that; + * similarly if this a non-whitespace character adding to a whitespace + * word, create a new word and return that. + * + * @param ch the new character to add + * @return either this word (if it was added), or a new word that + * contains ch + */ + public Word addChar(final char ch) { + if (text.length() == 0) { + text.append(ch); + return this; + } + if (Character.isWhitespace(text.charAt(0)) + && Character.isWhitespace(ch) + ) { + text.append(ch); + return this; + } + if (!Character.isWhitespace(text.charAt(0)) + && !Character.isWhitespace(ch) + ) { + text.append(ch); + return this; + } + + // We will be splitting here. + Word newWord = new Word(ch); + return newWord; + } + +} diff --git a/src/jexer/teditor/package-info.java b/src/jexer/teditor/package-info.java index 5d2f0caa..38af57d1 100644 --- a/src/jexer/teditor/package-info.java +++ b/src/jexer/teditor/package-info.java @@ -28,6 +28,6 @@ */ /** - * A "stream"-based text editor / word processor backend. + * A basic text editor backend supporting word highlighting. */ package jexer.teditor; -- 2.27.0