TEditor 50% complete
authorKevin Lamonte <kevin.lamonte@gmail.com>
Sat, 12 Aug 2017 17:39:06 +0000 (13:39 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Sat, 12 Aug 2017 17:39:06 +0000 (13:39 -0400)
14 files changed:
LICENSE
README.md
docs/TODO.md
docs/worklog.md
src/jexer/TApplication.java
src/jexer/TEditorWidget.java
src/jexer/backend/SwingTerminal.java
src/jexer/bits/ColorTheme.java
src/jexer/demos/DemoEditorWindow.java
src/jexer/demos/DemoMainWindow.java
src/jexer/teditor/Document.java
src/jexer/teditor/Highlighter.java [new file with mode: 0644]
src/jexer/teditor/Line.java
src/jexer/teditor/Word.java

diff --git a/LICENSE b/LICENSE
index 09bbfe05123f70fcd772c5296416413a0bf063e2..5f13f7a31da8db8c50f2c64eda52630afd2e255f 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2016 Kevin Lamonte
+Copyright (c) 2013-2017 Kevin Lamonte
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
index 1e61a77c39346de09ba1ee75bb5f4082c4c49f64..7dc0bb4b32ebe78dd9af6f88a7b5310eac880605 100644 (file)
--- a/README.md
+++ b/README.md
@@ -205,6 +205,11 @@ Some arbitrary design decisions had to be made when either the
 obviously expected behavior did not happen or when a specification was
 ambiguous.  This section describes such issues.
 
+  - The JVM needs some warmup time to exhibit the true performance
+    behavior.  Drag a window around for a bit to see this: the initial
+    performance is slow, then the JIT compiler kicks in and Jexer can
+    be visually competitive with C/C++ curses applications.
+
   - See jexer.tterminal.ECMA48 for more specifics of terminal
     emulation limitations.
 
index 08a912aa76746752ffd921a9c8d1c363a54f1584..e59638221e8828c0e424e6127c9678a6f60b5fd3 100644 (file)
@@ -10,29 +10,19 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the
 
 0.0.5
 
-- TApplication
-  - getAllWindows()
-  - Expose menu management functions (addMenu, getMenu, getAllMenus,
-    removeMenu, ...)
-
 - TEditor
-
-  - Swich Line from String to ArrayList<Cell>
-    - StringUtils.justify functions for ArrayList<Cell>
+  - TEditorWidget:
+    - Mouse wheel is buggy as hell
+    - Actual editing
+    - Cut and Paste
   - TEditorWindow extends TScrollableWindow
-  - TEditor widget with keystroke functions:
-    - cursorRight/Left/...
-    - insertChar
-    - deleteForwardChar
-    - deleteBackwardChar
-    - deleteBackwardWord
-    - wordCount
-    - ...
-
-- Eliminate all Eclipse warnings
+  - TTextArea extends TScrollableWidget
 
 0.0.6
 
+- TEditor
+  - True tokenization and syntax highlighting: Java, C, Clojure
+
 - Finish up multiscreen support:
   - cmAbort to cmScreenDisconnected
   - cmScreenConnected
@@ -98,8 +88,6 @@ Fix all marked TODOs in code
 
 Eliminate DEBUG, System.err prints
 
-Version in:
-
 Update written by date to current year:
     All code headers
     VERSION
@@ -108,6 +96,7 @@ Tag github
 
 Upload to SF
 
+Upload to sonatype
 
 
 Brainstorm Wishlist
index 2d1ae20f843e209573df647ef5e43de1c2b54f5f..570404097cc2a5a1be28f3e90a24c07955945539 100644 (file)
@@ -1,6 +1,21 @@
 Jexer Work Log
 ==============
 
+August 12, 2017
+
+TEditor is stubbed in about 50% complete now.  I have a Highlighter
+class that provides different colors based on Word text values, but it
+is a lot too simple to do true syntax highlighting.  I am noodling on
+the right design that would let TEditor be both a programmer's editor
+(so Highlighter needs to have state and do a lexical scan) and a word
+processor (where Word needs to tokenize on whitespace).  I estimate
+probably a good 2-4 weeks left to get the editor behavior where I want
+it, and then after that will be the 0.0.5 release.
+
+Finding more minor paper cuts and fixing them: the mouse cursor being
+ahead of a window drag event, SwingTerminal resetting blink on new
+input, prevent TWindow from resizing down into the status bar.
+
 August 8, 2017
 
 Multiscreen is looking really cool!  Demo6 now brings up three
index ab9c1962f6334094a03c6342eaad21eee6a03e8e..8b436ab9a99ec9219b9755ffce379c62d834e10f 100644 (file)
@@ -580,7 +580,7 @@ public class TApplication implements Runnable {
     }
 
     /**
-     * Get the list of windows.
+     * Get a (shallow) copy of the window list.
      *
      * @return a copy of the list of windows for this application
      */
@@ -1071,19 +1071,6 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Peek at the mouse position
-        if (event instanceof TMouseEvent) {
-            TMouseEvent mouse = (TMouseEvent) event;
-            synchronized (getScreen()) {
-                if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                    oldMouseX = mouseX;
-                    oldMouseY = mouseY;
-                    mouseX = mouse.getX();
-                    mouseY = mouse.getY();
-                }
-            }
-        }
-
         // Put into the main queue
         drainEventQueue.add(event);
     }
