retrofit
authorKevin Lamonte <kevin.lamonte@gmail.com>
Fri, 29 Nov 2019 18:54:52 +0000 (12:54 -0600)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Fri, 29 Nov 2019 18:54:52 +0000 (12:54 -0600)
12 files changed:
README.md
src/jexer/TApplication.java
src/jexer/TEditorWidget.java
src/jexer/TEditorWindow.java
src/jexer/backend/SwingComponent.java
src/jexer/backend/SwingTerminal.java
src/jexer/menu/TMenu.java
src/jexer/menu/TMenu.properties
src/jexer/teditor/Document.java
src/jexer/teditor/Highlighter.java
src/jexer/teditor/Line.java
src/jexer/teditor/Word.java

index adc81a97f5443cf7e3ce58de5273b6a2f3edc60a..4e24d8cf54916d06933bf68878c9e941ccf218ce 100644 (file)
--- a/README.md
+++ b/README.md
@@ -181,7 +181,7 @@ The table below lists terminals tested against Jexer's Xterm backend:
 
 5 - Sixel images can crash terminal.
 
-6 - Version 0.4.2382.0, on Windows 10.0.18362.30.  Tested against
+6 - Version 0.7.3291.0, on Windows 10.0.18362.30.  Tested against
     WSL-1 Debian instance.
 
 
index e2c3cd685f3cfd41674a49da97565b8e94b309d7..28e35091ded6e1ef006190574e945c0426c41057 100644 (file)
@@ -808,13 +808,26 @@ public class TApplication implements Runnable {
         }
 
         // Load the help system
-        try {
-            ClassLoader loader = Thread.currentThread().getContextClassLoader();
-            helpFile = new HelpFile();
-            helpFile.load(loader.getResourceAsStream("help.xml"));
-        } catch (Exception e) {
-            new TExceptionDialog(this, e);
-        }
+        invokeLater(new Runnable() {
+            /*
+             * This isn't the best solution.  But basically if a TApplication
+             * subclass constructor throws and needs to use TExceptionDialog,
+             * it may end up at the bottom of the window stack with a bunch
+             * of modal windows on top of it if said constructors spawn their
+             * windows also via invokeLater().  But if they don't do that,
+             * and instead just conventionally construct their windows, then
+             * this exception dialog will end up on top where it should be.
+             */
+            public void run() {
+                try {
+                    ClassLoader loader = Thread.currentThread().getContextClassLoader();
+                    helpFile = new HelpFile();
+                    helpFile.load(loader.getResourceAsStream("help.xml"));
+                } catch (Exception e) {
+                    new TExceptionDialog(TApplication.this, e);
+                }
+            }
+        });
     }
 
     // ------------------------------------------------------------------------
