more TEditor stubs
authorKevin Lamonte <kevin.lamonte@gmail.com>
Thu, 10 Aug 2017 20:49:55 +0000 (16:49 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Thu, 10 Aug 2017 20:49:55 +0000 (16:49 -0400)
12 files changed:
src/jexer/TEditorWidget.java [new file with mode: 0644]
src/jexer/TWidget.java
src/jexer/TWindow.java
src/jexer/backend/TWindowBackend.java
src/jexer/demos/Demo2.java
src/jexer/demos/DemoEditorWindow.java [new file with mode: 0644]
src/jexer/demos/DemoMainWindow.java
src/jexer/teditor/Document.java [new file with mode: 0644]
src/jexer/teditor/Fragment.java [deleted file]
src/jexer/teditor/Line.java
src/jexer/teditor/Word.java [new file with mode: 0644]
src/jexer/teditor/package-info.java

diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java
new file mode 100644 (file)
index 0000000..6fed1fc
--- /dev/null
@@ -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);
+        }
+    }
+
+}
index 726e13714ea4395af2c75fcf409ba685f24c4fbb..08c0a45ca9c68c28c8345b14faa5f1344d852daf 100644 (file)
@@ -951,9 +951,15 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @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<TWidget> {
         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.
      *
index 8032fdc3d676a9adc4f1988bec84e3a03a75d891..32907a8a9c9046ec4329182623b0137c72391086 100644 (file)
@@ -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));
index 7de6229ae1b70cbeff9dbaf1c42920754c82c63c..c3ed393dfcdb5d7c5688e6ff18dd05bddedc2267 100644 (file)
@@ -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);
             }
index 74046b02659f6f79a8592f92bef4dc37059706cc..b6572af2e9b435cd3946cc909121101c1b132f30 100644 (file)
@@ -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 (file)
index 0000000..5639ed7
--- /dev/null
@@ -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);
+        }
+    }
+
+}
index 587a2297cc962d2a99ae803b06101a572349328e..53f30d7d5a630efa202f202fa8c1809ce7176e89 100644 (file)
@@ -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 (file)
index 0000000..cae6f47
--- /dev/null
@@ -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<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]));
+        }
+    }
+
+}
diff --git a/src/jexer/teditor/Fragment.java b/src/jexer/teditor/Fragment.java
deleted file mode 100644 (file)
index 0aa18b3..0000000
+++ /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);
-
-}
index e36a6c9c45826b5e3b6c56d865a9aacba6106e21..b89d8277ed30b1ac6b1a500f9056839b9b87069c 100644 (file)
@@ -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<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
     }
 
 }
diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java
new file mode 100644 (file)
index 0000000..d7a6576
--- /dev/null
@@ -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;
+    }
+
+}
index 5d2f0caa4acc4b37d477c7de23b5c798af4bb366..38af57d1080920a60da7f32fbbbb801b7f1b59d9 100644 (file)
@@ -28,6 +28,6 @@
  */
 
 /**
- * A "stream"-based text editor / word processor backend.
+ * A basic text editor backend supporting word highlighting.
  */
 package jexer.teditor;