--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
* @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);
+ }
}
}
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.
*
}
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));
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);
}
* @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");
}
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);
}
} catch (Exception e) {
e.printStackTrace();
+ } finally {
+ if (server != null) {
+ try {
+ server.close();
+ } catch (Exception e) {
+ // SQUASH
+ }
+ }
}
}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
);
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,
--- /dev/null
+/*
+ * 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<Line> lines = new ArrayList<Line>();
+
+ /**
+ * 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<Line> getLines() {
+ return new ArrayList<Line>(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]));
+ }
+ }
+
+}
+++ /dev/null
-/*
- * 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);
-
-}
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<Cell> cells;
+ private ArrayList<Word> words = new ArrayList<Word>();
/**
- * 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<Cell>(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<Word> getWords() {
+ return new ArrayList<Word>(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<Cell> newCells = new ArrayList<Cell>(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<Cell> newCells = new ArrayList<Cell>(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<Cell>(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<Cell>(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
}
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
*/
/**
- * A "stream"-based text editor / word processor backend.
+ * A basic text editor backend supporting word highlighting.
*/
package jexer.teditor;