package com.himamis.retex.editor.android;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import com.himamis.retex.editor.android.event.ClickListenerAdapter;
import com.himamis.retex.editor.android.event.FocusListenerAdapter;
import com.himamis.retex.editor.android.event.KeyListenerAdapter;
import com.himamis.retex.editor.share.controller.EditorState;
import com.himamis.retex.editor.share.controller.InputController;
import com.himamis.retex.editor.share.editor.MathField;
import com.himamis.retex.editor.share.editor.MathFieldInternal;
import com.himamis.retex.editor.share.event.ClickListener;
import com.himamis.retex.editor.share.event.FocusListener;
import com.himamis.retex.editor.share.event.KeyListener;
import com.himamis.retex.editor.share.meta.MetaModel;
import com.himamis.retex.editor.share.model.MathComponent;
import com.himamis.retex.editor.share.model.MathContainer;
import com.himamis.retex.editor.share.model.MathFormula;
import com.himamis.retex.editor.share.model.MathSequence;
import com.himamis.retex.editor.share.parser.Parser;
import com.himamis.retex.renderer.android.FactoryProviderAndroid;
import com.himamis.retex.renderer.android.graphics.ColorA;
import com.himamis.retex.renderer.android.graphics.Graphics2DA;
import com.himamis.retex.renderer.share.TeXConstants;
import com.himamis.retex.renderer.share.TeXFormula;
import com.himamis.retex.renderer.share.TeXIcon;
import com.himamis.retex.renderer.share.platform.FactoryProvider;
import com.himamis.retex.renderer.share.platform.graphics.GraphicsFactory;
import com.himamis.retex.renderer.share.platform.graphics.Insets;
import java.util.ArrayList;
import java.util.Collections;
public class FormulaEditor extends View implements MathField {
private final static int CURSOR_MARGIN = 5;
// tolerance for cursor color
private final static int CURSOR_TOLERANCE = 10;
protected static MetaModel sMetaModel = new MetaModel();
protected MathFieldInternal mMathFieldInternal;
private TeXIcon mTeXIcon;
private Graphics2DA mGraphics;
private float mSize = 20;
private int mBackgroundColor = Color.TRANSPARENT;
private ColorA mForegroundColor = new ColorA(Color.BLACK);
private int mType = TeXFormula.SERIF;
private String mText;
private float mScale;
private float mMinHeight;
private Parser mParser;
private int iconWidth;
private Canvas mHiddenCanvas;
private Bitmap mHiddenBitmap;
private int mHiddenBitmapW = -1;
private int mHiddenBitmapH = -1;
private int mShiftX = 0;
public FormulaEditor(Context context) {
super(context);
init();
}
public FormulaEditor(Context context, AttributeSet attrs) {
super(context, attrs);
readAttributes(context, attrs, 0);
init();
}
public FormulaEditor(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
readAttributes(context, attrs, defStyleAttr);
init();
}
public void debug(String message) {
//System.out.println(message);
}
@Override
public boolean useCustomPaste() {
return false;
}
private void readAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.FormulaEditor,
defStyleAttr, 0);
try {
mSize = a.getFloat(R.styleable.FormulaEditor_fe_size, 20);
mBackgroundColor = a.getColor(R.styleable.FormulaEditor_fe_backgroundColor, Color.TRANSPARENT);
mForegroundColor = new ColorA(a.getColor(R.styleable.FormulaEditor_fe_foregroundColor, Color.BLACK));
mText = a.getString(R.styleable.FormulaEditor_fe_text);
mType = a.getInteger(R.styleable.FormulaEditor_fe_type, TeXFormula.SANSSERIF);
} finally {
a.recycle();
}
}
protected void init() {
initFactoryProvider();
setFocusable(true);
setFocusableInTouchMode(true);
mScale = getResources().getDisplayMetrics().scaledDensity;
mMathFieldInternal = new MathFieldInternal(this);
mMathFieldInternal.setSize(mSize * mScale);
mMathFieldInternal.setType(mType);
mMathFieldInternal.setFormula(MathFormula.newFormula(sMetaModel));
}
private float getMinHeigth() {
if (mMinHeight == 0) {
TeXIcon tempIcon = new TeXFormula("|").new TeXIconBuilder().setSize(mSize * mScale)
.setStyle(TeXConstants.STYLE_DISPLAY).build();
tempIcon.setInsets(createInsetsFromPadding());
mMinHeight = tempIcon.getIconHeight();
}
return mMinHeight;
}
private void initFactoryProvider() {
if (FactoryProvider.getInstance() == null) {
FactoryProvider.setInstance(new FactoryProviderAndroid(getContext().getAssets()));
}
}
/**
* Sets the color of the text. Must be called from the UI thread.
*
* @param foregroundColor color represented as packed ints
*/
public void setForegroundColor(int foregroundColor) {
mForegroundColor = new ColorA(foregroundColor);
invalidate();
}
/**
* Sets the color of the background. Must be called from the UI thread.
*
* @param backgroundColor color represented as packed ints
*/
public void setBackgroundColor(int backgroundColor) {
mBackgroundColor = backgroundColor;
invalidate();
}
/**
* Sets the text of the view. Must be called from the UI thread.
*
* @param text e.g. x^2
*/
public void setText(Parser parser, String text) {
mParser = parser;
mText = text;
createTeXFormula();
requestLayout();
}
private void createTeXFormula() {
mMathFieldInternal.setFormula(MathFormula.newFormula(sMetaModel, mParser, mText));
}
private Insets createInsetsFromPadding() {
return new Insets(
getPaddingTop(),
getPaddingLeft(),
getPaddingBottom(),
getPaddingRight()
);
}
@Override
public void setTeXIcon(TeXIcon icon) {
mTeXIcon = icon;
mTeXIcon.setInsets(createInsetsFromPadding());
updateShiftX();
}
@Override
public void setFocusListener(FocusListener focusListener) {
setOnFocusChangeListener(new FocusListenerAdapter(focusListener));
}
@Override
public void setClickListener(ClickListener clickListener) {
setOnTouchListener(new ClickListenerAdapter(clickListener, getContext()));
}
@Override
public void setKeyListener(KeyListener keyListener) {
setOnKeyListener(new KeyListenerAdapter(keyListener, this));
}
public void onEnter() {
// used in AlgebraInput
}
public void afterKeyTyped(com.himamis.retex.editor.share.event.KeyEvent keyEvent) {
// used in FormulaInput
}
@Override
public void repaint() {
invalidate();
}
@Override
public boolean hasParent() {
return getParent() != null;
}
public void requestViewFocus() {
requestFocus();
}
protected int getWidthForIconWithPadding() {
return mTeXIcon.getIconWidth();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int desiredWidth = getWidthForIconWithPadding();
final int desiredHeight = (int) (Math.max(getMinHeigth(), mTeXIcon.getIconHeight()) + 0.5);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
drawShifted(canvas, getShiftX());
}
protected void drawShifted(Canvas canvas, int shiftX) {
if (mTeXIcon == null) {
return;
}
if (mGraphics == null) {
mGraphics = new Graphics2DA();
}
// draw background
canvas.drawColor(mBackgroundColor);
int y = Math.round((getMeasuredHeight() - mTeXIcon.getIconHeight()) / 2.0f);
// draw latex
mGraphics.setCanvas(canvas);
mTeXIcon.setForeground(mForegroundColor);
mTeXIcon.paintIcon(null, mGraphics, shiftX, y);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw) {
updateShiftX();
}
}
public void fireInputChangedEvent() {
// implemented in AlgebraInput
}
@Override
public void paste() {
}
@Override
public void copy() {
}
protected int calcCursorX() {
int inputBarWidth = getWidth();
if (inputBarWidth == 0) {
debug("updateShiftX: inputBarWidth == 0");
return -1;
}
debug("mShiftX: " + mShiftX + ", inputBarWidth:" + inputBarWidth);
iconWidth = mTeXIcon.getIconWidth();
int iconHeight = mTeXIcon.getIconHeight();
// check if last shift is not too long
// (e.g. if new formula is shorter)
if (iconWidth + mShiftX < inputBarWidth) {
mShiftX = inputBarWidth - iconWidth;
if (mShiftX > 0) {
mShiftX = 0;
}
debug("shorter formula: mShiftX = " + mShiftX);
}
// find cursor (red pixels) and ensure is visible in view
if (iconWidth > mHiddenBitmapW || iconHeight > mHiddenBitmapH) {
mHiddenBitmap = Bitmap.createBitmap(iconWidth, iconHeight,
Bitmap.Config.ARGB_8888);
mHiddenCanvas = new Canvas(mHiddenBitmap);
mHiddenBitmapW = iconWidth;
mHiddenBitmapH = iconHeight;
debug("==== new Bitmap");
} else {
mHiddenCanvas.drawColor(Color.BLACK);
}
drawShifted(mHiddenCanvas, 0);
int[] pix = new int[iconWidth * iconHeight];
mHiddenBitmap.getPixels(pix, 0, iconWidth, 0, 0, iconWidth, iconHeight);
int pixRed = 0;
int cursorRed = 0;
int index = 0;
for (int y = 0; y < iconHeight; y++) {
for (int x = 0; x < iconWidth; x++) {
int color = pix[index];
int red = Color.red(color);
if (red > GraphicsFactory.CURSOR_RED - CURSOR_TOLERANCE
&& red < GraphicsFactory.CURSOR_RED + CURSOR_TOLERANCE) {
int green = Color.green(color);
if (green > GraphicsFactory.CURSOR_GREEN - CURSOR_TOLERANCE
&& green < GraphicsFactory.CURSOR_GREEN + CURSOR_TOLERANCE) {
int blue = Color.blue(color);
if (blue > GraphicsFactory.CURSOR_BLUE - CURSOR_TOLERANCE
&& blue < GraphicsFactory.CURSOR_BLUE + CURSOR_TOLERANCE) {
pixRed++;
cursorRed += x;
}
}
}
index++;
}
}
// if no red pixel, no cursor: do nothing
if (pixRed == 0) {
return -1;
}
return cursorRed / pixRed;
}
protected void updateShiftX() {
int cursorX = calcCursorX();
if (cursorX < 0) {
return;
}
int inputBarWidth = getWidth();
debug("cursorX: " + cursorX);
int margin = (int) (CURSOR_MARGIN * mScale);
if (cursorX - margin + mShiftX < 0) {
mShiftX = -cursorX + margin;
} else if (cursorX + margin + mShiftX > inputBarWidth) {
mShiftX = inputBarWidth - cursorX - margin;
}
debug("mShiftX: " + mShiftX);
}
public void scroll(int dx, int dy) {
if (isEmpty()) {
return;
}
mShiftX -= dx;
int inputBarWidth = getWidth();
if (iconWidth + mShiftX < inputBarWidth) {
mShiftX = inputBarWidth - iconWidth;
}
if (mShiftX > 0) {
mShiftX = 0;
}
repaint();
}
/**
* @return current shift in x for drawing the formula
*/
public int getShiftX() {
return mShiftX;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
BaseInputConnection fic = new BaseInputConnection(this, false);
outAttrs.actionLabel = null;
outAttrs.inputType = InputType.TYPE_NULL;
outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
return fic;
}
public boolean showKeyboard() {
InputMethodManager imm = (InputMethodManager) getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(this, InputMethodManager.SHOW_FORCED);
return true;
}
public void hideSoftKeyboard() {
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindowToken(), 0);
}
public void showCopyPasteButtons() {
// not implemented here
}
@Override
public MetaModel getMetaModel() {
return sMetaModel;
}
public InputController getInputController() {
return mMathFieldInternal.getInputController();
}
public EditorState getEditorState() {
return mMathFieldInternal.getEditorState();
}
public MathFieldInternal getMathFieldInternal() {
return mMathFieldInternal;
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
FormulaEditorState state = new FormulaEditorState(superState);
EditorState editorState = getEditorState();
state.currentOffset = editorState.getCurrentOffset();
state.currentPath = getCurrentPath(editorState.getCurrentField());
state.rootComponent = editorState.getRootComponent();
return state;
}
private ArrayList<Integer> getCurrentPath(MathComponent component) {
ArrayList<Integer> currentPath = new ArrayList<>();
getPath(component, currentPath);
Collections.reverse(currentPath);
return currentPath;
}
private void getPath(MathComponent component, ArrayList<Integer> path) {
MathContainer container = component.getParent();
if (container != null) {
int index = container.indexOf(component);
path.add(index);
getPath(container, path);
}
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof FormulaEditorState)) {
super.onRestoreInstanceState(state);
return;
}
FormulaEditorState formulaEditorState = (FormulaEditorState) state;
super.onRestoreInstanceState(formulaEditorState.getSuperState());
// Set the formula
MathFormula mathFormula = MathFormula.newFormula(sMetaModel);
mathFormula.setRootComponent(formulaEditorState.rootComponent);
mMathFieldInternal.setFormula(mathFormula);
// Change the editor state
EditorState editorState = getEditorState();
editorState.setRootComponent(formulaEditorState.rootComponent);
editorState.setCurrentField(getCurrentField(formulaEditorState.rootComponent, formulaEditorState.currentPath));
editorState.setCurrentOffset(formulaEditorState.currentOffset);
}
private MathSequence getCurrentField(MathSequence root, ArrayList<Integer> currentPath) {
MathContainer child = root;
for (int i : currentPath) {
child = (MathContainer) child.getArgument(i);
}
return (MathSequence) child;
}
public void hideCopyPasteButtons() {
// implemented in ReTeXInput
}
public boolean isEmpty() {
return mMathFieldInternal.isEmpty();
}
static class FormulaEditorState extends BaseSavedState {
public static final Parcelable.Creator<FormulaEditorState> CREATOR =
new Parcelable.Creator<FormulaEditorState>() {
public FormulaEditorState createFromParcel(Parcel in) {
return new FormulaEditorState(in);
}
public FormulaEditorState[] newArray(int size) {
return new FormulaEditorState[size];
}
};
MathSequence rootComponent;
ArrayList<Integer> currentPath;
Integer currentOffset;
public FormulaEditorState(Parcelable superState) {
super(superState);
}
public FormulaEditorState(Parcel source) {
super(source);
rootComponent = (MathSequence) source.readValue(null);
currentPath = source.readArrayList(Integer.class.getClassLoader());
currentOffset = source.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeValue(rootComponent);
out.writeList(currentPath);
out.writeInt(currentOffset);
}
}
}