@@ -3417,6 +3430,9 @@ public class TApplication implements Runnable {
      */
     public final TMenu addEditMenu() {
         TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
+        editMenu.addDefaultItem(TMenu.MID_UNDO, false);
+        editMenu.addDefaultItem(TMenu.MID_REDO, false);
+        editMenu.addSeparator();
         editMenu.addDefaultItem(TMenu.MID_CUT, false);
         editMenu.addDefaultItem(TMenu.MID_COPY, false);
         editMenu.addDefaultItem(TMenu.MID_PASTE, false);
index 6ff39e61308b04b2a9171e8e4aaa7517314a8ed3..bea25eda3e74c9c0e2c60c012dfd4e976d1198e4 100644 (file)
@@ -29,6 +29,8 @@
 package jexer;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 import jexer.bits.CellAttributes;
 import jexer.bits.StringUtils;
@@ -64,10 +66,10 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
     /**
      * The document being edited.
      */
-    private Document document;
+    protected Document document;
 
     /**
-     * The default color for the TEditor class.
+     * The default color for the editable text.
      */
     private CellAttributes defaultColor = null;
 
@@ -106,6 +108,42 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
      */
     private int selectionLine1;
 
+    /**
+     * The list of undo/redo states.
+     */
+    private List<SavedState> undoList = new ArrayList<SavedState>();
+
+    /**
+     * The position in undoList for undo/redo.
+     */
+    private int undoListI = 0;
+
+    /**
+     * The maximum size of the undo list.
+     */
+    private int undoLevel = 50;
+
+    /**
+     * The saved state for an undo/redo operation.
+     */
+    private class SavedState {
+        /**
+         * The Document state.
+         */
+        public Document document;
+
+        /**
+         * The topmost line number in the visible area.  0-based.
+         */
+        public int topLine = 0;
+
+        /**
+         * The leftmost column number in the visible area.  0-based.
+         */
+        public int leftColumn = 0;
+
+    }
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -291,13 +329,23 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
         if (keypress.getKey().isShift()) {
-            // Selection.
-            if (!inSelection) {
-                inSelection = true;
-                selectionColumn0 = document.getCursor();
-                selectionLine0 = document.getLineNumber();
-                selectionColumn1 = selectionColumn0;
-                selectionLine1 = selectionLine0;
+            if (keypress.equals(kbShiftLeft)
+                || keypress.equals(kbShiftRight)
+                || keypress.equals(kbShiftUp)
+                || keypress.equals(kbShiftDown)
+                || keypress.equals(kbShiftPgDn)
+                || keypress.equals(kbShiftPgUp)
+                || keypress.equals(kbShiftHome)
+                || keypress.equals(kbShiftEnd)
+            ) {
+                // Shifted navigation keys enable selection
+                if (!inSelection) {
+                    inSelection = true;
+                    selectionColumn0 = document.getCursor();
+                    selectionLine0 = document.getLineNumber();
+                    selectionColumn1 = selectionColumn0;
+                    selectionLine1 = selectionLine0;
+                }
             }
         } else {
             if (keypress.equals(kbLeft)
@@ -396,32 +444,40 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
             document.end();
             alignTopLine(false);
         } else if (keypress.equals(kbIns)) {
-            document.setOverwrite(!document.getOverwrite());
+            document.setOverwrite(!document.isOverwrite());
         } else if (keypress.equals(kbDel)) {
             if (inSelection) {
                 deleteSelection();
+                alignCursor();
             } else {
+                saveUndo();
                 document.del();
+                alignCursor();
             }
-            alignCursor();
         } else if (keypress.equals(kbBackspace)
             || keypress.equals(kbBackspaceDel)
         ) {
             if (inSelection) {
                 deleteSelection();
+                alignTopLine(false);
             } else {
+                saveUndo();
                 document.backspace();
+                alignTopLine(false);
             }
-            alignTopLine(false);
         } else if (keypress.equals(kbTab)) {
             deleteSelection();
-            // Add spaces until we hit modulo 8.
-            for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
-                document.addChar(' ');
-            }
+            saveUndo();
+            document.tab();
+            alignCursor();
+        } else if (keypress.equals(kbShiftTab)) {
+            deleteSelection();
+            saveUndo();
+            document.backTab();
             alignCursor();
         } else if (keypress.equals(kbEnter)) {
             deleteSelection();
+            saveUndo();
             document.enter();
             alignTopLine(true);
         } else if (!keypress.getKey().isFnKey()
@@ -430,6 +486,7 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
         ) {
             // Plain old keystroke, process it
             deleteSelection();
+            saveUndo();
             document.addChar(keypress.getKey().getChar());
             alignCursor();
         } else {
@@ -499,8 +556,21 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
             if (text != null) {
                 for (int i = 0; i < text.length(); ) {
                     int ch = text.codePointAt(i);
-                    onKeypress(new TKeypressEvent(false, 0, ch, false, false,
-                            false));
+                    switch (ch) {
+                    case '\n':
+                        onKeypress(new TKeypressEvent(kbEnter));
+                        break;
+                    case '\t':
+                        onKeypress(new TKeypressEvent(kbTab));
+                        break;
+                    default:
+                        if ((ch >= 0x20) && (ch != 0x7F)) {
+                            onKeypress(new TKeypressEvent(false, 0, ch,
+                                    false, false, false));
+                        }
+                        break;
+                    }
+
                     i += Character.charCount(ch);
                 }
             }
@@ -602,6 +672,15 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
     // TEditorWidget ----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Set the undo level.
+     *
+     * @param undoLevel the maximum number of undo operations
+     */
+    public void setUndoLevel(final int undoLevel) {
+        this.undoLevel = undoLevel;
+    }
+
     /**
      * Align visible area with document current line.
      *
@@ -836,6 +915,15 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
         document.setNotDirty();
     }
 
+    /**
+     * Get the overwrite value.
+     *
+     * @return true if new text will overwrite old text
+     */
+    public boolean isOverwrite() {
+        return document.isOverwrite();
+    }
+
     /**
      * Save contents to file.
      *
@@ -853,6 +941,9 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
         if (!inSelection) {
             return;
         }
+
+        saveUndo();
+
         inSelection = false;
 
         int startCol = selectionColumn0;
@@ -1191,8 +1282,20 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
 
         for (int i = 0; i < text.length(); ) {
             int ch = text.codePointAt(i);
-            onKeypress(new TKeypressEvent(false, 0, ch, false, false,
-                    false));
+            switch (ch) {
+            case '\n':
+                onKeypress(new TKeypressEvent(kbEnter));
+                break;
+            case '\t':
+                onKeypress(new TKeypressEvent(kbTab));
+                break;
+            default:
+                if ((ch >= 0x20) && (ch != 0x7F)) {
+                    onKeypress(new TKeypressEvent(false, 0, ch,
+                            false, false, false));
+                }
+                break;
+            }
             i += Character.charCount(ch);
         }
     }
@@ -1267,4 +1370,72 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
         return true;
     }
 
+    /**
+     * Save undo state.
+     */
+    private void saveUndo() {
+        SavedState state = new SavedState();
+        state.document = document.dup();
+        state.topLine = topLine;
+        state.leftColumn = leftColumn;
+        if (undoLevel > 0) {
+            while (undoList.size() > undoLevel) {
+                undoList.remove(0);
+            }
+        }
+        undoList.add(state);
+        undoListI = undoList.size() - 1;
+    }
+
+    /**
+     * Undo an edit.
+     */
+    public void undo() {
+        inSelection = false;
+        if ((undoListI >= 0) && (undoListI < undoList.size())) {
+            SavedState state = undoList.get(undoListI);
+            document = state.document.dup();
+            topLine = state.topLine;
+            leftColumn = state.leftColumn;
+            undoListI--;
+            setCursorY(document.getLineNumber() - topLine);
+            alignCursor();
+        }
+    }
+
+    /**
+     * Redo an edit.
+     */
+    public void redo() {
+        inSelection = false;
+        if ((undoListI >= 0) && (undoListI < undoList.size())) {
+            SavedState state = undoList.get(undoListI);
+            document = state.document.dup();
+            topLine = state.topLine;
+            leftColumn = state.leftColumn;
+            undoListI++;
+            setCursorY(document.getLineNumber() - topLine);
+            alignCursor();
+        }
+    }
+
+    /**
+     * Trim trailing whitespace from lines and trailing empty
+     * lines from the document.
+     */
+    public void cleanWhitespace() {
+        document.cleanWhitespace();
+        setCursorY(document.getLineNumber() - topLine);
+        alignCursor();
+    }
+
+    /**
+     * Set keyword highlighting.
+     *
+     * @param enabled if true, enable keyword highlighting
+     */
+    public void setHighlighting(final boolean enabled) {
+        document.setHighlighting(enabled);
+    }
+
 }
index d78185c32f3096cd615f345d9731751d285fc3b6..a28376ba3a18e8fcdfd75a300a32c180c2946385 100644 (file)
@@ -44,8 +44,10 @@ import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
 import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
+import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
+import jexer.menu.TMenu;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -150,28 +152,27 @@ public class TEditorWindow extends TScrollableWindow {
     }
 
     // ------------------------------------------------------------------------
-    // TWindow ----------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
-     * Draw the window.
+     * Called by application.switchWindow() when this window gets the
+     * focus, and also by application.addWindow().
      */
-    @Override
-    public void draw() {
-        // Draw as normal.
-        super.draw();
-
-        // Add the row:col on the bottom row
-        CellAttributes borderColor = getBorder();
-        String location = String.format(" %d:%d ",
-            editField.getEditingRowNumber(),
-            editField.getEditingColumnNumber());
-        int colon = location.indexOf(':');
-        putStringXY(10 - colon, getHeight() - 1, location, borderColor);
+    public void onFocus() {
+        super.onFocus();
+        getApplication().enableMenuItem(TMenu.MID_UNDO);
+        getApplication().enableMenuItem(TMenu.MID_REDO);
+    }
 
-        if (editField.isDirty()) {
-            putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
-        }
+    /**
+     * Called by application.switchWindow() when another window gets the
+     * focus.
+     */
+    public void onUnfocus() {
+        super.onUnfocus();
+        getApplication().disableMenuItem(TMenu.MID_UNDO);
+        getApplication().disableMenuItem(TMenu.MID_REDO);
     }
 
     /**
@@ -351,6 +352,48 @@ public class TEditorWindow extends TScrollableWindow {
         super.onCommand(command);
     }
 
+    /**
+     * Handle posted menu events.
+     *
+     * @param menu menu event
+     */
+    @Override
+    public void onMenu(final TMenuEvent menu) {
+        switch (menu.getId()) {
+        case TMenu.MID_UNDO:
+            editField.undo();
+            break;
+        case TMenu.MID_REDO:
+            editField.redo();
+            break;
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the window.
+     */
+    @Override
+    public void draw() {
+        // Draw as normal.
+        super.draw();
+
+        // Add the row:col on the bottom row
+        CellAttributes borderColor = getBorder();
+        String location = String.format(" %d:%d ",
+            editField.getEditingRowNumber(),
+            editField.getEditingColumnNumber());
+        int colon = location.indexOf(':');
+        putStringXY(10 - colon, getHeight() - 1, location, borderColor);
+
+        if (editField.isDirty()) {
+            putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
+        }
+    }
+
     /**
      * Returns true if this window does not want the application-wide mouse
      * cursor drawn over it.
index 3d1074cf889070d6bc1c7eee4d9be1da80b92d06..df3633398b699585757cab519b1102214983a50c 100644 (file)
@@ -83,7 +83,7 @@ class SwingComponent {
      * Adjustable Insets for this component.  This has the effect of adding a
      * black border around the drawing area.
      */
-    Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
+    Insets adjustInsets = null;
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
@@ -96,6 +96,16 @@ class SwingComponent {
      */
     public SwingComponent(final JFrame frame) {
         this.frame = frame;
+        if (System.getProperty("os.name").startsWith("Linux")) {
+            // On my Linux dev system, a Swing frame draws its contents just
+            // a little off.  No idea why, but I've seen it on both Debian
+            // and Fedora with KDE.  These adjustments to the adjustments
+            // seem to center it OK in the frame.
+            adjustInsets = new Insets(BORDER + 5, BORDER,
+                BORDER - 3, BORDER + 2);
+        } else {
+            adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
+        }
         setupFrame();
     }
 
@@ -106,6 +116,7 @@ class SwingComponent {
      */
     public SwingComponent(final JComponent component) {
         this.component = component;
+        adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
         setupComponent();
     }
 
index aa49467372f143a785166e46cc15a7899f81a9c6..0727efc894d5832dc515fa1342d622a2eac885df 100644 (file)
@@ -579,26 +579,12 @@ public class SwingTerminal extends LogicalScreen
         ) {
             do {
                 do {
-                    /*
-                     * TODO:
-                     *
-                     * Under Windows and Mac (I think?), there was a problem
-                     * with the screen not updating on the initial load.
-                     * Adding clearPhysical() below "fixed" it, but at a
-                     * horrible performance penalty on Linux which I am no
-                     * longer willing to accept.
-                     *
-                     * Fix this in the "right" way for Windows/OSX such that
-                     * the entire screen does not require a full redraw.
-                     */
-                    // clearPhysical();
                     drawToSwing();
                 } while (swing.getBufferStrategy().contentsRestored());
 
                 swing.getBufferStrategy().show();
                 Toolkit.getDefaultToolkit().sync();
             } while (swing.getBufferStrategy().contentsLost());
-
         } else {
             // Non-triple-buffered, call drawToSwing() once
             drawToSwing();
@@ -1333,7 +1319,10 @@ public class SwingTerminal extends LogicalScreen
         }
 
         // Enable anti-aliasing
-        if (gr instanceof Graphics2D) {
+        if ((gr instanceof Graphics2D) && (swing.getFrame() != null)) {
+            // Anti-aliasing on JComponent makes the hash character disappear
+            // for Terminus font, and also kills performance.  Only enable it
+            // for JFrame.
             ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                 RenderingHints.VALUE_ANTIALIAS_ON);
             ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
@@ -1344,7 +1333,6 @@ public class SwingTerminal extends LogicalScreen
         gr2.setColor(attrToBackgroundColor(cellColor));
         gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
 
-
         // Handle blink and underline
         if (!cell.isBlink()
             || (cell.isBlink() && cursorBlinkVisible)
@@ -1773,13 +1761,16 @@ public class SwingTerminal extends LogicalScreen
         } else {
             ch = key.getKeyChar();
         }
-        alt = key.isAltDown();
+        // Both meta and alt count as alt, thanks to Mac using alt for
+        // "symbols" so meta ("command") is the only other modifier left.
+        alt = key.isAltDown() | key.isMetaDown();
         ctrl = key.isControlDown();
         shift = key.isShiftDown();
 
         /*
         System.err.printf("Swing Key: %s\n", key);
         System.err.printf("   isKey: %s\n", isKey);
+        System.err.printf("   meta: %s\n", key.isMetaDown());
         System.err.printf("   alt: %s\n", alt);
         System.err.printf("   ctrl: %s\n", ctrl);
         System.err.printf("   shift: %s\n", shift);
index a44de72ea321eb13bcfd79b0c9386371f5e94a7e..6a875c7c8377f154e9cafb07b064cde7ab889d3d 100644 (file)
@@ -72,10 +72,12 @@ public class TMenu extends TWindow {
     public static final int MID_SHELL           = 13;
 
     // Edit menu
-    public static final int MID_CUT             = 20;
-    public static final int MID_COPY            = 21;
-    public static final int MID_PASTE           = 22;
-    public static final int MID_CLEAR           = 23;
+    public static final int MID_UNDO            = 20;
+    public static final int MID_REDO            = 21;
+    public static final int MID_CUT             = 22;
+    public static final int MID_COPY            = 23;
+    public static final int MID_PASTE           = 24;
+    public static final int MID_CLEAR           = 25;
 
     // Search menu
     public static final int MID_FIND            = 30;
@@ -603,6 +605,14 @@ public class TMenu extends TWindow {
             icon = 0x1F5C1;
             break;
 
+        case MID_UNDO:
+            label = i18n.getString("menuUndo");
+            key = kbCtrlZ;
+            break;
+        case MID_REDO:
+            label = i18n.getString("menuRedo");
+            key = kbCtrlY;
+            break;
         case MID_CUT:
             label = i18n.getString("menuCut");
             key = kbCtrlX;
index 4a0f8e6f6fef8b301b7467be871263b5858639cc..692293eb57f98bd7da88c66488c2b611c2fe69cc 100644 (file)
@@ -2,6 +2,8 @@ menuNew=&New
 menuExit=E&xit
 menuShell=O&S Shell
 menuOpen=&Open
+menuUndo=&Undo
+menuRedo=&Redo
 menuCut=Cu&t
 menuCopy=&Copy
 menuPaste=&Paste
index e94950371af02507ad1abaad4f305983e8260274..b4a9a3bfb6b3ab476d29b45b3a01b4fbdca77816 100644 (file)
@@ -76,6 +76,23 @@ public class Document {
      */
     private Highlighter highlighter = new Highlighter();
 
+    /**
+     * The tab stop size.
+     */
+    private int tabSize = 8;
+
+    /**
+     * If true, backspace at an indent level goes back a full indent level.
+     * If false, backspace always goes back one column.
+     */
+    private boolean backspaceUnindents = false;
+
+    /**
+     * If true, save files with tab characters.  If false, convert tabs to
+     * spaces when saving files.
+     */
+    private boolean saveWithTabs = false;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -99,16 +116,41 @@ public class Document {
         }
     }
 
+    /**
+     * Private constructor used by dup().
+     */
+    private Document() {
+        // NOP
+    }
+
     // ------------------------------------------------------------------------
     // Document ---------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Create a duplicate instance.
+     *
+     * @return duplicate intance
+     */
+    public Document dup() {
+        Document other = new Document();
+        for (Line line: lines) {
+            other.lines.add(line.dup());
+        }
+        other.lineNumber = lineNumber;
+        other.overwrite = overwrite;
+        other.dirty = dirty;
+        other.defaultColor = defaultColor;
+        other.highlighter.setTo(highlighter);
+        return other;
+    }
+
     /**
      * Get the overwrite flag.
      *
      * @return true if addChar() overwrites data, false if it inserts
      */
-    public boolean getOverwrite() {
+    public boolean isOverwrite() {
         return overwrite;
     }
 
@@ -141,7 +183,11 @@ public class Document {
                 "UTF-8");
 
             for (Line line: lines) {
-                output.write(line.getRawString());
+                if (saveWithTabs) {
+                    output.write(convertSpacesToTabs(line.getRawString()));
+                } else {
+                    output.write(line.getRawString());
+                }
                 output.write("\n");
             }
 
@@ -551,7 +597,7 @@ public class Document {
         dirty = true;
         int cursor = lines.get(lineNumber).getCursor();
         if (cursor > 0) {
-            lines.get(lineNumber).backspace();
+            lines.get(lineNumber).backspace(tabSize, backspaceUnindents);
         } else if (lineNumber > 0) {
             // Join two lines
             lineNumber--;
@@ -603,6 +649,62 @@ public class Document {
         }
     }
 
+    /**
+     * Get the tab stop size.
+     *
+     * @return the tab stop size
+     */
+    public int getTabSize() {
+        return tabSize;
+    }
+
+    /**
+     * Set the tab stop size.
+     *
+     * @param tabSize the new tab stop size
+     */
+    public void setTabSize(final int tabSize) {
+        this.tabSize = tabSize;
+    }
+
+    /**
+     * Set the backspace unindent option.
+     *
+     * @param backspaceUnindents If true, backspace at an indent level goes
+     * back a full indent level.  If false, backspace always goes back one
+     * column.
+     */
+    public void setBackspaceUnindents(final boolean backspaceUnindents) {
+        this.backspaceUnindents = backspaceUnindents;
+    }
+
+    /**
+     * Set the save with tabs option.
+     *
+     * @param saveWithTabs If true, save files with tab characters.  If
+     * false, convert tabs to spaces when saving files.
+     */
+    public void setSaveWithTabs(final boolean saveWithTabs) {
+        this.saveWithTabs = saveWithTabs;
+    }
+
+    /**
+     * Handle the tab character.
+     */
+    public void tab() {
+        if (overwrite) {
+            del();
+        }
+        lines.get(lineNumber).tab(tabSize);
+    }
+
+    /**
+     * Handle the backtab (shift-tab) character.
+     */
+    public void backTab() {
+        lines.get(lineNumber).backTab(tabSize);
+    }
+
     /**
      * Get a (shallow) copy of the list of lines.
      *
@@ -659,4 +761,63 @@ public class Document {
         return sb.toString();
     }
 
+    /**
+     * Trim trailing whitespace from lines and trailing empty
+     * lines from the document.
+     */
+    public void cleanWhitespace() {
+        for (Line line: getLines()) {
+            line.trimRight();
+        }
+        if (lines.size() == 0) {
+            return;
+        }
+        while (lines.get(lines.size() - 1).length() == 0) {
+            lines.remove(lines.size() - 1);
+        }
+        if (lineNumber > lines.size() - 1) {
+            lineNumber = lines.size() - 1;
+        }
+    }
+
+    /**
+     * Set keyword highlighting.
+     *
+     * @param enabled if true, enable keyword highlighting
+     */
+    public void setHighlighting(final boolean enabled) {
+        highlighter.setEnabled(enabled);
+        for (Line line: getLines()) {
+            line.scanLine();
+        }
+    }
+
+    /**
+     * Convert a string with leading spaces to a mix of tabs and spaces.
+     *
+     * @param string the string to convert
+     */
+    private String convertSpacesToTabs(final String string) {
+        if (string.length() == 0) {
+            return string;
+        }
+
+        int start = 0;
+        while (string.charAt(start) == ' ') {
+            start++;
+        }
+        int tabCount = start / 8;
+        if (tabCount == 0) {
+            return string;
+        }
+
+        StringBuilder sb = new StringBuilder(string.length());
+
+        for (int i = 0; i < tabCount; i++) {
+            sb.append('\t');
+        }
+        sb.append(string.substring(tabCount * 8));
+        return sb.toString();
+    }
+
 }
index 44a2ed08c705a43584aaea1c0e22bd1ab1ff85c7..23ee90014e863bc0568a3526f4e086a793b6465c 100644 (file)
@@ -56,13 +56,36 @@ public class Highlighter {
      * Public constructor sets the theme to the default.
      */
     public Highlighter() {
-        colors = new TreeMap<String, CellAttributes>();
+        // NOP
     }
 
     // ------------------------------------------------------------------------
     // Highlighter ------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Set keyword highlighting.
+     *
+     * @param enabled if true, enable keyword highlighting
+     */
+    public void setEnabled(final boolean enabled) {
+        if (enabled) {
+            setJavaColors();
+        } else {
+            colors = null;
+        }
+    }
+
+    /**
+     * Set my field values to that's field.
+     *
+     * @param rhs an instance of Highlighter
+     */
+    public void setTo(final Highlighter rhs) {
+        colors = new TreeMap<String, CellAttributes>();
+        colors.putAll(rhs.colors);
+    }
+
     /**
      * See if this is a character that should split a word.
      *
@@ -87,6 +110,9 @@ public class Highlighter {
      * @return color associated with name, e.g. bold yellow on blue
      */
     public CellAttributes getColor(final String name) {
+        if (colors == null) {
+            return null;
+        }
         CellAttributes attr = colors.get(name);
         return attr;
     }
@@ -95,19 +121,41 @@ public class Highlighter {
      * Sets to defaults that resemble the Borland IDE colors.
      */
     public void setJavaColors() {
+        colors = new TreeMap<String, CellAttributes>();
+
         CellAttributes color;
 
-        String [] keywords = {
+        String [] types = {
             "boolean", "byte", "short", "int", "long", "char", "float",
-            "double", "void", "new",
-            "static", "final", "volatile", "synchronized", "abstract",
-            "public", "private", "protected",
-            "class", "interface", "extends", "implements",
+            "double", "void",
+        };
+        color = new CellAttributes();
+        color.setForeColor(Color.GREEN);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        for (String str: types) {
+            colors.put(str, color);
+        }
+
+        String [] modifiers = {
+            "abstract", "final", "native", "private", "protected", "public",
+            "static", "strictfp", "synchronized", "transient", "volatile",
+        };
+        color = new CellAttributes();
+        color.setForeColor(Color.WHITE);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        for (String str: modifiers) {
+            colors.put(str, color);
+        }
+
+        String [] keywords = {
+            "new", "class", "interface", "extends", "implements",
             "if", "else", "do", "while", "for", "break", "continue",
             "switch", "case", "default",
         };
         color = new CellAttributes();
-        color.setForeColor(Color.WHITE);
+        color.setForeColor(Color.YELLOW);
         color.setBackColor(Color.BLUE);
         color.setBold(true);
         for (String str: keywords) {
index 7cd5febabee8462f6c51bc66e886903795aff698..b5c980a59f9c9812b6f6a84ce832fe9781051702 100644 (file)
@@ -32,6 +32,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
 import jexer.bits.StringUtils;
 
 /**
@@ -92,7 +93,31 @@ public class Line {
 
         this.defaultColor = defaultColor;
         this.highlighter = highlighter;
-        this.rawText = new StringBuilder(str);
+
+        this.rawText = new StringBuilder();
+        int col = 0;
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            if (ch == '\t') {
+                // Expand tabs
+                int j = col % 8;
+                do {
+                    rawText.append(' ');
+                    j++;
+                    col++;
+                } while ((j % 8) != 0);
+                continue;
+            }
+            if ((ch <= 0x20) || (ch == 0x7F)) {
+                // Replace all other C0 bytes with CP437 glyphs.
+                rawText.append(GraphicsChars.CP437[(int) ch]);
+                col++;
+                continue;
+            }
+
+            rawText.append(ch);
+            col++;
+        }
 
         scanLine();
     }
@@ -107,10 +132,33 @@ public class Line {
         this(str, defaultColor, null);
     }
 
+    /**
+     * Private constructor used by dup().
+     */
+    private Line() {
+        // NOP
+    }
+
     // ------------------------------------------------------------------------
     // Line -------------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Create a duplicate instance.
+     *
+     * @return duplicate intance
+     */
+    public Line dup() {
+        Line other = new Line();
+        other.defaultColor = defaultColor;
+        other.highlighter = highlighter;
+        other.position = position;
+        other.screenPosition = screenPosition;
+        other.rawText = new StringBuilder(rawText);
+        other.scanLine();
+        return other;
+    }
+
     /**
      * Get a (shallow) copy of the words in this line.
      *
@@ -193,9 +241,19 @@ public class Line {
     }
 
     /**
-     * Scan rawText and make words out of it.
+     * Get the raw length of this line.
+     *
+     * @return the length of this line in characters, which may be different
+     * from the number of cells needed to display it
+     */
+    public int length() {
+        return rawText.length();
+    }
+
+    /**
+     * Scan rawText and make words out of it.  Note package private access.
      */
-    private void scanLine() {
+    void scanLine() {
         words.clear();
         Word word = new Word(this.defaultColor, this.highlighter);
         words.add(word);
@@ -236,7 +294,7 @@ public class Line {
         if (getDisplayLength() == 0) {
             return false;
         }
-        if (position == getDisplayLength() - 1) {
+        if (screenPosition == getDisplayLength() - 1) {
             return false;
         }
         if (position < rawText.length()) {
@@ -267,7 +325,7 @@ public class Line {
      * @return true if the cursor position changed
      */
     public boolean end() {
-        if (position != getDisplayLength() - 1) {
+        if (screenPosition != getDisplayLength() - 1) {
             position = rawText.length();
             screenPosition = StringUtils.width(rawText.toString());
             return true;
@@ -281,7 +339,7 @@ public class Line {
     public void del() {
         assert (words.size() > 0);
 
-        if (position < getDisplayLength()) {
+        if (screenPosition < getDisplayLength()) {
             int n = Character.charCount(rawText.codePointAt(position));
             for (int i = 0; i < n; i++) {
                 rawText.deleteCharAt(position);
@@ -294,8 +352,32 @@ public class Line {
 
     /**
      * Delete the character immediately preceeding the cursor.
+     *
+     * @param tabSize the tab stop size
+     * @param backspaceUnindents If true, backspace at an indent level goes
+     * back a full indent level.  If false, backspace always goes back one
+     * column.
      */
-    public void backspace() {
+    public void backspace(final int tabSize, final boolean backspaceUnindents) {
+        if ((backspaceUnindents == true)
+            && (tabSize > 0)
+            && (screenPosition > 0)
+            && (rawText.charAt(position - 1) == ' ')
+            && ((screenPosition % tabSize) == 0)
+        ) {
+            boolean doBackTab = true;
+            for (int i = 0; i < position; i++) {
+                if (rawText.charAt(i) != ' ') {
+                    doBackTab = false;
+                    break;
+                }
+            }
+            if (doBackTab) {
+                backTab(tabSize);
+                return;
+            }
+        }
+
         if (left()) {
             del();
         }
@@ -307,7 +389,7 @@ public class Line {
      * @param ch the character to insert
      */
     public void addChar(final int ch) {
-        if (position < getDisplayLength() - 1) {
+        if (screenPosition < getDisplayLength() - 1) {
             rawText.insert(position, Character.toChars(ch));
         } else {
             rawText.append(Character.toChars(ch));
@@ -323,7 +405,7 @@ public class Line {
      * @param ch the character to replace
      */
     public void replaceChar(final int ch) {
-        if (position < getDisplayLength() - 1) {
+        if (screenPosition < getDisplayLength() - 1) {
             // Replace character
             String oldText = rawText.toString();
             rawText = new StringBuilder(oldText.substring(0, position));
@@ -345,7 +427,7 @@ public class Line {
      * @param screenPosition the position on screen
      * @return the equivalent position in text
      */
-    protected int screenToTextPosition(final int screenPosition) {
+    private int screenToTextPosition(final int screenPosition) {
         if (screenPosition == 0) {
             return 0;
         }
@@ -362,4 +444,55 @@ public class Line {
             " exceeds available text length " + rawText.length());
     }
 
+    /**
+     * Trim trailing whitespace from line, repositioning cursor if needed.
+     */
+    public void trimRight() {
+        if (rawText.length() == 0) {
+            return;
+        }
+        if (!Character.isWhitespace(rawText.charAt(rawText.length() - 1))) {
+            return;
+        }
+        while ((rawText.length() > 0)
+            && Character.isWhitespace(rawText.charAt(rawText.length() - 1))
+        ) {
+            rawText.deleteCharAt(rawText.length() - 1);
+        }
+        if (position >= rawText.length()) {
+            end();
+        }
+        scanLine();
+    }
+
+    /**
+     * Handle the tab character.
+     *
+     * @param tabSize the tab stop size
+     */
+    public void tab(final int tabSize) {
+        if (tabSize > 0) {
+            do {
+                addChar(' ');
+            } while ((screenPosition % tabSize) != 0);
+        }
+    }
+
+    /**
+     * Handle the backtab (shift-tab) character.
+     *
+     * @param tabSize the tab stop size
+     */
+    public void backTab(final int tabSize) {
+        if ((tabSize > 0) && (screenPosition > 0)
+            && (rawText.charAt(position - 1) == ' ')
+        ) {
+            do {
+                backspace(tabSize, false);
+            } while (((screenPosition % tabSize) != 0)
+                && (screenPosition > 0)
+                && (rawText.charAt(position - 1) == ' '));
+        }
+    }
+
 }
index 9a25d8183535f5864a7c81a7aa56a70e51c218ab..483f9c3d86c46a1dfbf225876eb7b42c219ba0c1 100644 (file)
@@ -135,8 +135,6 @@ public class Word {
      * @return the number of cells needed to display this word
      */
     public int getDisplayLength() {
-        // TODO: figure out how to handle the tab character.  Do we have a
-        // global tab stops list and current word position?
         return StringUtils.width(text.toString());
     }