@@ -1106,6 +1093,14 @@ public class TApplication implements Runnable {
 
         // Peek at the mouse position
         if (event instanceof TMouseEvent) {
+            TMouseEvent mouse = (TMouseEvent) event;
+            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+                oldMouseX = mouseX;
+                oldMouseY = mouseY;
+                mouseX = mouse.getX();
+                mouseY = mouse.getY();
+            }
+
             // See if we need to switch focus to another window or the menu
             checkSwitchFocus((TMouseEvent) event);
         }
@@ -1241,6 +1236,17 @@ public class TApplication implements Runnable {
      * @see #primaryHandleEvent(TInputEvent event)
      */
     private void secondaryHandleEvent(final TInputEvent event) {
+        // Peek at the mouse position
+        if (event instanceof TMouseEvent) {
+            TMouseEvent mouse = (TMouseEvent) event;
+            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+                oldMouseX = mouseX;
+                oldMouseY = mouseY;
+                mouseX = mouse.getX();
+                mouseY = mouse.getY();
+            }
+        }
+
         secondaryEventReceiver.handleEvent(event);
     }
 
@@ -2087,6 +2093,53 @@ public class TApplication implements Runnable {
         }
     }
 
+    /**
+     * Get a (shallow) copy of the menu list.
+     *
+     * @return a copy of the menu list
+     */
+    public final List<TMenu> getAllMenus() {
+        return new LinkedList<TMenu>(menus);
+    }
+
+    /**
+     * Add a top-level menu to the list.
+     *
+     * @param menu the menu to add
+     * @throws IllegalArgumentException if the menu is already used in
+     * another TApplication
+     */
+    public final void addMenu(final TMenu menu) {
+        if ((menu.getApplication() != null)
+            && (menu.getApplication() != this)
+        ) {
+            throw new IllegalArgumentException("Menu " + menu + " is already " +
+                "part of application " + menu.getApplication());
+        }
+        closeMenu();
+        menus.add(menu);
+        recomputeMenuX();
+    }
+
+    /**
+     * Remove a top-level menu from the list.
+     *
+     * @param menu the menu to remove
+     * @throws IllegalArgumentException if the menu is already used in
+     * another TApplication
+     */
+    public final void removeMenu(final TMenu menu) {
+        if ((menu.getApplication() != null)
+            && (menu.getApplication() != this)
+        ) {
+            throw new IllegalArgumentException("Menu " + menu + " is already " +
+                "part of application " + menu.getApplication());
+        }
+        closeMenu();
+        menus.remove(menu);
+        recomputeMenuX();
+    }
+
     /**
      * Turn off a sub-menu.
      */
index 6fed1fc818f05356f02e34b88dc7f9bd429e20e9..361ed83b9be4bc8355c53f9ad4318746e78363b8 100644 (file)
@@ -31,6 +31,7 @@ package jexer;
 import jexer.bits.CellAttributes;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
 import jexer.teditor.Document;
 import jexer.teditor.Line;
 import jexer.teditor.Word;
@@ -47,6 +48,21 @@ public final class TEditorWidget extends TWidget {
      */
     private Document document;
 
+    /**
+     * The default color for the TEditor class.
+     */
+    private CellAttributes defaultColor = null;
+
+    /**
+     * The topmost line number in the visible area.  0-based.
+     */
+    private int topLine = 0;
+
+    /**
+     * The leftmost column number in the visible area.  0-based.
+     */
+    private int leftColumn = 0;
+
     /**
      * Public constructor.
      *
@@ -64,7 +80,9 @@ public final class TEditorWidget extends TWidget {
         super(parent, x, y, width, height);
 
         setCursorVisible(true);
-        document = new Document(text);
+
+        defaultColor = getTheme().getColor("teditor");
+        document = new Document(text, defaultColor);
     }
 
     /**
@@ -72,23 +90,21 @@ public final class TEditorWidget extends TWidget {
      */
     @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);
+            getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
 
             // Now draw document's line
