-/**
+/*
* Jexer - Java Text User Interface
*
- * License: LGPLv3 or later
- *
- * This module is licensed under the GNU Lesser General Public License
- * Version 3. Please see the file "COPYING" in this directory for more
- * information about the GNU Lesser General Public License Version 3.
+ * The MIT License (MIT)
*
- * Copyright (C) 2015 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
*
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation; either version 3 of
- * the License, or (at your option) any later version.
+ * 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:
*
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
*
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program; if not, see
- * http://www.gnu.org/licenses/, or write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA
+ * 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
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 {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
/**
- * Field text.
+ * Background character for unfilled-in text.
*/
- protected String text = "";
+ protected int backgroundChar = GraphicsChars.HATCH;
/**
- * Get field text.
- *
- * @return field text
+ * Field text.
*/
- public final String getText() {
- return text;
- }
+ protected String text = "";
/**
* If true, only allow enough characters that will fit in the width. If
*/
protected int position = 0;
+ /**
+ * Current editing position screen column number.
+ */
+ protected int screenPosition = 0;
+
/**
* Beginning of visible portion.
*/
*/
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.
*
this.updateAction = updateAction;
}
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Returns true if the mouse is currently on the field.
*
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 cursor position.
- */
- protected void updateCursor() {
- if ((position > getWidth()) && fixed) {
- setCursorX(getWidth());
- } else if ((position - windowStart == getWidth()) && !fixed) {
- setCursorX(getWidth() - 1);
- } else {
- setCursorX(position - windowStart);
- }
- }
-
/**
* Handle mouse button presses.
*
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;
}
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();
return;
}
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;
}
return;
}
if (keypress.equals(kbHome)) {
- position = 0;
- windowStart = 0;
+ home();
return;
}
if (keypress.equals(kbEnd)) {
- position = text.length();
- if (fixed == true) {
- if (position >= getWidth()) {
- position = text.length() - 1;
- }
- } else {
- windowStart = text.length() - getWidth() + 1;
- if (windowStart < 0) {
- windowStart = 0;
- }
- }
+ end();
return;
}
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;
}
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);
+ normalizeWindowStart();
return;
}
) {
// 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)) {
} 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
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++;
}
}
*
* @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++;
}
}
+ /**
+ * Position the cursor at the first column. The field may adjust the
+ * window start to show as much of the field as possible.
+ */
+ public void home() {
+ position = 0;
+ screenPosition = 0;
+ windowStart = 0;
+ }
+
+ /**
+ * Set the editing position to the last filled character. The field may
+ * adjust the window start to show as much of the field as possible.
+ */
+ public void end() {
+ position = text.length();
+ screenPosition = StringUtils.width(text);
+ if (fixed == true) {
+ if (screenPosition >= getWidth()) {
+ position -= Character.charCount(text.codePointBefore(position));
+ screenPosition = StringUtils.width(text) - 1;
+ }
+ } else {
+ windowStart = StringUtils.width(text) - getWidth() + 1;
+ if (windowStart < 0) {
+ windowStart = 0;
+ }
+ }
+ }
+
+ /**
+ * Set the editing position. The field may adjust the window start to
+ * show as much of the field as possible.
+ *
+ * @param position the new position
+ * @throws IndexOutOfBoundsException if position is outside the range of
+ * the available text
+ */
+ public void setPosition(final int position) {
+ if ((position < 0) || (position >= text.length())) {
+ throw new IndexOutOfBoundsException("Max length is " +
+ text.length() + ", requested position " + position);
+ }
+ this.position = position;
+ 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;
+ }
+
}