package org.geogebra.desktop.gui;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTextPane;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyleConstants;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.algos.AlgoDependentText;
import org.geogebra.common.kernel.arithmetic.ExpressionNode;
import org.geogebra.common.kernel.arithmetic.ExpressionValue;
import org.geogebra.common.kernel.arithmetic.MyStringBuffer;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoText;
import org.geogebra.common.util.StringUtil;
import org.geogebra.common.util.lang.Unicode;
import org.geogebra.desktop.gui.dialog.TextInputDialogD;
import org.geogebra.desktop.gui.inputfield.MyTextFieldD;
import org.geogebra.desktop.main.AppD;
/**
* Extended JTextPane for editing GeoText strings. Uses embedded text fields
* (inner class DynamicTextField) to handle object references in a GeoText
* string.
*
* @author G. Sturr
*
*/
public class DynamicTextInputPane extends JTextPane implements FocusListener {
private static final long serialVersionUID = 1L;
/** application */
AppD app;
protected final DynamicTextInputPane thisPane;
/** doc */
public DefaultStyledDocument doc;
private JTextComponent focusedTextComponent;
/**************************************
* Constructs a DynamicTextInputPane
*
* @param app
* app
*/
public DynamicTextInputPane(AppD app) {
super();
this.app = app;
thisPane = this;
setBackground(Color.white);
doc = (DefaultStyledDocument) this.getDocument();
this.addKeyListener(new GeoGebraKeys());
this.addFocusListener(this);
focusedTextComponent = this;
// this.setCaret(new MyCaret());
}
@Override
public void replaceSelection(String content) {
if (focusedTextComponent == this) {
super.replaceSelection(content);
} else {
focusedTextComponent.replaceSelection(content);
}
}
/**
* @return focusedTextComponent
*/
public JTextComponent getFocusedTextComponent() {
return focusedTextComponent;
}
@Override
public void focusGained(FocusEvent e) {
if (e.getSource() instanceof JTextComponent) {
focusedTextComponent = (JTextComponent) e.getSource();
}
}
@Override
public void focusLost(FocusEvent e) {
// TODO Auto-generated method stub
}
/**
* Inserts dynamic text field at the current caret position and returns the
* text field's document
*
* @param text
* text to put in the dynamic field
* @param inputDialog
* input dialog
* @return dynamic text field
*/
public DynamicTextField insertDynamicText(String text,
TextInputDialogD inputDialog) {
return insertDynamicText(text, this.getCaretPosition(), inputDialog);
}
/**
* Inserts dynamic text field at a specified position and returns the text
* field's document
*
* @param text0
* text to put in the dynamic field
* @param pos0
* position of the dynamic text field
* @param inputDialog
* input dialog
* @return dynamic text field
*/
public DynamicTextField insertDynamicText(String text0, int pos0,
TextInputDialogD inputDialog) {
String text = text0;
int pos = pos0;
if (pos == -1) {
pos = getDocument().getLength(); // insert at end
}
int mode = DynamicTextField.MODE_VALUE;
String s;
if (text.endsWith("]")) {
if (text.startsWith(
s = app.getLocalization().getCommand("LaTeX") + "[")) {
// strip off outer command
String temp = text.substring(s.length(), text.length() - 1);
// check for second argument in LaTeX[str, false]
int commaIndex = temp.lastIndexOf(',');
int bracketCount = 0;
for (int i = commaIndex + 1; i < temp.length(); i++) {
if (temp.charAt(i) == '[') {
bracketCount++;
} else if (temp.charAt(i) == ']') {
bracketCount--;
}
}
if (bracketCount != 0 || commaIndex == -1) {
// no second argument
text = temp;
mode = DynamicTextField.MODE_FORMULATEXT;
}
} else if (text.startsWith(
s = app.getLocalization().getCommand("Name") + "[")) {
// strip off outer command
text = text.substring(s.length(), text.length() - 1);
mode = DynamicTextField.MODE_DEFINITION;
}
}
DynamicTextField tf = new DynamicTextField(app, inputDialog);
tf.setText(text);
tf.setMode(mode);
tf.addFocusListener(this);
// insert the text field into the text pane
setCaretPosition(pos);
insertComponent(tf);
return tf;
}
/**
* Converts the current editor content into a GeoText string.
*
* @param latex
* boolean
* @return String to convert to GeoText eg "value is "+a
*/
public String buildGeoGebraString(boolean latex) {
char currentQuote = Unicode.OPEN_DOUBLE_QUOTE;
StringBuilder sb = new StringBuilder();
Element elem;
for (int i = 0; i < doc.getLength(); i++) {
try {
elem = doc.getCharacterElement(i);
if (elem.getName().equals("component")) {
DynamicTextField tf = (DynamicTextField) StyleConstants
.getComponent(elem.getAttributes());
if (tf.getMode() == DynamicTextField.MODE_DEFINITION) {
sb.append("\"+");
sb.append("Name[");
sb.append(tf.getText());
sb.append(']');
sb.append("+\"");
} else if (latex || tf
.getMode() == DynamicTextField.MODE_FORMULATEXT) {
sb.append("\"+");
sb.append("LaTeX["); // internal name for FormulaText[ ]
sb.append(tf.getText());
sb.append(']');
sb.append("+\"");
} else {
// tf.getMode() == DynamicTextField.MODE_VALUE
// brackets needed for eg "hello"+(a+3)
sb.append("\"+(");
sb.append(tf.getText());
sb.append(")+\"");
}
} else if (elem.getName().equals("content")) {
String content = doc.getText(i, 1);
currentQuote = StringUtil.processQuotes(sb, content,
currentQuote);
}
} catch (BadLocationException e) {
e.printStackTrace();
}
}
// add quotes at start and end so it parses to a text
sb.insert(0, '"');
sb.append('"');
return sb.toString();
}
/**
* Builds and sets editor content to correspond with the text string of a
* GeoText
*
* @param geo
* GeoText
* @param id
* id
*/
public void setText(GeoText geo, TextInputDialogD id) {
super.setText("");
if (geo == null) {
return;
}
if (geo.isIndependent()) {
super.setText(geo.getTextString());
return;
}
// if dependent text then get the root
ExpressionNode root = ((AlgoDependentText) geo.getParentAlgorithm())
.getRoot();
// parse the root and set the text content
this.splitString(root, id);
}
/**
* @param en
* en
* @param id
* id
*/
public void splitString(ExpressionNode en, TextInputDialogD id) {
ExpressionValue left = en.getLeft();
ExpressionValue right = en.getRight();
StringTemplate tpl = StringTemplate.defaultTemplate;
if (en.isLeaf()) {
if (left.isGeoElement()) {
DynamicTextField d = insertDynamicText(
((GeoElement) left).getLabel(tpl), -1, id);
d.getDocument().addDocumentListener(id);
} else if (left.isExpressionNode()) {
splitString((ExpressionNode) left, id);
} else if (left instanceof MyStringBuffer) {
insertString(-1, left.toString(tpl).replaceAll("\"", ""), null);
} else {
insertDynamicText(left.toString(tpl), -1, id);
}
}
// STANDARD case: no leaf
else {
if (right != null && !en.containsMyStringBuffer()) {
// neither left nor right are free texts, eg a+3 in
// (a+3)+"hello"
// so no splitting needed
insertDynamicText(en.toString(tpl), -1, id);
return;
}
// expression node
if (left.isGeoElement()) {
DynamicTextField d = insertDynamicText(
((GeoElement) left).getLabel(tpl), -1, id);
d.getDocument().addDocumentListener(id);
} else if (left.isExpressionNode()) {
this.splitString((ExpressionNode) left, id);
} else if (left instanceof MyStringBuffer) {
insertString(-1, left.toString(tpl).replaceAll("\"", ""), null);
} else {
insertDynamicText(left.toString(tpl), -1, id);
}
if (right != null) {
if (right.isGeoElement()) {
DynamicTextField d = insertDynamicText(
((GeoElement) right).getLabel(tpl), -1, id);
d.getDocument().addDocumentListener(id);
} else if (right.isExpressionNode()) {
this.splitString((ExpressionNode) right, id);
} else if (right instanceof MyStringBuffer) {
insertString(-1, right.toString(tpl).replaceAll("\"", ""),
null);
} else {
insertDynamicText(right.toString(tpl), -1, id);
}
}
}
}
/**
* Overrides insertString to allow option offs = -1 for inserting at end.
*
* @param offs0
* offset
* @param str
* string to insert
* @param a
* attributes
*/
public void insertString(int offs0, String str, AttributeSet a) {
try {
int offs = offs0;
if (offs == -1) {
offs = doc.getLength(); // insert at end
}
doc.insertString(offs, str, a);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
/**
* Custom caret with damage area set to a thin width. This allows the caret
* to appear next to a DynamicTextField without destroying the field's
* border.
*/
static class MyCaret extends DefaultCaret {
private static final long serialVersionUID = 1L;
/**
*
*/
public MyCaret() {
super();
this.setBlinkRate(500);
}
@Override
protected synchronized void damage(Rectangle r) {
if (r == null) {
return;
}
x = r.x;
y = r.y;
width = 4;
height = r.height;
repaint();
}
}
/*********************************************************************
* Class for the dynamic text container.
*
*/
@SuppressWarnings("javadoc")
public class DynamicTextField extends MyTextFieldD {
private static final long serialVersionUID = 1L;
public static final int MODE_VALUE = 0;
public static final int MODE_DEFINITION = 1;
public static final int MODE_FORMULATEXT = 2;
int mode = MODE_VALUE;
TextInputDialogD id;
JPopupMenu contextMenu;
/**
* @param app
* @param id
*/
public DynamicTextField(AppD app, TextInputDialogD id) {
super(app);
this.id = id;
// see ticket #1339
this.enableColoring(false);
// handle alt+arrow to exit the field
addKeyListener(new MyKeyListener(this));
// add a mouse listener to trigger the context menu
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent evt) {
if (evt.isPopupTrigger()) {
createContextMenu();
contextMenu.show(evt.getComponent(), evt.getX(),
evt.getY());
}
}
@Override
public void mouseReleased(MouseEvent evt) {
if (evt.isPopupTrigger()) {
createContextMenu();
contextMenu.show(evt.getComponent(), evt.getX(),
evt.getY());
}
}
});
// special transparent border to show caret when next to the
// component
// setOpaque(false);
// setBorder(new CompoundBorder(new LineBorder(new Color(0, 0, 0,
// 0),
// 1), getBorder()));
// make sure the field is aligned nicely in the text pane
Font f = thisPane.getFont();
setFont(f);
FontMetrics fm = getFontMetrics(f);
int maxAscent = fm.getMaxAscent();
int height = (int) getPreferredSize().getHeight();
int borderHeight = getBorder().getBorderInsets(this).top;
int aboveBaseline = maxAscent + borderHeight;
float alignmentY = (float) (aboveBaseline) / ((float) (height));
setAlignmentY(alignmentY);
// document listener to update enclosing text pane
getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent e) {
// do nothing
}
@Override
public void insertUpdate(DocumentEvent e) {
thisPane.revalidate();
thisPane.repaint();
}
@Override
public void removeUpdate(DocumentEvent e) {
thisPane.revalidate();
thisPane.repaint();
}
});
// document listener for input dialog (updates preview pane)
getDocument().addDocumentListener(id);
}
@Override
public Dimension getMaximumSize() {
return this.getPreferredSize();
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
private class MyKeyListener extends KeyAdapter {
private DynamicTextField tf;
public MyKeyListener(DynamicTextField tf) {
this.tf = tf;
}
@Override
public void keyPressed(KeyEvent e) {
if ((e.isAltDown() || AppD.isAltDown(e))) {
switch (e.getKeyCode()) {
default:
// do nothing
break;
case KeyEvent.VK_LEFT:
id.exitTextField(tf, true);
break;
case KeyEvent.VK_RIGHT:
id.exitTextField(tf, false);
break;
}
}
}
}
void createContextMenu() {
contextMenu = new JPopupMenu();
JCheckBoxMenuItem item = new JCheckBoxMenuItem(
app.getLocalization().getMenu("Value"));
item.setSelected(mode == MODE_VALUE);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
mode = MODE_VALUE;
id.handleDocumentEvent();
}
});
contextMenu.add(item);
item = new JCheckBoxMenuItem(
app.getLocalization().getMenu("Definition"));
item.setSelected(mode == MODE_DEFINITION);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
mode = MODE_DEFINITION;
id.handleDocumentEvent();
}
});
contextMenu.add(item);
/*
* item = new JCheckBoxMenuItem(app.getMenu("Formula"));
* item.setSelected(mode == MODE_FORMULATEXT);
* item.addActionListener(new ActionListener(){ public void
* actionPerformed(ActionEvent arg0) { mode = MODE_FORMULATEXT; }
* });
*/
contextMenu.add(item);
app.setComponentOrientation(contextMenu);
}
}
}