From e8a11f986bfe2556e450d7b8ad6ef0059b369bbc Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Sat, 12 Aug 2017 13:39:06 -0400 Subject: [PATCH] TEditor 50% complete --- LICENSE | 2 +- README.md | 5 + docs/TODO.md | 29 +--- docs/worklog.md | 15 ++ src/jexer/TApplication.java | 81 +++++++-- src/jexer/TEditorWidget.java | 230 +++++++++++++++++++++++--- src/jexer/backend/SwingTerminal.java | 18 ++ src/jexer/bits/ColorTheme.java | 4 +- src/jexer/demos/DemoEditorWindow.java | 18 +- src/jexer/demos/DemoMainWindow.java | 2 +- src/jexer/teditor/Document.java | 156 +++++++++++++---- src/jexer/teditor/Highlighter.java | 133 +++++++++++++++ src/jexer/teditor/Line.java | 139 +++++++++++++--- src/jexer/teditor/Word.java | 66 +++++++- 14 files changed, 782 insertions(+), 116 deletions(-) create mode 100644 src/jexer/teditor/Highlighter.java diff --git a/LICENSE b/LICENSE index 09bbfe0..5f13f7a 100644 --- 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 diff --git a/README.md b/README.md index 1e61a77..7dc0bb4 100644 --- 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. diff --git a/docs/TODO.md b/docs/TODO.md index 08a912a..e596382 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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 - - StringUtils.justify functions for ArrayList + - 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 diff --git a/docs/worklog.md b/docs/worklog.md index 2d1ae20..5704040 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -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 diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index ab9c196..8b436ab 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -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 getAllMenus() { + return new LinkedList(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. */ diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java index 6fed1fc..361ed83 100644 --- a/src/jexer/TEditorWidget.java +++ b/src/jexer/TEditorWidget.java @@ -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); + } + } + } diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index 6e90219..b7a1624 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -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) { diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java index 8a67d18..9618ccc 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/src/jexer/bits/ColorTheme.java @@ -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); diff --git a/src/jexer/demos/DemoEditorWindow.java b/src/jexer/demos/DemoEditorWindow.java index 5639ed7..f04916a 100644 --- a/src/jexer/demos/DemoEditorWindow.java +++ b/src/jexer/demos/DemoEditorWindow.java @@ -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" + ); } diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java index 53f30d7..598ac7f 100644 --- a/src/jexer/demos/DemoMainWindow.java +++ b/src/jexer/demos/DemoMainWindow.java @@ -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; diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java index cae6f47..5b7050f 100644 --- a/src/jexer/teditor/Document.java +++ b/src/jexer/teditor/Document.java @@ -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 index 0000000..9576ad1 --- /dev/null +++ b/src/jexer/teditor/Highlighter.java @@ -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 colors; + + /** + * Public constructor sets the theme to the default. + */ + public Highlighter() { + colors = new TreeMap(); + } + + /** + * 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); + } + + } + + +} diff --git a/src/jexer/teditor/Line.java b/src/jexer/teditor/Line.java index b89d827..de12659 100644 --- a/src/jexer/teditor/Line.java +++ b/src/jexer/teditor/Line.java @@ -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 words = new ArrayList(); + /** + * 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 + } + } diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java index d7a6576..d9b3417 100644 --- a/src/jexer/teditor/Word.java +++ b/src/jexer/teditor/Word.java @@ -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; } -- 2.27.0