X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTField.java;h=90dd4e427abcf5660bfcc04b32b76c7c5da8edf7;hb=HEAD;hp=c93cecbcbf97670babe1d07d26508498467a3030;hpb=1b1206070bb94d706c14400b8332a81646b77a25;p=fanfix.git diff --git a/src/jexer/TField.java b/src/jexer/TField.java index c93cecb..90dd4e4 100644 --- a/src/jexer/TField.java +++ b/src/jexer/TField.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -30,39 +30,31 @@ package jexer; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; +import jexer.bits.StringUtils; +import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; +import static jexer.TCommand.*; import static jexer.TKeypress.*; /** * TField implements an editable text field. */ -public class TField extends TWidget { +public class TField extends TWidget implements EditMenuUser { - /** - * Field text. - */ - protected String text = ""; + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Get field text. - * - * @return field text + * Background character for unfilled-in text. */ - public final String getText() { - return text; - } + protected int backgroundChar = GraphicsChars.HATCH; /** - * Set field text. - * - * @param text the new field text + * Field text. */ - public final void setText(String text) { - this.text = text; - position = 0; - windowStart = 0; - } + protected String text = ""; /** * If true, only allow enough characters that will fit in the width. If @@ -75,6 +67,11 @@ public class TField extends TWidget { */ protected int position = 0; + /** + * Current editing position screen column number. + */ + protected int screenPosition = 0; + /** * Beginning of visible portion. */ @@ -100,6 +97,20 @@ public class TField extends TWidget { */ protected TAction updateAction; + /** + * The color to use when this field is active. + */ + private String activeColorKey = "tfield.active"; + + /** + * The color to use when this field is not active. + */ + private String inactiveColorKey = "tfield.inactive"; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor. * @@ -157,6 +168,10 @@ public class TField extends TWidget { this.updateAction = updateAction; } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Returns true if the mouse is currently on the field. * @@ -174,80 +189,6 @@ public class TField extends TWidget { return false; } - /** - * Dispatch to the action function. - * - * @param enter if true, the user pressed Enter, else this was an update - * to the text. - */ - protected void dispatch(final boolean enter) { - if (enter) { - if (enterAction != null) { - enterAction.DO(); - } - } else { - if (updateAction != null) { - updateAction.DO(); - } - } - } - - /** - * Draw the text field. - */ - @Override - public void draw() { - CellAttributes fieldColor; - - if (isAbsoluteActive()) { - fieldColor = getTheme().getColor("tfield.active"); - } else { - fieldColor = getTheme().getColor("tfield.inactive"); - } - - int end = windowStart + getWidth(); - if (end > text.length()) { - end = text.length(); - } - getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); - getScreen().putStringXY(0, 0, text.substring(windowStart, end), - fieldColor); - - // Fix the cursor, it will be rendered by TApplication.drawAll(). - updateCursor(); - } - - /** - * Update the visible cursor position to match the location of position - * and windowStart. - */ - protected void updateCursor() { - if ((position > getWidth()) && fixed) { - setCursorX(getWidth()); - } else if ((position - windowStart == getWidth()) && !fixed) { - setCursorX(getWidth() - 1); - } else { - setCursorX(position - windowStart); - } - } - - /** - * Normalize windowStart such that most of the field data if visible. - */ - protected void normalizeWindowStart() { - if (fixed) { - // windowStart had better be zero, there is nothing to do here. - assert (windowStart == 0); - return; - } - windowStart = position - (getWidth() - 1); - if (windowStart < 0) { - windowStart = 0; - } - - updateCursor(); - } - /** * Handle mouse button presses. * @@ -260,10 +201,11 @@ public class TField extends TWidget { if ((mouseOnField()) && (mouse.isMouse1())) { // Move cursor int deltaX = mouse.getX() - getCursorX(); - position += deltaX; - if (position > text.length()) { - position = text.length(); + screenPosition += deltaX; + if (screenPosition > StringUtils.width(text)) { + screenPosition = StringUtils.width(text); } + position = screenToTextPosition(screenPosition); updateCursor(); return; } @@ -279,11 +221,13 @@ public class TField extends TWidget { if (keypress.equals(kbLeft)) { if (position > 0) { - position--; + screenPosition -= StringUtils.width(text.codePointBefore(position)); + position -= Character.charCount(text.codePointBefore(position)); } if (fixed == false) { - if ((position == windowStart) && (windowStart > 0)) { - windowStart--; + if ((screenPosition == windowStart) && (windowStart > 0)) { + windowStart -= StringUtils.width(text.codePointAt( + screenToTextPosition(windowStart))); } } normalizeWindowStart(); @@ -292,17 +236,25 @@ public class TField extends TWidget { if (keypress.equals(kbRight)) { if (position < text.length()) { - position++; + int lastPosition = position; + screenPosition += StringUtils.width(text.codePointAt(position)); + position += Character.charCount(text.codePointAt(position)); if (fixed == true) { - if (position == getWidth()) { - position--; + if (screenPosition == getWidth()) { + screenPosition--; + position -= Character.charCount(text.codePointAt(lastPosition)); } } else { - if ((position - windowStart) == getWidth()) { - windowStart++; + while ((screenPosition - windowStart + + StringUtils.width(text.codePointAt(text.length() - 1))) + > getWidth() + ) { + windowStart += StringUtils.width(text.codePointAt( + screenToTextPosition(windowStart))); } } } + assert (position <= text.length()); return; } @@ -329,6 +281,7 @@ public class TField extends TWidget { if ((text.length() > 0) && (position < text.length())) { text = text.substring(0, position) + text.substring(position + 1); + screenPosition = StringUtils.width(text.substring(0, position)); } dispatch(false); return; @@ -336,15 +289,17 @@ public class TField extends TWidget { if (keypress.equals(kbBackspace) || keypress.equals(kbBackspaceDel)) { if (position > 0) { - position--; + position -= Character.charCount(text.codePointBefore(position)); text = text.substring(0, position) + text.substring(position + 1); + screenPosition = StringUtils.width(text.substring(0, position)); } if (fixed == false) { - if ((position == windowStart) + if ((screenPosition >= windowStart) && (windowStart > 0) ) { - windowStart--; + windowStart -= StringUtils.width(text.codePointAt( + screenToTextPosition(windowStart))); } } dispatch(false); @@ -358,26 +313,27 @@ public class TField extends TWidget { ) { // Plain old keystroke, process it if ((position == text.length()) - && (text.length() < getWidth())) { + && (StringUtils.width(text) < getWidth())) { // Append case appendChar(keypress.getKey().getChar()); } else if ((position < text.length()) - && (text.length() < getWidth())) { + && (StringUtils.width(text) < getWidth())) { // Overwrite or insert a character if (insertMode == false) { // Replace character text = text.substring(0, position) - + keypress.getKey().getChar() + + codePointString(keypress.getKey().getChar()) + text.substring(position + 1); - position++; + screenPosition += StringUtils.width(text.codePointAt(position)); + position += Character.charCount(keypress.getKey().getChar()); } else { // Insert character insertChar(keypress.getKey().getChar()); } } else if ((position < text.length()) - && (text.length() >= getWidth())) { + && (StringUtils.width(text) >= getWidth())) { // Multiple cases here if ((fixed == true) && (insertMode == true)) { @@ -385,17 +341,19 @@ public class TField extends TWidget { } else if ((fixed == true) && (insertMode == false)) { // Overwrite the last character, maybe move position text = text.substring(0, position) - + keypress.getKey().getChar() + + codePointString(keypress.getKey().getChar()) + text.substring(position + 1); - if (position < getWidth() - 1) { - position++; + if (screenPosition < getWidth() - 1) { + screenPosition += StringUtils.width(text.codePointAt(position)); + position += Character.charCount(keypress.getKey().getChar()); } } else if ((fixed == false) && (insertMode == false)) { // Overwrite the last character, definitely move position text = text.substring(0, position) - + keypress.getKey().getChar() + + codePointString(keypress.getKey().getChar()) + text.substring(position + 1); - position++; + screenPosition += StringUtils.width(text.codePointAt(position)); + position += Character.charCount(keypress.getKey().getChar()); } else { if (position == text.length()) { // Append this character @@ -419,24 +377,235 @@ public class TField extends TWidget { super.onKeypress(keypress); } + /** + * Handle posted command events. + * + * @param command command event + */ + @Override + public void onCommand(final TCommandEvent command) { + if (command.equals(cmCut)) { + // Copy text to clipboard, and then remove it. + getClipboard().copyText(text); + setText(""); + return; + } + + if (command.equals(cmCopy)) { + // Copy text to clipboard. + getClipboard().copyText(text); + return; + } + + if (command.equals(cmPaste)) { + // Paste text from clipboard. + String newText = getClipboard().pasteText(); + if (newText != null) { + setText(newText); + } + return; + } + + if (command.equals(cmClear)) { + // Remove text. + setText(""); + return; + } + + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Override TWidget's height: we can only set height at construction + * time. + * + * @param height new widget height (ignored) + */ + @Override + public void setHeight(final int height) { + // Do nothing + } + + /** + * Draw the text field. + */ + @Override + public void draw() { + CellAttributes fieldColor; + + if (isAbsoluteActive()) { + fieldColor = getTheme().getColor(activeColorKey); + } else { + fieldColor = getTheme().getColor(inactiveColorKey); + } + + int end = windowStart + getWidth(); + if (end > StringUtils.width(text)) { + end = StringUtils.width(text); + } + hLineXY(0, 0, getWidth(), backgroundChar, fieldColor); + putStringXY(0, 0, text.substring(screenToTextPosition(windowStart), + screenToTextPosition(end)), fieldColor); + + // Fix the cursor, it will be rendered by TApplication.drawAll(). + updateCursor(); + } + + // ------------------------------------------------------------------------ + // TField ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Convert a char (codepoint) to a string. + * + * @param ch the char + * @return the string + */ + private String codePointString(final int ch) { + StringBuilder sb = new StringBuilder(1); + sb.append(Character.toChars(ch)); + assert (Character.charCount(ch) == sb.length()); + return sb.toString(); + } + + /** + * Get field background character. + * + * @return background character + */ + public final int getBackgroundChar() { + return backgroundChar; + } + + /** + * Set field background character. + * + * @param backgroundChar the background character + */ + public void setBackgroundChar(final int backgroundChar) { + this.backgroundChar = backgroundChar; + } + + /** + * Get field text. + * + * @return field text + */ + public final String getText() { + return text; + } + + /** + * Set field text. + * + * @param text the new field text + */ + public void setText(final String text) { + assert (text != null); + this.text = text; + position = 0; + screenPosition = 0; + windowStart = 0; + if ((fixed == true) && (this.text.length() > getWidth())) { + this.text = this.text.substring(0, getWidth()); + } + } + + /** + * Dispatch to the action function. + * + * @param enter if true, the user pressed Enter, else this was an update + * to the text. + */ + protected void dispatch(final boolean enter) { + if (enter) { + if (enterAction != null) { + enterAction.DO(this); + } + } else { + if (updateAction != null) { + updateAction.DO(this); + } + } + } + + /** + * Determine string position from screen position. + * + * @param screenPosition the position on screen + * @return the equivalent position in text + */ + protected int screenToTextPosition(final int screenPosition) { + if (screenPosition == 0) { + return 0; + } + + int n = 0; + for (int i = 0; i < text.length(); i++) { + n += StringUtils.width(text.codePointAt(i)); + if (n >= screenPosition) { + return i + 1; + } + } + // screenPosition exceeds the available text length. + throw new IndexOutOfBoundsException("screenPosition " + screenPosition + + " exceeds available text length " + text.length()); + } + + /** + * Update the visible cursor position to match the location of position + * and windowStart. + */ + protected void updateCursor() { + if ((screenPosition > getWidth()) && fixed) { + setCursorX(getWidth()); + } else if ((screenPosition - windowStart >= getWidth()) && !fixed) { + setCursorX(getWidth() - 1); + } else { + setCursorX(screenPosition - windowStart); + } + } + + /** + * Normalize windowStart such that most of the field data if visible. + */ + protected void normalizeWindowStart() { + if (fixed) { + // windowStart had better be zero, there is nothing to do here. + assert (windowStart == 0); + return; + } + windowStart = screenPosition - (getWidth() - 1); + if (windowStart < 0) { + windowStart = 0; + } + + updateCursor(); + } + /** * Append char to the end of the field. * - * @param ch = char to append + * @param ch char to append */ - protected void appendChar(final char ch) { + protected void appendChar(final int ch) { // Append the LAST character - text += ch; - position++; + text += codePointString(ch); + position += Character.charCount(ch); + screenPosition += StringUtils.width(ch); assert (position == text.length()); if (fixed) { - if (position == getWidth()) { - position--; + if (screenPosition >= getWidth()) { + position -= Character.charCount(ch); + screenPosition -= StringUtils.width(ch); } } else { - if ((position - windowStart) == getWidth()) { + if ((screenPosition - windowStart) >= getWidth()) { windowStart++; } } @@ -447,10 +616,12 @@ public class TField extends TWidget { * * @param ch char to append */ - protected void insertChar(final char ch) { - text = text.substring(0, position) + ch + text.substring(position); - position++; - if ((position - windowStart) == getWidth()) { + protected void insertChar(final int ch) { + text = text.substring(0, position) + codePointString(ch) + + text.substring(position); + position += Character.charCount(ch); + screenPosition += StringUtils.width(ch); + if ((screenPosition - windowStart) == getWidth()) { assert (!fixed); windowStart++; } @@ -462,6 +633,7 @@ public class TField extends TWidget { */ public void home() { position = 0; + screenPosition = 0; windowStart = 0; } @@ -471,12 +643,14 @@ public class TField extends TWidget { */ public void end() { position = text.length(); + screenPosition = StringUtils.width(text); if (fixed == true) { - if (position >= getWidth()) { - position = text.length() - 1; - } + if (screenPosition >= getWidth()) { + position -= Character.charCount(text.codePointBefore(position)); + screenPosition = StringUtils.width(text) - 1; + } } else { - windowStart = text.length() - getWidth() + 1; + windowStart = StringUtils.width(text) - getWidth() + 1; if (windowStart < 0) { windowStart = 0; } @@ -500,4 +674,82 @@ public class TField extends TWidget { normalizeWindowStart(); } + /** + * Set the active color key. + * + * @param activeColorKey ColorTheme key color to use when this field is + * active + */ + public void setActiveColorKey(final String activeColorKey) { + this.activeColorKey = activeColorKey; + } + + /** + * Set the inactive color key. + * + * @param inactiveColorKey ColorTheme key color to use when this field is + * inactive + */ + public void setInactiveColorKey(final String inactiveColorKey) { + this.inactiveColorKey = inactiveColorKey; + } + + /** + * Set the action to perform when the user presses enter. + * + * @param action the action to perform when the user presses enter + */ + public void setEnterAction(final TAction action) { + enterAction = action; + } + + /** + * Set the action to perform when the field is updated. + * + * @param action the action to perform when the field is updated + */ + public void setUpdateAction(final TAction action) { + updateAction = action; + } + + // ------------------------------------------------------------------------ + // EditMenuUser ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut() { + return true; + } + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy() { + return true; + } + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste() { + return true; + } + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear() { + return true; + } + }