-            if (lineNumber + i < document.getLineCount()) {
-                Line line = document.getLine(lineNumber + i);
+            if (topLine + i < document.getLineCount()) {
+                Line line = document.getLine(topLine + i);
                 int x = 0;
                 for (Word word: line.getWords()) {
-                    getScreen().putStringXY(x, i, word.getText(),
+                    // For now, we are cheating: draw outside the left region
+                    // if needed and let screen do the clipping.
+                    getScreen().putStringXY(x - leftColumn, i, word.getText(),
                         word.getColor());
                     x += word.getDisplayLength();
-                    if (x > getWidth()) {
+                    if (x - leftColumn > getWidth()) {
                         break;
                     }
                 }
@@ -105,20 +121,93 @@ public final class TEditorWidget extends TWidget {
     @Override
     public void onMouseDown(final TMouseEvent mouse) {
         if (mouse.isMouseWheelUp()) {
-            document.up();
+            if (getCursorY() == getHeight() - 1) {
+                if (document.up()) {
+                    if (topLine > 0) {
+                        topLine--;
+                    }
+                    alignCursor();
+                }
+            } else {
+                if (topLine > 0) {
+                    topLine--;
+                    setCursorY(getCursorY() + 1);
+                }
+            }
             return;
         }
         if (mouse.isMouseWheelDown()) {
-            document.down();
+            if (getCursorY() == 0) {
+                if (document.down()) {
+                    if (topLine < document.getLineNumber()) {
+                        topLine++;
+                    }
+                    alignCursor();
+                }
+            } else {
+                if (topLine < document.getLineCount() - getHeight()) {
+                    topLine++;
+                    setCursorY(getCursorY() - 1);
+                }
+            }
             return;
         }
 
-        // TODO: click sets row and column
+        if (mouse.isMouse1()) {
+            // Set the row and column
+            int newLine = topLine + mouse.getY();
+            int newX = leftColumn + mouse.getX();
+            if (newLine > document.getLineCount()) {
+                // Go to the end
+                document.setLineNumber(document.getLineCount() - 1);
+                document.end();
+                if (document.getLineCount() > getHeight()) {
+                    setCursorY(getHeight() - 1);
+                } else {
+                    setCursorY(document.getLineCount() - 1);
+                }
+                alignCursor();
+                return;
+            }
+
+            document.setLineNumber(newLine);
+            setCursorY(mouse.getY());
+            if (newX > document.getCurrentLine().getDisplayLength()) {
+                document.end();
+                alignCursor();
+            } else {
+                setCursorX(mouse.getX());
+            }
+            return;
+        }
 
         // Pass to children
         super.onMouseDown(mouse);
     }
 
+    /**
+     * Align visible cursor with document cursor.
+     */
+    private void alignCursor() {
+        int width = getWidth();
+
+        int desiredX = document.getCursor() - leftColumn;
+        if (desiredX < 0) {
+            // We need to push the screen to the left.
+            leftColumn = document.getCursor();
+        } else if (desiredX > width - 1) {
+            // We need to push the screen to the right.
+            leftColumn = document.getCursor() - (width - 1);
+        }
+
+        /*
+        System.err.println("document cursor " + document.getCursor() +
+            " leftColumn " + leftColumn);
+         */
+
+        setCursorX(document.getCursor() - leftColumn);
+    }
+
     /**
      * Handle keystrokes.
      *
@@ -127,33 +216,104 @@ public final class TEditorWidget extends TWidget {
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
         if (keypress.equals(kbLeft)) {
-            document.left();
+            if (document.left()) {
+                alignCursor();
+            }
         } else if (keypress.equals(kbRight)) {
-            document.right();
+            if (document.right()) {
+                alignCursor();
+            }
         } else if (keypress.equals(kbUp)) {
-            document.up();
+            if (document.up()) {
+                if (getCursorY() > 0) {
+                    setCursorY(getCursorY() - 1);
+                } else {
+                    if (topLine > 0) {
+                        topLine--;
+                    }
+                }
+                alignCursor();
+            }
         } else if (keypress.equals(kbDown)) {
-            document.down();
+            if (document.down()) {
+                if (getCursorY() < getHeight() - 1) {
+                    setCursorY(getCursorY() + 1);
+                } else {
+                    if (topLine < document.getLineCount() - getHeight()) {
+                        topLine++;
+                    }
+                }
+                alignCursor();
+            }
         } else if (keypress.equals(kbPgUp)) {
-            document.up(getHeight() - 1);
+            for (int i = 0; i < getHeight() - 1; i++) {
+                if (document.up()) {
+                    if (getCursorY() > 0) {
+                        setCursorY(getCursorY() - 1);
+                    } else {
+                        if (topLine > 0) {
+                            topLine--;
+                        }
+                    }
+                    alignCursor();
+                } else {
+                    break;
+                }
+            }
         } else if (keypress.equals(kbPgDn)) {
-            document.down(getHeight() - 1);
+            for (int i = 0; i < getHeight() - 1; i++) {
+                if (document.down()) {
+                    if (getCursorY() < getHeight() - 1) {
+                        setCursorY(getCursorY() + 1);
+                    } else {
+                        if (topLine < document.getLineCount() - getHeight()) {
+                            topLine++;
+                        }
+                    }
+                    alignCursor();
+                } else {
+                    break;
+                }
+            }
         } else if (keypress.equals(kbHome)) {
-            document.home();
+            if (document.home()) {
+                leftColumn = 0;
+                if (leftColumn < 0) {
+                    leftColumn = 0;
+                }
+                setCursorX(0);
+            }
         } else if (keypress.equals(kbEnd)) {
-            document.end();
+            if (document.end()) {
+                alignCursor();
+            }
         } else if (keypress.equals(kbCtrlHome)) {
             document.setLineNumber(0);
             document.home();
+            topLine = 0;
+            leftColumn = 0;
+            setCursorX(0);
+            setCursorY(0);
         } else if (keypress.equals(kbCtrlEnd)) {
             document.setLineNumber(document.getLineCount() - 1);
             document.end();
+            topLine = document.getLineCount() - getHeight();
+            if (topLine < 0) {
+                topLine = 0;
+            }
+            if (document.getLineCount() > getHeight()) {
+                setCursorY(getHeight() - 1);
+            } else {
+                setCursorY(document.getLineCount() - 1);
+            }
+            alignCursor();
         } else if (keypress.equals(kbIns)) {
             document.setOverwrite(!document.getOverwrite());
         } else if (keypress.equals(kbDel)) {
             document.del();
         } else if (keypress.equals(kbBackspace)) {
             document.backspace();
+            alignCursor();
         } else if (!keypress.getKey().isFnKey()
             && !keypress.getKey().isAlt()
             && !keypress.getKey().isCtrl()
@@ -166,4 +326,32 @@ public final class TEditorWidget extends TWidget {
         }
     }
 
+    /**
+     * Method that subclasses can override to handle window/screen resize
+     * events.
+     *
+     * @param resize resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent resize) {
+        // Change my width/height, and pull the cursor in as needed.
+        if (resize.getType() == TResizeEvent.Type.WIDGET) {
+            setWidth(resize.getWidth());
+            setHeight(resize.getHeight());
+            // See if the cursor is now outside the window, and if so move
+            // things.
+            if (getCursorX() >= getWidth()) {
+                leftColumn += getCursorX() - (getWidth() - 1);
+                setCursorX(getWidth() - 1);
+            }
+            if (getCursorY() >= getHeight()) {
+                topLine += getCursorY() - (getHeight() - 1);
+                setCursorY(getHeight() - 1);
+            }
+        } else {
+            // Let superclass handle it
+            super.onResize(resize);
+        }
+    }
+
 }
index 6e902195fa59d40a8306fc1f27a713440992c364..b7a16249d49dda32af22bf5f053c8c02e6f99a3e 100644 (file)
@@ -657,6 +657,16 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    /**
+     * Reset the blink timer.
+     */
+    private void resetBlinkTimer() {
+        // See if it is time to flip the blink time.
+        long nowTime = (new Date()).getTime();
+        lastBlinkTime = nowTime;
+        cursorBlinkVisible = true;
+    }
+
     /**
      * Paint redraws the whole screen.
      *
@@ -1539,6 +1549,7 @@ public final class SwingTerminal extends LogicalScreen
         // Save it and we are done.
         synchronized (eventQueue) {
             eventQueue.add(new TKeypressEvent(keypress));
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1577,6 +1588,7 @@ public final class SwingTerminal extends LogicalScreen
         // Drop a cmAbort and walk away
         synchronized (eventQueue) {
             eventQueue.add(new TCommandEvent(cmAbort));
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1667,6 +1679,7 @@ public final class SwingTerminal extends LogicalScreen
             TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
                 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
             eventQueue.add(windowResize);
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1705,6 +1718,7 @@ public final class SwingTerminal extends LogicalScreen
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1733,6 +1747,7 @@ public final class SwingTerminal extends LogicalScreen
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1798,6 +1813,7 @@ public final class SwingTerminal extends LogicalScreen
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1845,6 +1861,7 @@ public final class SwingTerminal extends LogicalScreen
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1891,6 +1908,7 @@ public final class SwingTerminal extends LogicalScreen
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
+            resetBlinkTimer();
         }
         if (listener != null) {
             synchronized (listener) {
index 8a67d1834b7921f6129ef89c3dbb406f8eeca103..9618ccc44f085bac6a36bdbf6d964a513a59027d 100644 (file)
@@ -284,7 +284,7 @@ public final class ColorTheme {
         // TText text
         color = new CellAttributes();
         color.setForeColor(Color.WHITE);
-        color.setBackColor(Color.BLACK);
+        color.setBackColor(Color.BLUE);
         color.setBold(false);
         colors.put("ttext", color);
 
@@ -457,7 +457,7 @@ public final class ColorTheme {
         // TEditor
         color = new CellAttributes();
         color.setForeColor(Color.WHITE);
-        color.setBackColor(Color.BLACK);
+        color.setBackColor(Color.BLUE);
         color.setBold(false);
         colors.put("teditor", color);
 
index 5639ed7c12f00be2c2097a66adb2906ee505f4f1..f04916a1df1a933f7ab0f495dfbaae22a631f674 100644 (file)
@@ -80,7 +80,23 @@ public class DemoEditorWindow extends TWindow {
 "on many more platforms.\n" +
 "\n" +
 "This library is licensed MIT.  See the file LICENSE for the full license\n" +
-"for the details.\n");
+"for the details.\n" +
+"\n" +
+"package jexer.demos;\n" +
+"\n" +
+"import jexer.*;\n" +
+"import jexer.event.*;\n" +
+"import static jexer.TCommand.*;\n" +
+"import static jexer.TKeypress.*;\n" +
+"\n" +
+"/**\n" +
+" * This window demonstates the TText, THScroller, and TVScroller widgets.\n" +
+" */\n" +
+"public class DemoEditorWindow extends TWindow {\n" +
+"\n" +
+"1 2 3 123\n" +
+"\n"
+        );
 
     }
 
index 53f30d7d5a630efa202f202fa8c1809ce7176e89..598ac7f0ce92f889d56a78a6b42d7f41675dad02 100644 (file)
@@ -77,7 +77,7 @@ public class DemoMainWindow extends TWindow {
     private DemoMainWindow(final TApplication parent, final int flags) {
         // Construct a demo window.  X and Y don't matter because it will be
         // centered on screen.
-        super(parent, "Demo Window", 0, 0, 60, 22, flags);
+        super(parent, "Demo Window", 0, 0, 60, 23, flags);
 
         int row = 1;
 
index cae6f47106db0ae9a3656dd1d318f7916771be12..5b7050fe22efd3963ca762256e163ba30c39a87b 100644 (file)
@@ -31,6 +31,8 @@ package jexer.teditor;
 import java.util.ArrayList;
 import java.util.List;
 
+import jexer.bits.CellAttributes;
+
 /**
  * A Document represents a text file, as a collection of lines.
  */
@@ -52,6 +54,16 @@ public class Document {
      */
     private boolean overwrite = false;
 
+    /**
+     * The default color for the TEditor class.
+     */
+    private CellAttributes defaultColor = null;
+
+    /**
+     * The text highlighter to use.
+     */
+    private Highlighter highlighter = new Highlighter();
+
     /**
      * Get the overwrite flag.
      *
@@ -81,6 +93,15 @@ public class Document {
         return lineNumber;
     }
 
+    /**
+     * Get the current editing line.
+     *
+     * @return the line
+     */
+    public Line getCurrentLine() {
+        return lines.get(lineNumber);
+    }
+
     /**
      * Get a specific line by number.
      *
@@ -100,19 +121,56 @@ public class Document {
      */
     public void setLineNumber(final int n) {
         if ((n < 0) || (n > lines.size())) {
-            throw new IndexOutOfBoundsException("Line size is " + lines.size() +
-                ", requested index " + n);
+            throw new IndexOutOfBoundsException("Lines array size is " +
+                lines.size() + ", requested index " + n);
         }
         lineNumber = n;
     }
 
+    /**
+     * Get the current cursor position of the editing line.
+     *
+     * @return the cursor position
+     */
+    public int getCursor() {
+        return lines.get(lineNumber).getCursor();
+    }
+
+    /**
+     * Construct a new Document from an existing text string.
+     *
+     * @param str the text string
+     * @param defaultColor the color for unhighlighted text
+     */
+    public Document(final String str, final CellAttributes defaultColor) {
+        this.defaultColor = defaultColor;
+
+        // TODO: set different colors based on file extension
+        highlighter.setJavaColors();
+
+        String [] rawLines = str.split("\n");
+        for (int i = 0; i < rawLines.length; i++) {
+            lines.add(new Line(rawLines[i], this.defaultColor, highlighter));
+        }
+    }
+
     /**
      * Increment the line number by one.  If at the last line, do nothing.
+     *
+     * @return true if the editing line changed
      */
-    public void down() {
+    public boolean down() {
         if (lineNumber < lines.size() - 1) {
+            int x = lines.get(lineNumber).getCursor();
             lineNumber++;
+            if (x > lines.get(lineNumber).getDisplayLength()) {
+                lines.get(lineNumber).end();
+            } else {
+                lines.get(lineNumber).setCursor(x);
+            }
+            return true;
         }
+        return false;
     }
 
     /**
@@ -120,21 +178,42 @@ public class Document {
      * increment only to the last line.
      *
      * @param n the number of lines to increment by
+     * @return true if the editing line changed
      */
-    public void down(final int n) {
-        lineNumber += n;
-        if (lineNumber > lines.size() - 1) {
-            lineNumber = lines.size() - 1;
+    public boolean down(final int n) {
+        if (lineNumber < lines.size() - 1) {
+            int x = lines.get(lineNumber).getCursor();
+            lineNumber += n;
+            if (lineNumber > lines.size() - 1) {
+                lineNumber = lines.size() - 1;
+            }
+            if (x > lines.get(lineNumber).getDisplayLength()) {
+                lines.get(lineNumber).end();
+            } else {
+                lines.get(lineNumber).setCursor(x);
+            }
+            return true;
         }
+        return false;
     }
 
     /**
      * Decrement the line number by one.  If at the first line, do nothing.
+     *
+     * @return true if the editing line changed
      */
-    public void up() {
+    public boolean up() {
         if (lineNumber > 0) {
+            int x = lines.get(lineNumber).getCursor();
             lineNumber--;
+            if (x > lines.get(lineNumber).getDisplayLength()) {
+                lines.get(lineNumber).end();
+            } else {
+                lines.get(lineNumber).setCursor(x);
+            }
+            return true;
         }
+        return false;
     }
 
     /**
@@ -142,40 +221,59 @@ public class Document {
      * decrement only to the first line.
      *
      * @param n the number of lines to decrement by
+     * @return true if the editing line changed
      */
-    public void up(final int n) {
-        lineNumber -= n;
-        if (lineNumber < 0) {
-            lineNumber = 0;
+    public boolean up(final int n) {
+        if (lineNumber > 0) {
+            int x = lines.get(lineNumber).getCursor();
+            lineNumber -= n;
+            if (lineNumber < 0) {
+                lineNumber = 0;
+            }
+            if (x > lines.get(lineNumber).getDisplayLength()) {
+                lines.get(lineNumber).end();
+            } else {
+                lines.get(lineNumber).setCursor(x);
+            }
+            return true;
         }
+        return false;
     }
 
     /**
      * Decrement the cursor by one.  If at the first column, do nothing.
+     *
+     * @return true if the cursor position changed
      */
-    public void left() {
-        lines.get(lineNumber).left();
+    public boolean left() {
+        return lines.get(lineNumber).left();
     }
 
     /**
      * Increment the cursor by one.  If at the last column, do nothing.
+     *
+     * @return true if the cursor position changed
      */
-    public void right() {
-        lines.get(lineNumber).right();
+    public boolean right() {
+        return lines.get(lineNumber).right();
     }
 
     /**
      * Go to the first column of this line.
+     *
+     * @return true if the cursor position changed
      */
-    public void home() {
-        lines.get(lineNumber).home();
+    public boolean home() {
+        return lines.get(lineNumber).home();
     }
 
     /**
      * Go to the last column of this line.
+     *
+     * @return true if the cursor position changed
      */
-    public void end() {
-        lines.get(lineNumber).end();
+    public boolean end() {
+        return lines.get(lineNumber).end();
     }
 
     /**
@@ -199,7 +297,11 @@ public class Document {
      * @param ch the character to replace or insert
      */
     public void addChar(final char ch) {
-        lines.get(lineNumber).addChar(ch);
+        if (overwrite) {
+            lines.get(lineNumber).replaceChar(ch);
+        } else {
+            lines.get(lineNumber).addChar(ch);
+        }
     }
 
     /**
@@ -235,16 +337,4 @@ public class Document {
         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/Highlighter.java b/src/jexer/teditor/Highlighter.java
new file mode 100644 (file)
index 0000000..9576ad1
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * 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.SortedMap;
+import java.util.TreeMap;
+
+import jexer.bits.CellAttributes;
+import jexer.bits.Color;
+
+/**
+ * Highlighter provides color choices for certain text strings.
+ */
+public class Highlighter {
+
+    /**
+     * The highlighter colors.
+     */
+    private SortedMap<String, CellAttributes> colors;
+
+    /**
+     * Public constructor sets the theme to the default.
+     */
+    public Highlighter() {
+        colors = new TreeMap<String, CellAttributes>();
+    }
+
+    /**
+     * See if this is a character that should split a word.
+     *
+     * @param ch the character
+     * @return true if the word should be split
+     */
+    public boolean shouldSplit(final char ch) {
+        // For now, split on punctuation
+        String punctuation = "'\"\\<>{}[]!@#$%^&*();:.,-+/*?";
+        if (punctuation.indexOf(ch) != -1) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Retrieve the CellAttributes for a named theme color.
+     *
+     * @param name theme color name, e.g. "twindow.border"
+     * @return color associated with name, e.g. bold yellow on blue
+     */
+    public CellAttributes getColor(final String name) {
+        CellAttributes attr = (CellAttributes) colors.get(name);
+        return attr;
+    }
+
+    /**
+     * Sets to defaults that resemble the Borland IDE colors.
+     */
+    public void setJavaColors() {
+        CellAttributes color;
+
+        String [] keywords = {
+            "boolean", "byte", "short", "int", "long", "char", "float",
+            "double", "void", "new",
+            "static", "final", "volatile", "synchronized", "abstract",
+            "public", "private", "protected",
+            "class", "interface", "extends", "implements",
+            "if", "else", "do", "while", "for", "break", "continue",
+            "switch", "case", "default",
+        };
+        color = new CellAttributes();
+        color.setForeColor(Color.WHITE);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        for (String str: keywords) {
+            colors.put(str, color);
+        }
+
+        String [] operators = {
+            "[", "]", "(", ")", "{", "}",
+            "*", "-", "+", "/", "=", "%",
+            "^", "&", "!", "<<", ">>", "<<<", ">>>",
+            "&&", "||",
+            ">", "<", ">=", "<=", "!=", "==",
+            ",", ";", ".", "?", ":",
+        };
+        color = new CellAttributes();
+        color.setForeColor(Color.CYAN);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        for (String str: operators) {
+            colors.put(str, color);
+        }
+
+        String [] packageKeywords = {
+            "package", "import",
+        };
+        color = new CellAttributes();
+        color.setForeColor(Color.GREEN);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        for (String str: packageKeywords) {
+            colors.put(str, color);
+        }
+
+    }
+
+
+}
index b89d8277ed30b1ac6b1a500f9056839b9b87069c..de1265982c67e3190a122a1dae0e692ba7e2a17e 100644 (file)
@@ -31,6 +31,8 @@ package jexer.teditor;
 import java.util.ArrayList;
 import java.util.List;
 
+import jexer.bits.CellAttributes;
+
 /**
  * A Line represents a single line of text on the screen, as a collection of
  * words.
@@ -42,10 +44,20 @@ public class Line {
      */
     private ArrayList<Word> words = new ArrayList<Word>();
 
+    /**
+     * The default color for the TEditor class.
+     */
+    private CellAttributes defaultColor = null;
+
+    /**
+     * The text highlighter to use.
+     */
+    private Highlighter highlighter = null;
+
     /**
      * The current cursor position on this line.
      */
-    private int cursorX;
+    private int cursor = 0;
 
     /**
      * The current word that the cursor position is in.
@@ -57,6 +69,32 @@ public class Line {
      */
     private int displayLength = -1;
 
+    /**
+     * Get the current cursor position.
+     *
+     * @return the cursor position
+     */
+    public int getCursor() {
+        return cursor;
+    }
+
+    /**
+     * Set the current cursor position.
+     *
+     * @param cursor the new cursor position
+     */
+    public void setCursor(final int cursor) {
+        if ((cursor < 0)
+            || ((cursor >= getDisplayLength())
+                && (getDisplayLength() > 0))
+        ) {
+            throw new IndexOutOfBoundsException("Max length is " +
+                getDisplayLength() + ", requested position " + cursor);
+        }
+        this.cursor = cursor;
+        // TODO: set word
+    }
+
     /**
      * Get a (shallow) copy of the list of words.
      *
@@ -80,16 +118,30 @@ public class Line {
             n += word.getDisplayLength();
         }
         displayLength = n;
+
+        // If we have any visible characters, add one to the display so that
+        // the cursor is immediately after the data.
+        if (displayLength > 0) {
+            displayLength++;
+        }
         return displayLength;
     }
 
     /**
-     * Construct a new Line from an existing text string.
+     * Construct a new Line from an existing text string, and highlight
+     * certain strings.
      *
      * @param str the text string
+     * @param defaultColor the color for unhighlighted text
+     * @param highlighter the highlighter to use
      */
-    public Line(final String str) {
-        currentWord = new Word();
+    public Line(final String str, final CellAttributes defaultColor,
+        final Highlighter highlighter) {
+
+        this.defaultColor = defaultColor;
+        this.highlighter = highlighter;
+
+        currentWord = new Word(this.defaultColor, this.highlighter);
         words.add(currentWord);
         for (int i = 0; i < str.length(); i++) {
             char ch = str.charAt(i);
@@ -99,40 +151,81 @@ public class Line {
                 currentWord = newWord;
             }
         }
+        for (Word word: words) {
+            word.applyHighlight();
+        }
+    }
+
+    /**
+     * Construct a new Line from an existing text string.
+     *
+     * @param str the text string
+     * @param defaultColor the color for unhighlighted text
+     */
+    public Line(final String str, final CellAttributes defaultColor) {
+        this(str, defaultColor, null);
     }
 
     /**
      * Decrement the cursor by one.  If at the first column, do nothing.
+     *
+     * @return true if the cursor position changed
      */
-    public void left() {
-        if (cursorX == 0) {
-            return;
+    public boolean left() {
+        if (cursor == 0) {
+            return false;
         }
-        // TODO
+        // TODO: switch word
+        cursor--;
+        return true;
     }
 
     /**
      * Increment the cursor by one.  If at the last column, do nothing.
+     *
+     * @return true if the cursor position changed
      */
-    public void right() {
-        if (cursorX == getDisplayLength() - 1) {
-            return;
+    public boolean right() {
+        if (getDisplayLength() == 0) {
+            return false;
         }
-        // TODO
+        if (cursor == getDisplayLength() - 1) {
+            return false;
+        }
+        // TODO: switch word
+        cursor++;
+        return true;
     }
 
     /**
      * Go to the first column of this line.
+     *
+     * @return true if the cursor position changed
      */
-    public void home() {
-        // TODO
+    public boolean home() {
+        if (cursor > 0) {
+            cursor = 0;
+            currentWord = words.get(0);
+            return true;
+        }
+        return false;
     }
 
     /**
      * Go to the last column of this line.
+     *
+     * @return true if the cursor position changed
      */
-    public void end() {
-        // TODO
+    public boolean end() {
+        if (cursor != getDisplayLength() - 1) {
+            cursor = getDisplayLength() - 1;
+            if (cursor < 0) {
+                cursor = 0;
+            }
+            currentWord = words.get(words.size() - 1);
+            return true;
+        }
+        return false;
     }
 
     /**
@@ -150,13 +243,21 @@ public class Line {
     }
 
     /**
-     * Replace or insert a character at the cursor, depending on overwrite
-     * flag.
+     * Insert a character at the cursor.
      *
-     * @param ch the character to replace or insert
+     * @param ch the character to insert
      */
     public void addChar(final char ch) {
         // TODO
     }
 
+    /**
+     * Replace a character at the cursor.
+     *
+     * @param ch the character to replace
+     */
+    public void replaceChar(final char ch) {
+        // TODO
+    }
+
 }
index d7a65760e628e9295e7932d6b0b38b690a1258bb..d9b3417c8eda196f222eea813e91f6158f778ebd 100644 (file)
@@ -33,6 +33,10 @@ import jexer.bits.CellAttributes;
 /**
  * A Word represents text that was entered by the user.  It can be either
  * whitespace or non-whitespace.
+ *
+ * Very dumb highlighting is supported, it has no sense of parsing (not even
+ * comments).  For now this only highlights some Java keywords and
+ * puctuation.
  */
 public class Word {
 
@@ -41,6 +45,16 @@ public class Word {
      */
     private CellAttributes color = new CellAttributes();
 
+    /**
+     * The default color for the TEditor class.
+     */
+    private CellAttributes defaultColor = null;
+
+    /**
+     * The text highlighter to use.
+     */
+    private Highlighter highlighter = null;
+
     /**
      * The actual text of this word.  Average word length is 6 characters,
      * with a lot of shorter ones, so start with 3.
@@ -108,15 +122,44 @@ public class Word {
      * Construct a word with one character.
      *
      * @param ch the first character of the word
+     * @param defaultColor the color for unhighlighted text
+     * @param highlighter the highlighter to use
      */
-    public Word(final char ch) {
+    public Word(final char ch, final CellAttributes defaultColor,
+        final Highlighter highlighter) {
+
+        this.defaultColor = defaultColor;
+        this.highlighter = highlighter;
         text.append(ch);
     }
 
     /**
      * Construct a word with an empty string.
+     *
+     * @param defaultColor the color for unhighlighted text
+     * @param highlighter the highlighter to use
+     */
+    public Word(final CellAttributes defaultColor,
+        final Highlighter highlighter) {
+
+        this.defaultColor = defaultColor;
+        this.highlighter = highlighter;
+    }
+
+    /**
+     * Perform highlighting.
      */
-    public Word() {}
+    public void applyHighlight() {
+        color.setTo(defaultColor);
+        if (highlighter == null) {
+            return;
+        }
+        String key = text.toString();
+        CellAttributes newColor = highlighter.getColor(key);
+        if (newColor != null) {
+            color.setTo(newColor);
+        }
+    }
 
     /**
      * Add a character to this word.  If this is a whitespace character
@@ -133,21 +176,36 @@ public class Word {
             text.append(ch);
             return this;
         }
+
+        // Give the highlighter the option to split here.
+        if (highlighter != null) {
+            if (highlighter.shouldSplit(ch)
+                || highlighter.shouldSplit(text.charAt(0))
+            ) {
+                Word newWord = new Word(ch, defaultColor, highlighter);
+                return newWord;
+            }
+        }
+
+        // Highlighter didn't care, so split at whitespace.
         if (Character.isWhitespace(text.charAt(0))
             && Character.isWhitespace(ch)
         ) {
+            // Adding to a whitespace word, keep at it.
             text.append(ch);
             return this;
         }
         if (!Character.isWhitespace(text.charAt(0))
             && !Character.isWhitespace(ch)
         ) {
+            // Adding to a non-whitespace word, keep at it.
             text.append(ch);
             return this;
         }
 
-        // We will be splitting here.
-        Word newWord = new Word(ch);
+        // Switching from whitespace to non-whitespace or vice versa, so
+        // split here.
+        Word newWord = new Word(ch, defaultColor, highlighter);
         return newWord;
     }