*
* 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"),
import jexer.bits.CellAttributes;
import jexer.bits.GraphicsChars;
+import jexer.bits.StringUtils;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
import static jexer.TKeypress.*;
*/
public class TField extends TWidget {
- /**
- * 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
*/
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 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.
*
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) {
+ if (position < text.length()) {
+ screenPosition -= StringUtils.width(text.charAt(position));
+ } else {
+ screenPosition--;
+ }
position--;
}
if (fixed == false) {
- if ((position == windowStart) && (windowStart > 0)) {
- windowStart--;
+ if ((screenPosition == windowStart) && (windowStart > 0)) {
+ windowStart -= StringUtils.width(text.charAt(
+ screenToTextPosition(windowStart)));
}
}
normalizeWindowStart();
if (keypress.equals(kbRight)) {
if (position < text.length()) {
+ screenPosition += StringUtils.width(text.charAt(position));
position++;
if (fixed == true) {
- if (position == getWidth()) {
+ if (screenPosition == getWidth()) {
+ screenPosition--;
position--;
}
} else {
- if ((position - windowStart) == getWidth()) {
- windowStart++;
+ if ((screenPosition - windowStart) >= getWidth()) {
+ windowStart += StringUtils.width(text.charAt(
+ screenToTextPosition(windowStart)));
}
}
}
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;
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.charAt(
+ screenToTextPosition(windowStart)));
}
}
dispatch(false);
) {
// 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) {
text = text.substring(0, position)
+ keypress.getKey().getChar()
+ text.substring(position + 1);
+ screenPosition += StringUtils.width(text.charAt(position));
position++;
} 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)) {
text = text.substring(0, position)
+ keypress.getKey().getChar()
+ text.substring(position + 1);
- if (position < getWidth() - 1) {
+ if (screenPosition < getWidth() - 1) {
+ screenPosition += StringUtils.width(text.charAt(position));
position++;
}
} else if ((fixed == false) && (insertMode == false)) {
text = text.substring(0, position)
+ keypress.getKey().getChar()
+ text.substring(position + 1);
+ screenPosition += StringUtils.width(text.charAt(position));
position++;
} else {
if (position == text.length()) {
super.onKeypress(keypress);
}
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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 -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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;
+ windowStart = 0;
+ }
+
+ /**
+ * 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();
+ }
+ }
+ }
+
+ /**
+ * 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.charAt(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++;
+ screenPosition += StringUtils.width(ch);
assert (position == text.length());
if (fixed) {
- if (position == getWidth()) {
+ if (screenPosition >= getWidth()) {
position--;
+ 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);
+ protected void insertChar(final int ch) {
+ text = text.substring(0, position) + ((char) ch) + text.substring(position);
position++;
- if ((position - windowStart) == getWidth()) {
+ 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 = text.length() - 1;
+ 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 " +
+ StringUtils.width(text) + ", 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;
+ }
+
}