From: Kevin Lamonte Date: Mon, 14 Aug 2017 18:45:33 +0000 (-0400) Subject: TEditor 80% complete X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=71a389c9810382e014682dde52e94d3f34e385fa;p=fanfix-jexer.git TEditor 80% complete --- diff --git a/docs/TODO.md b/docs/TODO.md index e596382..20ce1fe 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -11,17 +11,18 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the 0.0.5 - TEditor + - Document + - Filename + - Pick appropriate Highlighter: plain, Java, XML, ... - TEditorWidget: - - Mouse wheel is buggy as hell - - Actual editing - Cut and Paste - - TEditorWindow extends TScrollableWindow - TTextArea extends TScrollableWidget 0.0.6 - TEditor - - True tokenization and syntax highlighting: Java, C, Clojure + - True tokenization and syntax highlighting: Java, C, Clojure, XML + - Tab character support - Finish up multiscreen support: - cmAbort to cmScreenDisconnected diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 8b436ab..691e1c7 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -2420,6 +2420,16 @@ public class TApplication implements Runnable { return true; } + if (command.equals(cmMenu)) { + if (!modalWindowActive() && (activeMenu == null)) { + if (menus.size() > 0) { + menus.get(0).setActive(true); + activeMenu = menus.get(0); + return true; + } + } + } + return false; } diff --git a/src/jexer/TCommand.java b/src/jexer/TCommand.java index 8381d9e..a814fae 100644 --- a/src/jexer/TCommand.java +++ b/src/jexer/TCommand.java @@ -121,6 +121,16 @@ public class TCommand { */ public static final int HELP = 20; + /** + * Enter first menu. + */ + public static final int MENU = 21; + + /** + * Save file. + */ + public static final int SAVE = 30; + /** * Type of command, one of EXIT, CASCADE, etc. */ @@ -189,5 +199,7 @@ public class TCommand { public static final TCommand cmWindowPrevious = new TCommand(WINDOW_PREVIOUS); public static final TCommand cmWindowClose = new TCommand(WINDOW_CLOSE); public static final TCommand cmHelp = new TCommand(HELP); + public static final TCommand cmSave = new TCommand(SAVE); + public static final TCommand cmMenu = new TCommand(MENU); } diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java index 361ed83..cf4d887 100644 --- a/src/jexer/TEditorWidget.java +++ b/src/jexer/TEditorWidget.java @@ -28,6 +28,8 @@ */ package jexer; +import java.io.IOException; + import jexer.bits.CellAttributes; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; @@ -121,34 +123,16 @@ public final class TEditorWidget extends TWidget { @Override public void onMouseDown(final TMouseEvent mouse) { if (mouse.isMouseWheelUp()) { - if (getCursorY() == getHeight() - 1) { - if (document.up()) { - if (topLine > 0) { - topLine--; - } - alignCursor(); - } - } else { - if (topLine > 0) { - topLine--; - setCursorY(getCursorY() + 1); - } + if (topLine > 0) { + topLine--; + alignDocument(false); } return; } if (mouse.isMouseWheelDown()) { - if (getCursorY() == 0) { - if (document.down()) { - if (topLine < document.getLineNumber()) { - topLine++; - } - alignCursor(); - } - } else { - if (topLine < document.getLineCount() - getHeight()) { - topLine++; - setCursorY(getCursorY() - 1); - } + if (topLine < document.getLineCount() - 1) { + topLine++; + alignDocument(true); } return; } @@ -185,6 +169,63 @@ public final class TEditorWidget extends TWidget { super.onMouseDown(mouse); } + /** + * Align visible area with document current line. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignTopLine(final boolean topLineIsTop) { + int line = document.getLineNumber(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move topLine to bring document back into view. + if (topLineIsTop) { + topLine = line - (getHeight() - 1); + } else { + topLine = line; + } + } + + /* + System.err.println("line " + line + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + setCursorY(line - topLine); + alignCursor(); + } + + /** + * Align document current line with visible area. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignDocument(final boolean topLineIsTop) { + int line = document.getLineNumber(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move document to ensure it fits view. + if (topLineIsTop) { + document.setLineNumber(topLine); + } else { + document.setLineNumber(topLine + (getHeight() - 1)); + } + } + + /* + System.err.println("getLineNumber() " + document.getLineNumber() + + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + /** * Align visible cursor with document cursor. */ @@ -224,57 +265,17 @@ public final class TEditorWidget extends TWidget { alignCursor(); } } else if (keypress.equals(kbUp)) { - if (document.up()) { - if (getCursorY() > 0) { - setCursorY(getCursorY() - 1); - } else { - if (topLine > 0) { - topLine--; - } - } - alignCursor(); - } + document.up(); + alignTopLine(false); } else if (keypress.equals(kbDown)) { - if (document.down()) { - if (getCursorY() < getHeight() - 1) { - setCursorY(getCursorY() + 1); - } else { - if (topLine < document.getLineCount() - getHeight()) { - topLine++; - } - } - alignCursor(); - } + document.down(); + alignTopLine(true); } else if (keypress.equals(kbPgUp)) { - 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; - } - } + document.up(getHeight() - 1); + alignTopLine(false); } else if (keypress.equals(kbPgDn)) { - 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; - } - } + document.down(getHeight() - 1); + alignTopLine(true); } else if (keypress.equals(kbHome)) { if (document.home()) { leftColumn = 0; @@ -297,29 +298,25 @@ public final class TEditorWidget extends TWidget { } 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(); + alignTopLine(false); } else if (keypress.equals(kbIns)) { document.setOverwrite(!document.getOverwrite()); } else if (keypress.equals(kbDel)) { + // TODO: join lines document.del(); + alignCursor(); } else if (keypress.equals(kbBackspace)) { document.backspace(); alignCursor(); + } else if (keypress.equals(kbEnter)) { + // TODO: split lines } else if (!keypress.getKey().isFnKey() && !keypress.getKey().isAlt() && !keypress.getKey().isCtrl() ) { // Plain old keystroke, process it document.addChar(keypress.getKey().getChar()); + alignCursor(); } else { // Pass other keys (tab etc.) on to TWidget super.onKeypress(keypress); @@ -354,4 +351,87 @@ public final class TEditorWidget extends TWidget { } } + /** + * Get the number of lines in the underlying Document. + * + * @return the number of lines + */ + public int getLineCount() { + return document.getLineCount(); + } + + /** + * Get the current editing row number. 1-based. + * + * @return the editing row number. Row 1 is the first row. + */ + public int getEditingRowNumber() { + return document.getLineNumber() + 1; + } + + /** + * Set the current editing row number. 1-based. + * + * @param row the new editing row number. Row 1 is the first row. + */ + public void setEditingRowNumber(final int row) { + document.setLineNumber(row - 1); + } + + /** + * Get the current editing column number. 1-based. + * + * @return the editing column number. Column 1 is the first column. + */ + public int getEditingColumnNumber() { + return document.getCursor() + 1; + } + + /** + * Set the current editing column number. 1-based. + * + * @param column the new editing column number. Column 1 is the first + * column. + */ + public void setEditingColumnNumber(final int column) { + document.setCursor(column - 1); + } + + /** + * Get the maximum possible row number. 1-based. + * + * @return the maximum row number. Row 1 is the first row. + */ + public int getMaximumRowNumber() { + return document.getLineCount() + 1; + } + + /** + * Get the maximum possible column number. 1-based. + * + * @return the maximum column number. Column 1 is the first column. + */ + public int getMaximumColumnNumber() { + return document.getLineLengthMax() + 1; + } + + /** + * Get the dirty value. + * + * @return true if the buffer is dirty + */ + public boolean isDirty() { + return document.isDirty(); + } + + /** + * Save contents to file. + * + * @param filename file to save to + * @throws IOException if a java.io operation throws + */ + public void saveToFilename(final String filename) throws IOException { + document.saveToFilename(filename); + } + } diff --git a/src/jexer/TEditorWindow.java b/src/jexer/TEditorWindow.java new file mode 100644 index 0000000..f96b177 --- /dev/null +++ b/src/jexer/TEditorWindow.java @@ -0,0 +1,267 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import java.io.File; +import java.io.IOException; +import java.util.Scanner; + +import jexer.TApplication; +import jexer.TEditorWidget; +import jexer.THScroller; +import jexer.TScrollableWindow; +import jexer.TVScroller; +import jexer.TWidget; +import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; +import jexer.event.TCommandEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; +import static jexer.TCommand.*; +import static jexer.TKeypress.*; + +/** + * TEditorWindow is a basic text file editor. + */ +public class TEditorWindow extends TScrollableWindow { + + /** + * Hang onto my TEditor so I can resize it with the window. + */ + private TEditorWidget editField; + + /** + * The fully-qualified name of the file being edited. + */ + private String filename = ""; + + /** + * Setup other fields after the editor is created. + */ + private void setupAfterEditor() { + hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20); + vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2); + setMinimumWindowWidth(25); + setMinimumWindowHeight(10); + setTopValue(1); + setBottomValue(editField.getMaximumRowNumber()); + setLeftValue(1); + setRightValue(editField.getMaximumColumnNumber()); + + statusBar = newStatusBar("Editor"); + statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); + statusBar.addShortcutKeypress(kbF2, cmSave, "Save"); + statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); + statusBar.addShortcutKeypress(kbF10, cmMenu, "Menu"); + } + + /** + * Public constructor sets window title. + * + * @param parent the main application + * @param title the window title + */ + public TEditorWindow(final TApplication parent, final String title) { + + super(parent, title, 0, 0, parent.getScreen().getWidth(), + parent.getScreen().getHeight() - 2, RESIZABLE); + + editField = addEditor("", 0, 0, getWidth() - 2, getHeight() - 2); + setupAfterEditor(); + } + + /** + * Public constructor sets window title and contents. + * + * @param parent the main application + * @param title the window title, usually a filename + * @param contents the data for the editing window, usually the file data + */ + public TEditorWindow(final TApplication parent, final String title, + final String contents) { + + super(parent, title, 0, 0, parent.getScreen().getWidth(), + parent.getScreen().getHeight() - 2, RESIZABLE); + + filename = title; + editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2); + setupAfterEditor(); + } + + /** + * Public constructor. + * + * @param parent the main application + */ + public TEditorWindow(final TApplication parent) { + this(parent, "New Text Document"); + } + + /** + * 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); + } + } + + /** + * Check if a mouse press/release/motion event coordinate is over the + * editor. + * + * @param mouse a mouse-based event + * @return whether or not the mouse is on the emulator + */ + private final boolean mouseOnEditor(final TMouseEvent mouse) { + if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1) + && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1) + && (mouse.getAbsoluteY() >= getAbsoluteY() + 1) + && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1) + ) { + return true; + } + return false; + } + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouseOnEditor(mouse)) { + editField.onMouseDown(mouse); + setBottomValue(editField.getMaximumRowNumber()); + setVerticalValue(editField.getEditingRowNumber()); + setRightValue(editField.getMaximumColumnNumber()); + setHorizontalValue(editField.getEditingColumnNumber()); + } else { + // Let the scrollbars get the event + super.onMouseDown(mouse); + + if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) { + editField.setEditingRowNumber(getVerticalValue()); + } + // TODO: horizontal scrolling + } + } + + /** + * Handle window/screen resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + if (event.getType() == TResizeEvent.Type.WIDGET) { + // Resize the text field + TResizeEvent editSize = new TResizeEvent(TResizeEvent.Type.WIDGET, + event.getWidth() - 2, event.getHeight() - 2); + editField.onResize(editSize); + + // Have TScrollableWindow handle the scrollbars + super.onResize(event); + return; + } + + // Pass to children instead + for (TWidget widget: getChildren()) { + widget.onResize(event); + } + } + + /** + * Method that subclasses can override to handle posted command events. + * + * @param command command event + */ + @Override + public void onCommand(final TCommandEvent command) { + if (command.equals(cmOpen)) { + try { + String filename = fileOpenBox("."); + if (filename != null) { + try { + File file = new File(filename); + StringBuilder fileContents = new StringBuilder(); + Scanner scanner = new Scanner(file); + String EOL = System.getProperty("line.separator"); + + try { + while (scanner.hasNextLine()) { + fileContents.append(scanner.nextLine() + EOL); + } + new TEditorWindow(getApplication(), filename, + fileContents.toString()); + } finally { + scanner.close(); + } + } catch (IOException e) { + // TODO: make this a message box + e.printStackTrace(); + } + } + } catch (IOException e) { + // TODO: make this a message box + e.printStackTrace(); + } + return; + } + + if (command.equals(cmSave)) { + if (filename.length() > 0) { + try { + editField.saveToFilename(filename); + } catch (IOException e) { + // TODO: make this a message box + e.printStackTrace(); + } + } + return; + } + + // Didn't handle it, let children get it instead + super.onCommand(command); + } + +} diff --git a/src/jexer/TScrollableWindow.java b/src/jexer/TScrollableWindow.java index 8935ac3..ce20df7 100644 --- a/src/jexer/TScrollableWindow.java +++ b/src/jexer/TScrollableWindow.java @@ -53,8 +53,8 @@ public class TScrollableWindow extends TWindow implements Scrollable { protected void placeScrollbars() { if (hScroller != null) { hScroller.setY(getHeight() - 2); - hScroller.setWidth(getWidth() - 3); - hScroller.setBigChange(getWidth() - 3); + hScroller.setWidth(getWidth() - hScroller.getX() - 3); + hScroller.setBigChange(getWidth() - hScroller.getX() - 3); } if (vScroller != null) { vScroller.setX(getWidth() - 2); diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 32907a8..9621305 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -284,9 +284,60 @@ public class TWindow extends TWidget { * @param maximumWindowWidth new maximum width */ public final void setMaximumWindowWidth(final int maximumWindowWidth) { + if ((maximumWindowWidth != -1) + && (maximumWindowWidth < minimumWindowWidth + 1) + ) { + throw new IllegalArgumentException("Maximum window width cannot " + + "be smaller than minimum window width + 1"); + } this.maximumWindowWidth = maximumWindowWidth; } + /** + * Set the minimum width for this window. + * + * @param minimumWindowWidth new minimum width + */ + public final void setMinimumWindowWidth(final int minimumWindowWidth) { + if ((maximumWindowWidth != -1) + && (minimumWindowWidth > maximumWindowWidth - 1) + ) { + throw new IllegalArgumentException("Minimum window width cannot " + + "be larger than maximum window width - 1"); + } + this.minimumWindowWidth = minimumWindowWidth; + } + + /** + * Set the maximum height for this window. + * + * @param maximumWindowHeight new maximum height + */ + public final void setMaximumWindowHeight(final int maximumWindowHeight) { + if ((maximumWindowHeight != -1) + && (maximumWindowHeight < minimumWindowHeight + 1) + ) { + throw new IllegalArgumentException("Maximum window height cannot " + + "be smaller than minimum window height + 1"); + } + this.maximumWindowHeight = maximumWindowHeight; + } + + /** + * Set the minimum height for this window. + * + * @param minimumWindowHeight new minimum height + */ + public final void setMinimumWindowHeight(final int minimumWindowHeight) { + if ((maximumWindowHeight != -1) + && (minimumWindowHeight > maximumWindowHeight - 1) + ) { + throw new IllegalArgumentException("Minimum window height cannot " + + "be larger than maximum window height - 1"); + } + this.minimumWindowHeight = minimumWindowHeight; + } + /** * Recenter the window on-screen. */ diff --git a/src/jexer/bits/GraphicsChars.java b/src/jexer/bits/GraphicsChars.java index 8626582..3240309 100644 --- a/src/jexer/bits/GraphicsChars.java +++ b/src/jexer/bits/GraphicsChars.java @@ -146,4 +146,5 @@ public final class GraphicsChars { public static final char WINDOW_LEFT_BOTTOM_DOUBLE = CP437[0xC8]; public static final char WINDOW_RIGHT_BOTTOM_DOUBLE = CP437[0xBC]; public static final char VERTICAL_BAR = CP437[0xB3]; + public static final char OCTOSTAR = CP437[0x0F]; } diff --git a/src/jexer/demos/DemoApplication.java b/src/jexer/demos/DemoApplication.java index 43bb709..4d8671f 100644 --- a/src/jexer/demos/DemoApplication.java +++ b/src/jexer/demos/DemoApplication.java @@ -76,7 +76,7 @@ public class DemoApplication extends TApplication { item = subMenu.addItem(2002, "&Normal (sub)"); if (getScreen() instanceof SwingTerminal) { - TMenu swingMenu = addMenu("&Swing"); + TMenu swingMenu = addMenu("Swin&g"); item = swingMenu.addItem(3000, "&Bigger +2"); item = swingMenu.addItem(3001, "&Smaller -2"); } diff --git a/src/jexer/demos/DemoEditorWindow.java b/src/jexer/demos/DemoEditorWindow.java index f04916a..0053831 100644 --- a/src/jexer/demos/DemoEditorWindow.java +++ b/src/jexer/demos/DemoEditorWindow.java @@ -34,7 +34,7 @@ import static jexer.TCommand.*; import static jexer.TKeypress.*; /** - * This window demonstates the TText, THScroller, and TVScroller widgets. + * This window demonstates the TEditor widget. */ public class DemoEditorWindow extends TWindow { @@ -56,10 +56,9 @@ public class DemoEditorWindow extends TWindow { super(parent, title, 0, 0, 44, 22, RESIZABLE); editField = addEditor(text, 0, 0, 42, 20); - statusBar = newStatusBar("Editable text window"); + statusBar = newStatusBar("Editable text demo window"); statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); } diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java index 598ac7f..8840605 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, 23, flags); + super(parent, "Demo Window", 0, 0, 64, 23, flags); int row = 1; @@ -123,13 +123,20 @@ public class DemoMainWindow extends TWindow { row += 2; addLabel("Editor window", 1, row); - addButton("Edito&r", 35, row, + addButton("&1 Widget", 35, row, new TAction() { public void DO() { new DemoEditorWindow(getApplication()); } } ); + addButton("&2 Window", 48, row, + new TAction() { + public void DO() { + new TEditorWindow(getApplication()); + } + } + ); row += 2; addLabel("Text areas", 1, row); diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java index 5b7050f..42082e5 100644 --- a/src/jexer/teditor/Document.java +++ b/src/jexer/teditor/Document.java @@ -28,6 +28,9 @@ */ package jexer.teditor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.List; @@ -54,6 +57,11 @@ public class Document { */ private boolean overwrite = false; + /** + * If true, the document has been edited. + */ + private boolean dirty = false; + /** * The default color for the TEditor class. */ @@ -73,6 +81,41 @@ public class Document { return overwrite; } + /** + * Get the dirty value. + * + * @return true if the buffer is dirty + */ + public boolean isDirty() { + return dirty; + } + + /** + * Save contents to file. + * + * @param filename file to save to + * @throws IOException if a java.io operation throws + */ + public void saveToFilename(final String filename) throws IOException { + OutputStreamWriter output = null; + try { + output = new OutputStreamWriter(new FileOutputStream(filename), + "UTF-8"); + + for (Line line: lines) { + output.write(line.getRawString()); + output.write("\n"); + } + + dirty = false; + } + finally { + if (output != null) { + output.close(); + } + } + } + /** * Set the overwrite flag. * @@ -136,6 +179,15 @@ public class Document { return lines.get(lineNumber).getCursor(); } + /** + * Set the current cursor position of the editing line. 0-based. + * + * @param cursor the new cursor position + */ + public void setCursor(final int cursor) { + lines.get(lineNumber).setCursor(cursor); + } + /** * Construct a new Document from an existing text string. * @@ -280,6 +332,7 @@ public class Document { * Delete the character under the cursor. */ public void del() { + dirty = true; lines.get(lineNumber).del(); } @@ -287,6 +340,7 @@ public class Document { * Delete the character immediately preceeding the cursor. */ public void backspace() { + dirty = true; lines.get(lineNumber).backspace(); } @@ -297,6 +351,7 @@ public class Document { * @param ch the character to replace or insert */ public void addChar(final char ch) { + dirty = true; if (overwrite) { lines.get(lineNumber).replaceChar(ch); } else { diff --git a/src/jexer/teditor/Line.java b/src/jexer/teditor/Line.java index de12659..d6016c2 100644 --- a/src/jexer/teditor/Line.java +++ b/src/jexer/teditor/Line.java @@ -60,14 +60,19 @@ public class Line { private int cursor = 0; /** - * The current word that the cursor position is in. + * The raw text of this line, what is passed to Word to determine + * highlighting behavior. */ - private Word currentWord; + private StringBuilder rawText; /** - * We use getDisplayLength() a lot, so cache the value. + * Get a (shallow) copy of the words in this line. + * + * @return a copy of the word list */ - private int displayLength = -1; + public List getWords() { + return new ArrayList(words); + } /** * Get the current cursor position. @@ -92,39 +97,52 @@ public class Line { getDisplayLength() + ", requested position " + cursor); } this.cursor = cursor; - // TODO: set word } /** - * Get a (shallow) copy of the list of words. + * Get the on-screen display length. * - * @return the list of words + * @return the number of cells needed to display this line */ - public List getWords() { - return new ArrayList(words); + public int getDisplayLength() { + int n = rawText.length(); + + // For now just return the raw text length. + if (n > 0) { + // If we have any visible characters, add one to the display so + // that the cursor is immediately after the data. + return n + 1; + } + return n; } /** - * Get the on-screen display length. + * Get the raw string that matches this line. * - * @return the number of cells needed to display this line + * @return the string */ - public int getDisplayLength() { - if (displayLength != -1) { - return displayLength; - } - int n = 0; - for (Word word: words) { - n += word.getDisplayLength(); - } - displayLength = n; + public String getRawString() { + return rawText.toString(); + } - // If we have any visible characters, add one to the display so that - // the cursor is immediately after the data. - if (displayLength > 0) { - displayLength++; + /** + * Scan rawText and make words out of it. + */ + private void scanLine() { + words.clear(); + Word word = new Word(this.defaultColor, this.highlighter); + words.add(word); + for (int i = 0; i < rawText.length(); i++) { + char ch = rawText.charAt(i); + Word newWord = word.addChar(ch); + if (newWord != word) { + words.add(newWord); + word = newWord; + } + } + for (Word w: words) { + w.applyHighlight(); } - return displayLength; } /** @@ -140,20 +158,9 @@ public class Line { this.defaultColor = defaultColor; this.highlighter = highlighter; + this.rawText = new StringBuilder(str); - currentWord = new Word(this.defaultColor, this.highlighter); - words.add(currentWord); - for (int i = 0; i < str.length(); i++) { - char ch = str.charAt(i); - Word newWord = currentWord.addChar(ch); - if (newWord != currentWord) { - words.add(newWord); - currentWord = newWord; - } - } - for (Word word: words) { - word.applyHighlight(); - } + scanLine(); } /** @@ -175,7 +182,6 @@ public class Line { if (cursor == 0) { return false; } - // TODO: switch word cursor--; return true; } @@ -192,7 +198,6 @@ public class Line { if (cursor == getDisplayLength() - 1) { return false; } - // TODO: switch word cursor++; return true; } @@ -205,7 +210,6 @@ public class Line { public boolean home() { if (cursor > 0) { cursor = 0; - currentWord = words.get(0); return true; } return false; @@ -222,7 +226,6 @@ public class Line { if (cursor < 0) { cursor = 0; } - currentWord = words.get(words.size() - 1); return true; } return false; @@ -232,14 +235,23 @@ public class Line { * Delete the character under the cursor. */ public void del() { - // TODO + assert (words.size() > 0); + + if (cursor < getDisplayLength()) { + rawText.deleteCharAt(cursor); + } + + // Re-scan the line to determine the new word boundaries. + scanLine(); } /** * Delete the character immediately preceeding the cursor. */ public void backspace() { - // TODO + if (left()) { + del(); + } } /** @@ -248,7 +260,13 @@ public class Line { * @param ch the character to insert */ public void addChar(final char ch) { - // TODO + if (cursor < getDisplayLength() - 1) { + rawText.insert(cursor, ch); + } else { + rawText.append(ch); + } + scanLine(); + cursor++; } /** @@ -257,7 +275,13 @@ public class Line { * @param ch the character to replace */ public void replaceChar(final char ch) { - // TODO + if (cursor < getDisplayLength() - 1) { + rawText.setCharAt(cursor, ch); + } else { + rawText.append(ch); + } + scanLine(); + cursor++; } } diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java index d9b3417..d4532bb 100644 --- a/src/jexer/teditor/Word.java +++ b/src/jexer/teditor/Word.java @@ -165,7 +165,8 @@ public class Word { * Add a character to this word. If this is a whitespace character * adding to a non-whitespace word, create a new word and return that; * similarly if this a non-whitespace character adding to a whitespace - * word, create a new word and return that. + * word, create a new word and return that. Note package private access: + * this is only called by Line to figure out highlighting boundaries. * * @param ch the new character to add * @return either this word (if it was added), or a new word that