package org.geogebra.desktop.gui.inputfield;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.DefaultCaret;
import org.geogebra.common.awt.GColor;
import org.geogebra.common.gui.SetLabels;
import org.geogebra.common.gui.VirtualKeyboardListener;
import org.geogebra.common.gui.inputfield.ColorProvider;
import org.geogebra.common.main.GeoGebraColorConstants;
import org.geogebra.common.util.StringUtil;
import org.geogebra.common.util.TextObject;
import org.geogebra.desktop.awt.GColorD;
import org.geogebra.desktop.awt.GGraphics2DD;
import org.geogebra.desktop.gui.GuiManagerD;
import org.geogebra.desktop.gui.util.GeoGebraIconD;
import org.geogebra.desktop.gui.virtualkeyboard.VirtualKeyboardD;
import org.geogebra.desktop.main.AppD;
/**
* Extends JTextField to add (1) dynamic coloring of bracket symbols and quote
* enclosed text and (2) a popup symbol table for inserting special characters.
* The popup is triggered by either an in-line button click or a ctrl-up key
* press.
*
*/
public class MyTextFieldD extends JTextField
implements ActionListener, FocusListener, VirtualKeyboardListener,
CaretListener, SetLabels, TextObject {
private static final long serialVersionUID = 1L;
private AppD app;
// symbol table popup fields
private SymbolTablePopupD tablePopup;
private MyTextFieldD thisField = this;
private ImageIcon icon = GeoGebraIconD
.createSymbolTableIcon(this.getFont());
private boolean showSymbolTableIcon = false;
// colored character rendering fields
boolean caretUpdated = true;
boolean caretShowing = true;
// border button fields
private BorderButtonD borderBtn;
private Border defaultBorder;
private boolean enableColoring = true;
private boolean isColoringLabels;
// matched brackets color = cyan (better contrast than "Light sea green")
private static GColor COLOR_MATCHED = GeoGebraColorConstants.BALANCED_BRACKET_COLOR;
// unmatched brackets color = red
private static GColor COLOR_UNMATCHED = GeoGebraColorConstants.UNBALANCED_BRACKET_COLOR;
// class for distinguishing graphically existing object
private ColorProvider ip;
/************************************
* Construct an instance of MyTextField without a fixed column width
*/
public MyTextFieldD(AppD app) {
super();
this.app = app;
initField();
}
/************************************
* Construct an instance of MyTextField with a fixed column width
*
* @param columns
*/
public MyTextFieldD(AppD app, int columns) {
super(columns);
this.app = app;
initField();
}
/**
* Initializes the field: registers listeners, creates and sets the
* BorderButton
*/
private void initField() {
// The OS X caret contains code that auto-selects text in undesirable
// ways. So we replace the mac caret with a new default caret.
if (app.isMacOS()) {
DefaultCaret c = new DefaultCaret();
int blinkRate = getCaret().getBlinkRate();
c.setBlinkRate(blinkRate);
setCaret(c);
}
setOpaque(true);
addFocusListener(this);
addCaretListener(this);
JTextField dummy = new JTextField();
defaultBorder = dummy.getBorder();
borderBtn = new BorderButtonD(this);
borderBtn.setBorderButton(0, icon, this);
setDefaultBorder();
setOrientation();
}
/**
* returns true if bracket coloring is enabled
*
* @return
*/
public boolean enableColoring() {
return enableColoring;
}
/**
* sets the flag to enable bracket coloring
*
* @param enableColoring
*/
public void enableColoring(boolean enableColoring) {
this.enableColoring = enableColoring;
}
/**
* enables coloring of labels
*
* @param isCasInput
*/
public void enableLabelColoring(boolean isCasInput) {
if (ip == null) {
ip = new ColorProvider(app, isCasInput);
return;
}
ip.setIsCasInput(isCasInput);
}
/**
* @param val
* true if labels should be coloured
*/
public void setColoringLabels(boolean val) {
this.isColoringLabels = val;
}
// ====================================================
// BorderButton
// ====================================================
public BorderButtonD getBorderButton() {
return borderBtn;
}
private void setDefaultBorder() {
super.setBorder(
BorderFactory.createCompoundBorder(defaultBorder, borderBtn));
}
protected void setBorderButton(int index, ImageIcon icon,
ActionListener al) {
borderBtn.setBorderButton(index, icon, al);
setDefaultBorder();
}
protected void setBorderButtonVisible(int index, boolean isVisible) {
borderBtn.setIconVisible(index, isVisible);
setDefaultBorder();
}
protected boolean isBorderButtonVisible(int index) {
return borderBtn.isIconVisible(index);
}
/**
* Overrides <code>setBorder</code> to prevent removal of the BorderButton
*/
@Override
public void setBorder(Border border) {
super.setBorder(BorderFactory.createCompoundBorder(border, borderBtn));
}
// ====================================================
// Event Handlers, Listeners
// ====================================================
boolean selectAllOnFocus = false;
/**
* Sets a flag to force all text to be selected on focus (helpful for tabbed
* data entry)
*
* @param selectAllOnFocus
*/
public void setSelectAllOnFocus(boolean selectAllOnFocus) {
this.selectAllOnFocus = selectAllOnFocus;
}
int oldStyle = Font.PLAIN;
@Override
public void focusGained(FocusEvent e) {
if (selectAllOnFocus) {
thisField.setText(thisField.getText());
thisField.selectAll();
}
if (showSymbolTableIcon && hasFocus()) {
borderBtn.setIconVisible(0, true);
}
thisField.repaint();
if (app.getGuiManager() != null) {
((GuiManagerD) app.getGuiManager()).setCurrentTextfield(this,
false);
}
}
@Override
public void focusLost(FocusEvent e) {
if (showSymbolTableIcon) {
borderBtn.setIconVisible(0, false);
}
thisField.repaint();
if (app.getGuiManager() != null) {
((GuiManagerD) app.getGuiManager()).setCurrentTextfield(null,
!(e.getOppositeComponent() instanceof VirtualKeyboardD));
}
}
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals(0 + BorderButtonD.cmdSuffix)) {
getTablePopup().showPopup(true);
}
}
/**
* Caret update
*/
@Override
public void caretUpdate(CaretEvent e) {
caretUpdated = true;
repaint();
}
/**
* Inserts a string into the text at the current caret position
*/
@Override
public void insertString(String text) {
int start = getSelectionStart();
int end = getSelectionEnd();
// clear selection if there is one
if (start != end) {
String oldText = getText();
StringBuilder sb = new StringBuilder();
sb.append(oldText.substring(0, start));
sb.append(oldText.substring(end));
setText(sb.toString());
setCaretPosition(start);
}
// insert the string
int pos1 = getCaretPosition();
String oldText = getText();
StringBuilder sb = new StringBuilder();
sb.append(oldText.substring(0, pos1));
sb.append(text);
sb.append(oldText.substring(pos1));
setText(sb.toString());
// reset the caret position
setCaretPosition(pos1 + text.length());
// make sure AutoComplete works
if (this instanceof AutoCompleteTextFieldD) {
AutoCompleteTextFieldD tf = (AutoCompleteTextFieldD) this;
tf.updateCurrentWord(false);
tf.startAutoCompletion();
}
}
/**
* Sets a flag to show the symbol table icon when the field is focused
*
* @param showSymbolTableIcon
*/
public void setShowSymbolTableIcon(boolean showSymbolTableIcon) {
this.showSymbolTableIcon = showSymbolTableIcon;
}
public void setOpenSymbolTableUpwards(boolean openUpwards) {
getTablePopup().setOpenUpwards(openUpwards);
}
private SymbolTablePopupD getTablePopup() {
if (tablePopup == null) {
tablePopup = new SymbolTablePopupD(app, this);
}
return tablePopup;
}
/**
* Overrides processKeyEvents so that the symbol table popup can be
* triggered by ctrl-up.
*/
@Override
public void processKeyEvent(KeyEvent e) {
int keyCode = e.getKeyCode();
if ((e.isControlDown() || AppD.isControlDown(e))
&& keyCode == KeyEvent.VK_UP) {
getTablePopup().showPopup(false);
return;
}
super.processKeyEvent(e);
}
@Override
public void setLabels() {
if (tablePopup != null) {
tablePopup.setLabels();
}
}
public void setOrientation() {
app.setComponentOrientation(this);
}
// fields for custom painting
private float pos = 0; // start position of text (not pixel location)
private int caret; // caret position
private int scrollOffset = 0;
private int width = 0, height = 0, textBottom, fontHeight;
private FontRenderContext frc;
private Font font;
private Graphics2D g2;
private Insets insets;
@Override
public void paintComponent(Graphics gr) {
g2 = (Graphics2D) gr;
super.paintComponent(g2);
if (!enableColoring || !this.hasFocus()) {
return;
}
// ===============================================================
// prepare for custom drawing
GGraphics2DD.setAntialiasing(g2);
String text = getText();
// get font info
fontHeight = g2.getFontMetrics().getHeight();
textBottom = (getHeight() - fontHeight) / 2 + fontHeight - 4;
frc = g2.getFontRenderContext();
font = g2.getFont();
// get textField dimensions
insets = getInsets();
width = getWidth() - insets.right - insets.left;
height = getHeight() - insets.top - insets.bottom;
// get text position information
scrollOffset = getScrollOffset();
pos = 0; // text start position (not in pixels)
if (getHorizontalAlignment() == SwingConstants.RIGHT) {
pos = Math.max(0,
getHorizontalVisibility().getExtent() - getLength(text));
}
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
// get caret position information
caret = getCaretPosition();
float caretPos = -1;
if (caret == 0) {
caretPos = pos;
}
// get the bracket positions
String text2 = StringUtil.ignoreIndices(text);
int[] brkPos = org.geogebra.common.gui.inputfield.MyTextField
.getBracketPositions(text2, caret);
int wrong = StringUtil.checkBracketsBackward(text2);
int bracket1pos = brkPos[0];
int bracket2pos = brkPos[1];
// ===============================================================
// perform custom drawing
//
// NOTE: using setClip was disabled because it causes the field to bleed
// outside of bounds in some layouts
// g2.setClip(insets.left, insets.top, width, height);
// hide previously drawn text with a white rectangle
g2.setColor(Color.WHITE);
g2.fillRect(insets.left, insets.top, width, height);
// set the text for checking labels
if (ip != null && isColoringLabels) {
ip.setText(text);
}
// redraw the text using color
boolean textMode = false;
for (int i = 0; i < text.length(); i++) {
GColor fg = null;
// determine the color
if (text.charAt(i) == '\"') {
textMode = !textMode;
}
if (i == wrong)
{
fg = COLOR_UNMATCHED; // unmatched bracket
}
if (i == bracket1pos || i == bracket2pos) {
if (bracket2pos > -1) {
fg = COLOR_MATCHED; // matched bracket
} else {
fg = COLOR_UNMATCHED; // unmatched bracket
}
}
if (fg == null) {
if (textMode || text.charAt(i) == '\"') {
fg = GeoGebraColorConstants.INPUT_TEXT_COLOR;
} else if (ip != null && isColoringLabels) {
fg = ip.getColor(i);
// g2.setColor(GColorD.getAwtColor(ip.getColor(i)));
} else {
fg = GeoGebraColorConstants.INPUT_DEFAULT_COLOR;
}
}
if (fg != null) {
g2.setColor(GColorD.getAwtColor(fg));
}
// now draw the text
drawText(text.charAt(i) + "", i >= selStart && i < selEnd);
if (i + 1 == caret) {
caretPos = pos;
}
}
// draw caret if there's been no caret movement since last repaint
if (caretUpdated) {
caretShowing = false;
} else {
caretShowing = !caretShowing;
}
caretUpdated = false;
if (caretShowing && caretPos > -1 && hasFocus()) {
g2.setColor(Color.black);
g2.fillRect((int) caretPos - scrollOffset + insets.left,
textBottom - fontHeight + 4, 1, fontHeight);
g2.setPaintMode();
}
}
private float getLength(String text) {
if (text == null || text.length() == 0) {
return 0;
}
TextLayout layout = new TextLayout(text, font, frc);
return layout.getAdvance();
}
private void drawText(String str, boolean selected/* , Color bg */) {
if ("".equals(str)) {
return;
}
// compute advance
FontMetrics metrics = g2.getFontMetrics(font);
float advance = metrics.stringWidth(str);
if (selected) {
g2.setColor(getSelectionColor());
g2.fillRect((int) pos - scrollOffset + insets.left,
textBottom - fontHeight + 4, (int) advance, fontHeight);
g2.setColor(getSelectedTextColor());
}
// there is no background coloring now
/*
* if (bg != null) { Color col = g2.getColor(); g2.setColor(bg);
* g2.fillRect((int) pos - scrollOffset + insets.left, textBottom -
* fontHeight + 4, (int) advance, fontHeight); g2.setColor(col); }
*/
// g2.setClip(0, 0, width, height);
if (pos - scrollOffset + insets.left >= 0
&& pos + advance - scrollOffset <= width) {
g2.drawString(str, pos - scrollOffset + insets.left, textBottom);
}
pos += advance;
}
@Override
public void paste() {
super.paste();
String text = getText();
// make sure <TAB> can't get pasted into Input Bar
if (text.indexOf('\t') > -1) {
int pos2 = getCaretPosition();
setText(text.replace('\t', ' '));
setCaretPosition(pos2);
}
}
@Override
public void wrapSetText(final String s) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setText(s);
}
});
}
}